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:
1 parent
b3c8329ae8
commit
3ad2f981d4
30 files changed
+1092
-37
No files matched your search
@@ -5,6 +5,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -247,6 +248,28 @@ func TestLeasesThroughDHCP(t *testing.T) {
|
||||
if len(evs) != 4 || evs[0].Type != "DHCPACK" {
|
||||
t.Errorf("events %+v", evs)
|
||||
}
|
||||
// Details: on their own, or with ?details=1.
|
||||
if len(evs[0].Options) != 0 {
|
||||
t.Errorf("list with options: %+v", evs[0])
|
||||
}
|
||||
var ev api.Event
|
||||
if code := e.call(e.token, "GET", "/events/"+fmt.Sprint(evs[0].ID), nil, &ev); code != 200 || ev.Direction != "out" || len(ev.Options) == 0 {
|
||||
t.Errorf("event: %d %+v", code, ev)
|
||||
}
|
||||
found := false
|
||||
for _, o := range ev.Options {
|
||||
found = found || o.Name == "lease-time" && o.Value == "43200 s (12h)"
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("no lease time in %+v", ev.Options)
|
||||
}
|
||||
e.call(e.token, "GET", "/events?details=1&mac="+mac.String(), nil, &evs)
|
||||
if len(evs[3].Options) == 0 || evs[3].Direction != "in" {
|
||||
t.Errorf("details=1: %+v", evs[3])
|
||||
}
|
||||
if code := e.call(e.token, "GET", "/events/99999", nil, nil); code != 404 {
|
||||
t.Errorf("missing event: %d", code)
|
||||
}
|
||||
// By subnet: its addresses, and DISCOVERs on its interface.
|
||||
e.call(e.token, "POST", "/subnets", api.SubnetInput{Name: ptr("office"), Prefix: ptr("10.0.10.0/24"), Interface: ptr("eth0")}, nil)
|
||||
for ref, want := range map[string]int{"iot": 4, "10.0.20.0/24": 4, "office": 0} {
|
||||
@@ -476,3 +499,31 @@ func TestReserveByClientID(t *testing.T) {
|
||||
t.Errorf("lease time: %d", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccessAPI(t *testing.T) {
|
||||
e := newEnv(t)
|
||||
var mr api.MutationResponse
|
||||
if code := e.call(e.token, "POST", "/subnets", api.SubnetInput{Prefix: ptr("10.0.20.0/24"), Interface: ptr("eth0.20"), Access: ptr("allow-list"), Pools: &[]string{"10.0.20.100-10.0.20.110"}}, &mr); code != 201 || len(mr.Warnings) == 0 || mr.Warnings[0].Code != "allow_list_empty" {
|
||||
t.Fatalf("create: %d %+v", code, mr.Warnings)
|
||||
}
|
||||
in := api.SubnetInput{Allow: &[]model.ClientMatch{{Match: "AA-BB-CC-*", Comment: " sonos "}, {Match: "id 01:AA:BB"}}, Deny: &[]model.ClientMatch{{Match: "aabbcc000001"}}}
|
||||
if code := e.call(e.token, "PATCH", "/subnets/10.0.20.0_24", in, nil); code != 200 {
|
||||
t.Fatalf("patch: %d", code)
|
||||
}
|
||||
sn := e.eng.State().Subnets[0]
|
||||
if sn.Access != "allow-list" || sn.Allow[0] != (model.ClientMatch{Match: "aa:bb:cc:*", Comment: "sonos"}) || sn.Allow[1].Match != "id 01aabb" || sn.Deny[0].Match != "aa:bb:cc:00:00:01" {
|
||||
t.Errorf("subnet %+v %+v %+v", sn.Access, sn.Allow, sn.Deny)
|
||||
}
|
||||
if code := e.call(e.token, "PATCH", "/subnets/10.0.20.0_24", api.SubnetInput{Deny: &[]model.ClientMatch{{Match: "nonsense"}}}, nil); code != 400 {
|
||||
t.Errorf("bad entry: %d", code)
|
||||
}
|
||||
var eb api.ErrorBody
|
||||
if code := e.call(e.token, "PATCH", "/subnets/10.0.20.0_24", api.SubnetInput{Access: ptr("whitelist")}, &eb); code != 422 || eb.Error.Problems[0].Code != "access_mode" {
|
||||
t.Errorf("bad mode: %d %+v", code, eb)
|
||||
}
|
||||
// "deny-list" is the default, stored as "".
|
||||
e.call(e.token, "PATCH", "/subnets/10.0.20.0_24", api.SubnetInput{Access: ptr("deny-list")}, nil)
|
||||
if sn = e.eng.State().Subnets[0]; sn.Access != "" || len(sn.Allow) != 2 {
|
||||
t.Errorf("deny-list keeps the allow list: %+v", sn)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user