Steward 0.1.0 (in development): a DHCPv4 server with a web UI, API and CLI

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>
This commit is contained in:
abrown38andClaude Opus 5.5 committed 2026-10-14 12:14:07 +01:00
commit 3c0808591f
111 files changed
+17851

No files matched your search

+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env python3
# Writes docs/options.md from the option catalog in internal/dhcp4/catalog.go
# (the options by name, and the reserved codes). Run from the repository
# root after changing the catalog:
#
# python3 scripts/optionsdoc.py
import re
src = open("internal/dhcp4/catalog.go").read()
rows = re.findall(r'\{Code: (\d+), Name: "([^"]+)", Type: (\w+), Help: "((?:[^"\\]|\\.)*)"', src)
types = {"TypeIP": "ip", "TypeIPs": "ips", "TypeString": "string", "TypeU8": "uint8", "TypeU16": "uint16", "TypeU32": "uint32",
"TypeI32": "int32", "TypeBool": "bool", "TypeDomains": "domains", "TypeRoutes": "routes", "TypeHex": "hex"}
reserved = re.findall(r'^\t(\d+): +"([^"]+)",', src, re.M)
out = """# DHCP options
Options are set by name, or by number with a type. Set them in the
settings (every subnet), on a subnet, or on a reservation; the most
specific wins ([subnets.md](subnets.md#options)).
```sh
steward settings set --option domain-name-servers=10.0.10.2,10.0.10.3
steward subnet update office --option routers=10.0.10.1 --unset-option ntp-servers
steward reservation update nas --option 224:string=hello # an option Steward doesn't know
```
`steward option list` prints the list below; the web UI offers it in each
options editor. (This page is written by scripts/optionsdoc.py from the
catalog in internal/dhcp4/catalog.go.)
## Options by name
| Code | Name | Type | What |
|---:|---|---|---|
"""
for code, name, typ, help_ in rows:
help_ = help_.replace('\\"', '"').replace("|", "\\|")
out += f"| {code} | `{name}` | {types[typ]} | {help_} |\n"
out += """
The names are Kea's, so a Kea configuration's options carry over.
## Value types
| Type | How to write it | Example |
|---|---|---|
| ip | an IPv4 address | `10.0.10.255` |
| ips | IPv4 addresses, separated by commas or spaces, in order of preference | `10.0.10.2, 10.0.10.3` |
| string | text, as it is | `lan` |
| uint8, uint16, uint32 | a whole number | `1500` |
| int32 | a whole number, maybe negative | `-3600` |
| bool | `true` or `false` (`yes`/`no`, `on`/`off`, `1`/`0` work too) | `false` |
| domains | domain names, separated by commas or spaces; sent in DNS wire format (RFC 3397) | `lan, home.arpa` |
| routes | `DESTINATION/LENGTH via ROUTER`, separated by commas (Kea's `DEST - ROUTER` works too); RFC 3442 | `10.20.0.0/16 via 10.0.10.1, 0.0.0.0/0 via 10.0.10.1` |
| hex | bytes in hex, with or without colons | `01:02:0a` |
Values are checked when they are set, and stored in a canonical form:
`10.0.10.2,10.0.10.3` becomes `10.0.10.2, 10.0.10.3`.
Clients that use `classless-static-route` ignore `routers`, so give the
default route in it as well (`0.0.0.0/0 via ROUTER`).
## Options by number
An option the list doesn't have is set by its number and type,
`CODE:TYPE=VALUE`; without a type the value is hex:
```sh
steward subnet update office --option 224:string=hello --option 225=01:02
```
In the API, give `{"option": "224", "type": "string", "value": "hello"}`.
## Options Steward sets itself
These can't be set; Steward fills them in (`option_reserved` if you try):
| Code | Why |
|---:|---|
"""
for code, why in reserved:
out += f"| {code} | {why} |\n"
open("docs/options.md", "w").write(out)
print("wrote docs/options.md")