# Working on Steward Notes for coding agents (and people) changing this repository. Read this first, then [docs/design.md](docs/design.md) (how it fits together and what 0.1 leaves out) and [docs/development.md](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`** (or `Revert`, `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`, with `internal/jarl` for 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.sh` in `../jarl`) with Pebble pointed at its named (docs/development.md). * **Leases are not configuration.** `dhcpd` owns 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.md`** under `## [Unreleased]`, and the matching `docs/` page in the same change. New validation codes get a row in `docs/validation.md`; new options go in `internal/dhcp4/catalog.go`; then run `python3 scripts/optionsdoc.py` to regenerate `docs/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 test` runs `go vet` and a `gofmt -l` check. `goimports` isn't installed, so fix imports by hand. ## Testing ```sh 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 prints `RESULT: N passed, 0 failed`; run it after any change to `dhcpd`, `engine`, `server` or 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 with `listen: ["127.0.0.1:28067"]` and `socket: /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 a `debian:trixie` container 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.js` with 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()` from `ui.js`, never `innerHTML`. * The CSP is `self` only: no inline styles or scripts. Use a class, or set `el.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 (`ciaddr` set) are unicast. * `SO_BINDTODEVICE` needs no capability since Linux 5.7. The ping check uses an unprivileged ICMP socket, allowed by `net.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 is `vX` with message "Steward X". ## Shell hygiene * **Never kill processes by pattern** (`pkill -f`). The pattern matches the shell running it. Find the PID with `ps` and kill that. * **Run long-lived test servers in the background mode of your tool**, not with `&` in a shell that exits.