# Steward's design Steward is a DHCPv4 server with the same shape as [Jarl](https://gitea.adbraun.lol/adam/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](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. Active leases are pinged every minute too, for a "last seen" time that never takes a client offline ([subnets.md](subnets.md#online-and-last-seen)). * **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-kea` reads a `kea-dhcp4.conf` and 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.