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
@@ -0,0 +1,93 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Subnet access modes: who a subnet answers.
|
||||
const (
|
||||
// AccessDenyList answers every client except those on the deny list
|
||||
// (the default, stored as "").
|
||||
AccessDenyList = "deny-list"
|
||||
// AccessAllowList answers only clients on the allow list or with a
|
||||
// reservation in the subnet, and not on the deny list.
|
||||
AccessAllowList = "allow-list"
|
||||
)
|
||||
|
||||
// ClientMatch is an entry of a subnet's allow or deny list: a MAC address
|
||||
// ("aa:bb:cc:dd:ee:ff"), a MAC address prefix for a maker's devices
|
||||
// ("aa:bb:cc:*"), or a client identifier ("id 01aabbccddeeff").
|
||||
type ClientMatch struct {
|
||||
Match string `json:"match"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// ParseClientMatch reads and normalizes an allow or deny list entry.
|
||||
func ParseClientMatch(s string) (string, error) {
|
||||
s = strings.ToLower(strings.TrimSpace(s))
|
||||
if rest, ok := strings.CutPrefix(s, "id"); ok && (strings.HasPrefix(rest, " ") || strings.HasPrefix(rest, ":") || strings.HasPrefix(rest, "=")) {
|
||||
id, err := NormalizeClientID(rest[1:])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "id " + id, nil
|
||||
}
|
||||
if prefix, ok := strings.CutSuffix(s, "*"); ok {
|
||||
prefix = strings.TrimRight(strings.ReplaceAll(prefix, "-", ":"), ":")
|
||||
parts := strings.Split(prefix, ":")
|
||||
if prefix == "" || len(parts) > 5 {
|
||||
return "", fmt.Errorf("%q: a MAC address prefix has 1 to 5 bytes, as aa:bb:cc:*", s)
|
||||
}
|
||||
for i, p := range parts {
|
||||
if len(p) == 1 {
|
||||
p = "0" + p
|
||||
}
|
||||
if len(p) != 2 || strings.Trim(p, "0123456789abcdef") != "" {
|
||||
return "", fmt.Errorf("%q: a MAC address prefix is written as aa:bb:cc:*", s)
|
||||
}
|
||||
parts[i] = p
|
||||
}
|
||||
return strings.Join(parts, ":") + ":*", nil
|
||||
}
|
||||
mac, err := NormalizeMAC(s)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%q is not a MAC address (aa:bb:cc:dd:ee:ff), a prefix (aa:bb:cc:*) or a client ID (id 01aabbccddeeff)", s)
|
||||
}
|
||||
return mac, nil
|
||||
}
|
||||
|
||||
// Matches reports whether the entry is the client's: mac as
|
||||
// "aa:bb:cc:dd:ee:ff", id the client identifier in hex ("" for none).
|
||||
func (m ClientMatch) Matches(mac, id string) bool {
|
||||
switch {
|
||||
case strings.HasPrefix(m.Match, "id "):
|
||||
return id != "" && m.Match[3:] == id
|
||||
case strings.HasSuffix(m.Match, "*"):
|
||||
return mac != "" && strings.HasPrefix(mac, m.Match[:len(m.Match)-1])
|
||||
}
|
||||
return mac != "" && m.Match == mac
|
||||
}
|
||||
|
||||
// Admits says whether the subnet answers a client, and if not why. resv
|
||||
// tells whether the client has an (enabled) reservation in the subnet.
|
||||
func (s *Subnet) Admits(mac, id string, resv bool) (bool, string) {
|
||||
for _, m := range s.Deny {
|
||||
if m.Matches(mac, id) {
|
||||
why := "on the deny list of " + s.Label()
|
||||
if m.Comment != "" {
|
||||
why += " (" + m.Comment + ")"
|
||||
}
|
||||
return false, why
|
||||
}
|
||||
}
|
||||
if s.Access != AccessAllowList || resv {
|
||||
return true, ""
|
||||
}
|
||||
for _, m := range s.Allow {
|
||||
if m.Matches(mac, id) {
|
||||
return true, ""
|
||||
}
|
||||
}
|
||||
return false, "not on the allow list of " + s.Label()
|
||||
}
|
||||
Reference in new issue
Block a user