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>
104 lines
4.6 KiB
Markdown
104 lines
4.6 KiB
Markdown
# 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.
|
|
* **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.
|