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

+28
View File
@@ -15,6 +15,7 @@ import (
"sync/atomic"
"time"
"steward/internal/dhcp4"
"steward/internal/model"
)
@@ -324,6 +325,18 @@ type Event struct {
IP string `json:"ip,omitempty"`
Hostname string `json:"hostname,omitempty"`
Detail string `json:"detail,omitempty"`
// ID numbers the events since stewardd started.
ID uint64 `json:"id"`
// Direction is "in" for a client's message, "out" for Steward's
// answer, "" for an event of Steward's own (CONFLICT, NO ADDRESS).
Direction string `json:"direction,omitempty"`
ClientID string `json:"client_id,omitempty"`
// SubnetID is the subnet that refused a client (DENIED).
SubnetID int64 `json:"subnet_id,omitempty"`
// Fields and Options are the message, in words (GET /events/{id}, or
// ?details=1).
Fields []dhcp4.Field `json:"fields,omitempty"`
Options []dhcp4.Field `json:"options,omitempty"`
}
type events struct {
@@ -331,11 +344,14 @@ type events struct {
max int
list []Event
counts map[string]uint64
lastID uint64
}
func (e *events) add(ev Event) {
e.mu.Lock()
defer e.mu.Unlock()
e.lastID++
ev.ID = e.lastID
e.list = append(e.list, ev)
if len(e.list) > e.max {
e.list = append([]Event(nil), e.list[len(e.list)-e.max:]...)
@@ -362,6 +378,18 @@ func (s *Server) Events(limit int, keep func(Event) bool) []Event {
return out
}
// Event returns one event, with its details.
func (s *Server) Event(id uint64) (Event, bool) {
s.events.mu.Lock()
defer s.events.mu.Unlock()
for i := len(s.events.list) - 1; i >= 0; i-- {
if s.events.list[i].ID == id {
return s.events.list[i], true
}
}
return Event{}, false
}
// Counts returns how many messages of each type were seen and sent since
// the start.
func (s *Server) Counts() map[string]uint64 {