Allow and deny lists per subnet; every option of a DHCP message in words

- A subnet is a deny-list subnet (the default: everyone not denied) or
  an allow-list subnet (only clients allowed or with a reservation);
  the deny list counts in both. Entries are MAC addresses, prefixes
  (aa:bb:cc:*) or client IDs. A refused client gets no answer (DENIED
  in the activity log, with an Allow button) or a NAK on renewal.
  steward access, the subnet's Access tab. Schema migration 7.
- The activity log keeps each message's header fields and options,
  decoded into words (dhcp4.Describe): the parameter request list by
  name, client IDs, lease times. ▸ on an Activity row, steward
  activity show N / --details, GET /events/{id}.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
abrown38andClaude Opus 5.5 committed 2026-10-18 20:27:51 +01:00
1 parent b3c8329ae8
commit 3ad2f981d4
30 files changed
+1092 -37

No files matched your search

+36
View File
@@ -0,0 +1,36 @@
package dhcp4
import (
"net"
"strings"
"testing"
)
func TestDescribe(t *testing.T) {
p := NewRequest(Discover, net.HardwareAddr{0xaa, 0, 0, 0, 0, 1}, 0x1234)
p.Set(OptParamRequest, []byte{1, 3, 6, 224})
p.Set(OptClientID, []byte{1, 0xaa, 0, 0, 0, 0, 1})
p.Set(OptLeaseTime, U32(5400))
p.Set(81, []byte{0x05, 0, 0, 6, 'l', 'a', 'p', 't', 'o', 'p', 0})
fields, opts := p.Describe()
got := map[string]string{}
for _, f := range append(fields, opts...) {
got[f.Name] = f.Value
}
for name, want := range map[string]string{
"transaction ID": "0x00001234",
"message-type": "DHCPDISCOVER",
"parameter-request-list": "subnet-mask (1), routers (3), domain-name-servers (6), option-224 (224)",
"client-identifier": "01:aa:00:00:00:00:01 (type 1: the MAC address aa:00:00:00:00:01)",
"lease-time": "5400 s (1h30m)",
"client-fqdn": "laptop (server updates the A record)",
"client hardware address": "aa:00:00:00:00:01",
} {
if !strings.EqualFold(got[name], want) {
t.Errorf("%s: %q, want %q", name, got[name], want)
}
}
if shortDuration(7*24*3600e9) != "7d" || shortDuration(90e9) != "1m30s" || shortDuration(43200e9) != "12h" {
t.Error("shortDuration")
}
}