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
Steward's design
Steward is a DHCPv4 server with the same shape as
Jarl: one daemon, stewardd, that
answers DHCP and serves a REST API and a web UI; one CLI, steward, that
talks to that API; SQLite for everything. Unlike Jarl, which manages
BIND, Steward is the server: there is no other program to configure,
check or reload.
Scope of 0.1.0
- IPv4 only. DHCPv6 can come later in the same daemon.
- Subnets per interface. Every subnet names the network interface it is
served on (
eth0,eth0.10,br-lan). A request is only answered from the subnets of the interface it arrived on, so a VLAN's clients never get another VLAN's addresses. An interface may carry several subnets (a shared network): addresses are then handed out from them in turn. - Pools: address ranges in a subnet that are handed out to any client.
- Reservations: a fixed address (and optionally a host name and options) for a MAC address or client identifier.
- Options: the common ones by name (
routers,domain-name-servers,domain-name,domain-search,ntp-servers, …, see options.md), any other by number. Set globally, per subnet and per reservation; the most specific wins. - Leases in SQLite, written before the client is answered, so a crash or restart never hands out an address twice.
- Conflict checks: an address is pinged before it is offered; one that answers, or that a client declines, is held back for a while.
- The Jarl way of working: an API used by both the CLI and the web UI; users, sessions and API tokens; every configuration change validated as a whole before it is saved and used; a history of changes that can be undone; an audit log.
- Moving from Kea:
stewardd import-keareads akea-dhcp4.confand its lease file, so switching doesn't renumber anything. - A systemd unit and Debian packages, as for Jarl.
Not in 0.1.0: relays (requests with giaddr, answered from the relay's
subnet), DHCPv6, failover between two servers, client classes, and names
in DNS through Jarl (that needs dynamic records in Jarl first).
How it fits together
cmd/stewardd the daemon: serve, init, import-kea, user, backup
cmd/steward the CLI
internal/dhcp4 DHCPv4 messages and the option catalog (no I/O)
internal/model the configuration (State: settings, subnets, reservations), leases, users
internal/validate whole-configuration checks, with stable codes
internal/store SQLite: configuration, leases, history, users, tokens, sessions, audit
internal/engine configuration changes: validate, save, record history, undo
internal/dhcpd the DHCP server: sockets per interface, the protocol, address allocation
internal/server the HTTP API and the web UI
internal/web the web UI's files (plain ES modules, embedded)
internal/kea the Kea importer
Configuration changes
The configuration is small, so it is held in memory as a model.State
and changed as a whole. An API call becomes a list of changes, each one
"object X was A and becomes B" (B empty for a delete, A empty for a
create). The engine applies them to a copy of the state, validates the
result, and only if that passes saves the objects and a history entry in
one transaction, then hands the new state to the DHCP server. Undoing a
history entry applies its changes backwards, after checking that the
objects are still as the entry left them.
Answering DHCP
dhcpd opens one UDP socket on port 67 per interface that has a subnet,
bound to that interface (SO_BINDTODEVICE). The interface a request
arrived on picks the subnets. The server identifier is the interface's
own address in the subnet.
For a DISCOVER the address is, in order: the client's reservation; the address it had before (its lease, even expired, so clients keep their address); the address it asks for, if free; the first free address in the pools (addresses never handed out first, then the one released longest ago). Reserved addresses are never handed out to anyone else. The offer is held for a minute.
A REQUEST is answered as RFC 2131 describes for each client state (selecting, init-reboot, renewing, rebinding). The lease is saved before the ACK is sent. Steward is authoritative for its subnets: a client asking for an address that doesn't belong on its interface gets a NAK, so it starts over at once instead of waiting for its old lease to run out.
Replies go to the client's address when it has one (renewals), and otherwise as a broadcast on the interface. That works with every client and needs no raw sockets.