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>
5.2 KiB
Developing Steward
Layout
cmd/stewardd/ the daemon: serve, init, user, check, backup, restore, config
cmd/steward/ the CLI
internal/
dhcp4/ DHCPv4 messages (RFC 2131/2132/3396) and the option catalog; no I/O
model/ the configuration (State: settings, subnets, reservations), leases, users, history
validate/ whole-configuration checks with stable codes (docs/validation.md)
store/ SQLite (modernc.org/sqlite, no cgo): configuration, leases, history, users, tokens, sessions, audit
engine/ configuration changes: edit a copy, diff, validate, save with history, undo
dhcpd/ the DHCP server: sockets per interface, the protocol, address choice, leases, ping check, activity
server/ the HTTP API and the web UI, authentication, the local socket
api/, client/ JSON types and the Go client shared by server and CLI
kea/ the Kea configuration and lease file importer
auth/ passwords (argon2id), tokens, sessions, grants and roles
config/ stewardd's YAML configuration
version/ what a build reports
web/static/ the web UI: plain ES modules, no build step, embedded with go:embed
deploy/ systemd units, sysusers.d and tmpfiles.d files, Debian package scripts
scripts/e2e.sh end-to-end test with real DHCP clients in Docker
scripts/mkdeb.sh the Debian packages (make deb)
scripts/logo.py the web UI's logo from artwork/logo.svg
scripts/optionsdoc.py docs/options.md from the option catalog
artwork/logo.svg the logo's master
design.md describes how the pieces fit.
Building and testing
make # bin/stewardd, bin/steward, bash completion
make test # go vet, gofmt check, go test ./...
make e2e # scripts/e2e.sh: needs Docker
make deb # dist/steward_<version>_<arch>.deb and steward-cli_...
Unit tests cover the protocol without sockets. dhcpd.Server.Handle
takes a parsed message and an interface name and returns the reply, with
fake interfaces, a fake pinger and a fake clock
(internal/dhcpd/handle_test.go). The API tests drive real DHCP exchanges
through Handle too.
The e2e test runs the real thing:
- stewardd in a
debian:trixiecontainer on two Docker bridge networks (eth0 and eth1, like two VLANs); - busybox
udhcpcanddhcpcdclients in Alpine containers.
It checks:
- addresses and options per interface, and reservations;
- the ping check, against a container that squats on a pool address;
- init-reboot, renewal and release with dhcpcd;
- a client moved to another VLAN: NAK, then that VLAN's address;
- a full pool and a disabled subnet;
- history and undo, and leases across a restart;
- the API with tokens, and backups.
KEEP=1 leaves the containers running; DOCKER_HOST points at another
Docker daemon. Root isn't needed on the host.
stewardd can't serve port 67 unprivileged, so a stewardd run by hand
outside a container shows permission denied for its interfaces. The
API and web UI still work, which is enough for UI work. The systemd
sandbox is only tested by installing the package in a container that
runs systemd (--privileged /sbin/init) and pointing a client container
at it.
The web UI
Plain JavaScript modules under internal/web/static/, built with the
same helpers as Jarl's:
- All DOM goes through
h()inui.js, neverinnerHTML. - The Content-Security-Policy allows only the app's own scripts and
styles, so no inline styles. Use a class in
style.css, or setel.style.prop. - The files are embedded, so rebuild stewardd after changing them.
The logo's master is artwork/logo.svg; scripts/logo.py writes
assets/logo.svg (the favicon) and assets/logo.js (drawn inline by
brand.js). The STEWARD wordmark is drawn by wordmark() in brand.js,
in the letters of Jarl's wordmark.
Conventions
- Every user-visible change goes into
CHANGELOG.mdunder "Unreleased", with the docs updated in the same change. - Errors say what is wrong and how to fix it, naming the object.
- New validation rules get a stable code, a test in
internal/validate/validate_test.goand a row in validation.md. - Configuration changes go through
engine.Apply, never straight to the store, so they are validated, recorded and undoable. - Schema changes append a migration to
internal/store/store.go; never edit a shipped one. gofmtandgo vetclean (make test).
Releasing
As for Jarl:
- Move "Unreleased" in
CHANGELOG.mdunder## [<version>] - <date>. A release candidate may leave it there. - Put the version in
VERSION, and update the README's status line. - Run
make test && make e2e. - Commit as "Release X", tag it
git tag -a vX -m "Steward X", and push.
The tag starts .gitea/workflows/release.yml:
- it checks the tag against
VERSIONand the CHANGELOG; - it runs
make test; - it builds the tarballs, the CLI for each OS and the
.debs (amd64, arm64); scripts/gitea-release.shpublishes the release, and uploads the.debs to the registry:stable, ortestingfor a version with a hyphen.
It needs the repository secret RELEASE_TOKEN (write:repository,
write:package).