Subnets served per interface, pools, reservations, options at three levels, leases in SQLite saved before the ACK, ping and decline conflict checks, validated changes with history and undo, users and tokens, an audit log, a Kea importer, a sandboxed systemd unit and Debian packages. Tested with real udhcpc and dhcpcd clients in Docker (scripts/e2e.sh). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4.6 KiB
4.6 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. - 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.