- 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>
94 lines
2.9 KiB
Go
94 lines
2.9 KiB
Go
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()
|
|
}
|