Hidden interfaces, last-seen pings, activity by subnet

- interfaces.exclude in steward.yaml (names or shell patterns) leaves
  interfaces out of GET /interfaces, the web UI's picker and
  'steward interface list'. Subnets on them are still served.
- With the ping check on, active leases are pinged every minute; a
  client that answers gets a last-seen time (leases.last_seen,
  migration 4). Not answering never takes a client offline.
- The activity log filters by subnet (--subnet, the Activity page,
  GET /events?subnet=).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
abrown38andClaude Opus 5.5 committed 2026-10-18 17:50:21 +01:00
1 parent 6ad1bb9a0d
commit 5e6c61cc8e
31 files changed
+358 -39

No files matched your search

+3 -3
View File
@@ -62,7 +62,7 @@ Creating answers 201, other changes 200.
|---|---|
| `GET /healthz` | `{"ok": true, "version"}`, without authentication |
| `GET /status` | build, start time, `interfaces` (`listening`, `error`, `addresses`, `subnets`), `subnets` with usage, `problems`, `leases` (counts by state), `messages` (counts by type), `ping_problem` |
| `GET /interfaces` | the host's interfaces, their addresses and subnets |
| `GET /interfaces` | the host's interfaces, their addresses and subnets, leaving out those matching `interfaces.exclude` |
| `GET /catalog` | the option catalog, value types, reserved codes, roles |
### Settings
@@ -109,11 +109,11 @@ settings'.
| | |
|---|---|
| `GET /leases[?subnet=&state=&interface=&q=]` | every lease, with its state now (`active`, `expired`, `released`, `declined`), `subnet` and its reservation; `q` searches address, MAC and host name |
| `GET /leases[?subnet=&state=&interface=&q=]` | every lease, with its state now (`active`, `expired`, `released`, `declined`), `subnet` and its reservation, and `last_seen` when the client last answered a ping (left out if it never has); `q` searches address, MAC and host name |
| `GET /leases/{ip}` | one |
| `DELETE /leases/{ip}` | forget it (audited) |
| `POST /leases/{ip}/reserve` | make it a reservation; optional `{"hostname", "comment"}` |
| `GET /events[?mac=&interface=&limit=]` | the latest DHCP messages, newest first (the last 1000 are kept in memory) |
| `GET /events[?mac=&subnet=&interface=&limit=]` | the latest DHCP messages, newest first (the last 1000 are kept in memory). `subnet` (ID, name or prefix) keeps messages for its addresses, and those without an address on its interface |
### History and import
+7 -5
View File
@@ -75,7 +75,7 @@ one. An option Steward doesn't know: `--option 224:string=hello`.
| Command | |
|---|---|
| `reservation list [--subnet SUBNET]` | reservations, and whether each client is online |
| `reservation list [--subnet SUBNET]` | reservations, whether each client is online, and when it last answered a ping |
| `reservation show RES` | RES is its ID, address or MAC address |
| `reservation add IP MAC [--hostname H] [--client-id HEX] [--option NAME=VALUE]... [--comment C]` | `-` for MAC with `--client-id` |
| `reservation update RES [--ip] [--mac] [--client-id] [--hostname] [--option]... [--unset-option]... [--comment]` | |
@@ -93,9 +93,11 @@ one. An option Steward doesn't know: `--option 224:string=hello`.
### activity
`steward activity [--mac MAC] [--interface I] [--limit N] [-f]`: the latest
DHCP messages, received and sent, oldest first. `-f` keeps watching, which
is useful while a device tries to get an address:
`steward activity [--mac MAC] [--subnet SUBNET] [--interface I] [--limit N] [-f]`:
the latest DHCP messages, received and sent, oldest first. `--subnet`
takes an ID, name or prefix; a message without an address (a DISCOVER,
"no address") shows for every subnet on its interface. `-f` keeps
watching, which is useful while a device tries to get an address:
```
11:02:13.120 eth0 DHCPDISCOVER 3c:22:fb:11:22:33 - adams-macbook
@@ -115,7 +117,7 @@ sent.
| `settings show` | |
| `settings set [--lease-time 1d] [--ping-check on\|off] [--ping-timeout 500ms] [--decline-hold 24h] [--authoritative on\|off] [--option NAME=VALUE]... [--unset-option NAME]...` | |
| `option list` | the options Steward knows ([options.md](options.md)) |
| `interface list` | the server's interfaces, their addresses and subnets |
| `interface list` | the server's interfaces, their addresses and subnets (not those in `interfaces.exclude`) |
### history
+1
View File
@@ -14,6 +14,7 @@ the file when it starts.
| `tls.cert_file`, `tls.key_file` | none | serve HTTPS with this certificate; stewardd rereads it when the file changes, or on `systemctl reload steward` |
| `tls.acme.*` | off | get the certificate from Let's Encrypt, with Jarl publishing the challenge ([certificates.md](certificates.md#settings)) |
| `jarl.server`, `jarl.token_file` (or `jarl.token`), `jarl.ca_cert`, `jarl.insecure` | none | the Jarl server Steward uses for DNS, and its token ([jarl.md](jarl.md)) |
| `interfaces.exclude` | `[]` | interfaces left out of the web UI's interface picker, `steward interface list` and `GET /interfaces`: names or shell patterns, such as `["lo", "docker*", "veth*"]`. A subnet can still name one, and is still served |
| `trusted_proxies` | `[]` | addresses or prefixes of reverse proxies whose `X-Forwarded-For` and `X-Forwarded-Proto` are believed |
| `socket` | `/run/steward/steward.sock` | the local API socket; `""` turns it off |
| `database` | `/var/lib/steward/steward.db` | the SQLite database: configuration, leases, history, users, audit log |
+3 -1
View File
@@ -25,7 +25,9 @@ check or reload.
* **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.
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
+1
View File
@@ -55,6 +55,7 @@ It checks:
* addresses and options per interface, and reservations;
* the ping check, against a container that squats on a pool address;
* last seen: a client that stays up answers pings, one that left has none;
* init-reboot, renewal and release with dhcpcd;
* a client moved to another VLAN: NAK, then that VLAN's address;
* a full pool and a disabled subnet;
+14 -1
View File
@@ -20,7 +20,9 @@ For VLANs, give the server an interface on each, such as `eth0.20`, or a
bridge such as `br-iot`, each with an address in its subnet. That address
is the server identifier clients see, and the one they renew their lease
with. `steward interface list` shows the server's interfaces with their
addresses and subnets.
addresses and subnets. To keep interfaces such as `lo`, `docker0` or
`veth*` out of that list and the web UI's picker, name them in
`interfaces.exclude` in steward.yaml ([configuration.md](configuration.md)).
An interface that doesn't exist yet is fine. The subnet gets a warning
(`interface_unknown`), and stewardd starts serving it within 30 seconds of
@@ -143,6 +145,17 @@ then is the ACK sent.
`steward lease list` shows active and held-back leases; `--all` shows
the others too. Leases that ended more than 30 days ago are deleted.
### Online and last seen
A client is **online** while its lease is active. With the ping check on
(the default), Steward also pings every active lease's address once a
minute, and a client that answers gets a **last seen** time: on the
Leases page, next to a reservation's online badge, and in `steward lease
list`. Many devices block pings (Windows' firewall does by default), so a
client that doesn't answer stays online and simply has no last-seen time,
or keeps the one it had. The time stays with the lease when it is
renewed, and is kept across restarts.
`steward lease delete IP` frees an address at once. A client still using
it gets a NAK when it next renews, then asks for a new address.