tls.acme names the certificate and jarl: the Jarl server and a token that may only write the challenge record. Steward gets the certificate with DNS-01 (lego), writing the record through Jarl's API into every view that has the zone, renews it with a third of its lifetime left and swaps it in without a restart. A check runs before every order: Jarl, the zone, the token's permission, a test record, the zone's name servers, public resolvers, clean-up; each step says what to fix. Staging by default. With a certificate, each listener serves HTTPS and plain HTTP on one port (redirecting plain HTTP, except from the server itself). steward cert / cert check / cert renew, Server -> Certificate in the web UI, GET /certificate, POST /certificate/check|renew. docs/certificates.md and docs/jarl.md (the Jarl user and the token's grant). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
5.0 KiB
5.0 KiB
Working on Steward
Notes for coding agents (and people) changing this repository. Read this first, then docs/design.md (how it fits together and what 0.1 leaves out) and docs/development.md (layout, tests, releasing).
Steward is a DHCPv4 server: stewardd answers DHCP and serves a REST API
and a web UI; steward is the CLI; SQLite holds everything. It is the
companion of Jarl (the BIND manager, ../jarl) and copies its ways:
the same auth, socket and web UI helpers, and the same release process.
When in doubt, do it the way Jarl does. Go module steward,
CGO_ENABLED=0, Linux only for stewardd.
Ground rules
- Configuration changes go through
engine.Apply(orRevert,Replace). It edits a copy of the state, diffs it, validates the whole result, saves the objects and a history entry in one transaction, then hands the new state to the DHCP server. Never write subnets, reservations or settings to the store from a handler. - The web UI's certificate (
internal/certs, withinternal/jarlfor Jarl's API) is obtained through lego, with Jarl publishing the DNS-01 challenge. A preflight check runs before every order. Test it against Jarl's e2e environment (KEEP=1 PEBBLE=0 scripts/e2e.shin../jarl) with Pebble pointed at its named (docs/development.md). - Leases are not configuration.
dhcpdowns them: kept in memory, and written to the store (PutLease) before the ACK is sent. They are not in the history. - Subnets are per interface. A request is only answered from the subnets of the interface it came in on. Keep that true in every code path.
- Every user-visible change updates
CHANGELOG.mdunder## [Unreleased], and the matchingdocs/page in the same change. New validation codes get a row indocs/validation.md; new options go ininternal/dhcp4/catalog.go; then runpython3 scripts/optionsdoc.pyto regeneratedocs/options.md. - Errors say what is wrong and how to fix it, naming the object.
- Schema changes append a migration to
internal/store/store.go; never edit a shipped one. make testrunsgo vetand agofmt -lcheck.goimportsisn't installed, so fix imports by hand.
Testing
make test # unit tests: the protocol through dhcpd.Server.Handle with fakes, API via httptest
make e2e # real udhcpc and dhcpcd clients against stewardd in Docker (needs Docker)
- Docker on this machine needs
DOCKER_HOST=unix:///var/run/docker.sock. The e2e test printsRESULT: N passed, 0 failed; run it after any change todhcpd,engine,serveror the CLI. - Port 67 needs
CAP_NET_BIND_SERVICE. A stewardd run by hand shows its interfaces as "not serving: permission denied", but the API and web UI work, which is fine for UI work. Use a scratch config withlisten: ["127.0.0.1:28067"]andsocket: /run/user/$UID/...; socket paths are limited to 107 bytes. - The systemd sandbox is only tested by a package test: run
make deb, install it in adebian:trixiecontainer running systemd (--privileged --cgroupns=host /sbin/init) on a Docker network, and run a client container on that network. - The web UI has no JS tests. Bundle
assets/app.jswith esbuild to catch syntax and import errors, and look at it in headless Chrome.
Web UI rules
As in Jarl:
- Build DOM only with
h()fromui.js, neverinnerHTML. - The CSP is
selfonly: no inline styles or scripts. Use a class, or setel.style.prop. - Rebuild stewardd after editing, since the files are embedded.
- Function over polish. Leave pick-list checkboxes unticked by default.
DHCP facts worth knowing
- Replies to clients without an address are broadcast on the interface
(255.255.255.255:68 through the interface's socket). That works with
every client and needs no raw socket. Renewals (
ciaddrset) are unicast. SO_BINDTODEVICEneeds no capability since Linux 5.7. The ping check uses an unprivileged ICMP socket, allowed bynet.ipv4.ping_group_range.- Renewal REQUESTs often have no host name: keep the lease's previous one.
- dhcpcd sends a DUID-based client identifier, busybox udhcpc none. Leases match by client ID when both sides have one, else by MAC address.
- A container's interface names follow the order the networks were attached in (eth0 first).
Git and releases
The maintainer's rules:
- Commit only when asked. Never push. The maintainer pushes, to Gitea at gitea.adbraun.lol.
- Branches are
dev-X.Y.Z, never named like a tag. - A release is a commit "Release X": it sets
VERSION, updates the README status line, and (for a final release) moves[Unreleased]to## [X] - YYYY-MM-DD. The annotated tag isvXwith message "Steward X".
Shell hygiene
- Never kill processes by pattern (
pkill -f). The pattern matches the shell running it. Find the PID withpsand kill that. - Run long-lived test servers in the background mode of your tool, not
with
&in a shell that exits.