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,167 @@
|
||||
package dhcp4
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Field is a message's header field or option, in words: for the activity
|
||||
// log's details.
|
||||
type Field struct {
|
||||
// Code is the option's number (0 for a header field).
|
||||
Code int `json:"code,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// protocol are the options the server and clients set themselves, which
|
||||
// the catalog leaves out because they can't be configured.
|
||||
var protocol = map[byte]Def{
|
||||
1: {Name: "subnet-mask", Type: TypeIP},
|
||||
12: {Name: "host-name", Type: TypeString},
|
||||
50: {Name: "requested-address", Type: TypeIP},
|
||||
51: {Name: "lease-time", Type: "seconds"},
|
||||
52: {Name: "option-overload", Type: TypeU8},
|
||||
53: {Name: "message-type", Type: "message-type"},
|
||||
54: {Name: "server-identifier", Type: TypeIP},
|
||||
55: {Name: "parameter-request-list", Type: "codes"},
|
||||
56: {Name: "message", Type: TypeString},
|
||||
57: {Name: "max-message-size", Type: TypeU16},
|
||||
58: {Name: "renewal-time", Type: "seconds"},
|
||||
59: {Name: "rebinding-time", Type: "seconds"},
|
||||
61: {Name: "client-identifier", Type: "client-id"},
|
||||
77: {Name: "user-class", Type: TypeHex},
|
||||
81: {Name: "client-fqdn", Type: "fqdn"},
|
||||
82: {Name: "relay-agent-information", Type: TypeHex},
|
||||
116: {Name: "auto-configure", Type: TypeU8},
|
||||
118: {Name: "subnet-selection", Type: TypeIP},
|
||||
}
|
||||
|
||||
// optionDef is an option's definition for display.
|
||||
func optionDef(code byte) Def {
|
||||
if d, ok := protocol[code]; ok {
|
||||
d.Code = code
|
||||
return d
|
||||
}
|
||||
return ByCode(code)
|
||||
}
|
||||
|
||||
// OptionName is an option's name: "routers", "parameter-request-list",
|
||||
// "option-224".
|
||||
func OptionName(code byte) string { return optionDef(code).Name }
|
||||
|
||||
// DescribeOption is an option's value in words.
|
||||
func DescribeOption(code byte, b []byte) string {
|
||||
d := optionDef(code)
|
||||
switch d.Type {
|
||||
case "seconds":
|
||||
if len(b) == 4 {
|
||||
s := binary.BigEndian.Uint32(b)
|
||||
return fmt.Sprintf("%d s (%s)", s, shortDuration(time.Duration(s)*time.Second))
|
||||
}
|
||||
case "message-type":
|
||||
if len(b) == 1 {
|
||||
return MessageType(b[0]).String()
|
||||
}
|
||||
case "codes":
|
||||
names := make([]string, len(b))
|
||||
for i, c := range b {
|
||||
names[i] = OptionName(c) + " (" + strconv.Itoa(int(c)) + ")"
|
||||
}
|
||||
return strings.Join(names, ", ")
|
||||
case "client-id":
|
||||
hexed := Decode(TypeHex, b)
|
||||
switch {
|
||||
case len(b) == 7 && b[0] == 1:
|
||||
return hexed + " (type 1: the MAC address " + Decode(TypeHex, b[1:]) + ")"
|
||||
case len(b) > 5 && b[0] == 255:
|
||||
return hexed + " (RFC 4361: IAID " + Decode(TypeHex, b[1:5]) + " and a DUID)"
|
||||
}
|
||||
return hexed
|
||||
case "fqdn":
|
||||
// Flags, two obsolete RCODEs, then the name: in DNS wire format
|
||||
// when the E flag is set, else as text.
|
||||
if len(b) >= 3 {
|
||||
name := string(b[3:])
|
||||
if b[0]&0x04 != 0 {
|
||||
if names, err := decodeNames(b[3:]); err == nil && len(names) > 0 {
|
||||
name = names[0]
|
||||
}
|
||||
}
|
||||
var flags []string
|
||||
if b[0]&0x01 != 0 {
|
||||
flags = append(flags, "server updates the A record")
|
||||
}
|
||||
if b[0]&0x08 != 0 {
|
||||
flags = append(flags, "no DNS updates")
|
||||
}
|
||||
if len(flags) == 0 {
|
||||
flags = append(flags, "client updates the A record")
|
||||
}
|
||||
return name + " (" + strings.Join(flags, ", ") + ")"
|
||||
}
|
||||
default:
|
||||
return Decode(d.Type, b)
|
||||
}
|
||||
return Decode(TypeHex, b)
|
||||
}
|
||||
|
||||
// shortDuration writes 43200s as "12h", 90s as "1m30s", a week as "7d".
|
||||
func shortDuration(d time.Duration) string {
|
||||
if d == 0 {
|
||||
return "0s"
|
||||
}
|
||||
var out string
|
||||
for _, u := range []struct {
|
||||
d time.Duration
|
||||
name string
|
||||
}{{24 * time.Hour, "d"}, {time.Hour, "h"}, {time.Minute, "m"}, {time.Second, "s"}} {
|
||||
if n := d / u.d; n > 0 {
|
||||
out += strconv.Itoa(int(n)) + u.name
|
||||
d -= n * u.d
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Describe lists a message's header fields that are set, and its options
|
||||
// in the order they are in the message.
|
||||
func (p *Packet) Describe() (fields, options []Field) {
|
||||
add := func(name, value string) { fields = append(fields, Field{Name: name, Value: value}) }
|
||||
add("transaction ID", fmt.Sprintf("0x%08x", p.XID))
|
||||
add("client hardware address", p.MAC().String())
|
||||
if !IsZero(p.CIAddr) {
|
||||
add("client address (ciaddr)", p.CIAddr.String())
|
||||
}
|
||||
if !IsZero(p.YIAddr) {
|
||||
add("your address (yiaddr)", p.YIAddr.String())
|
||||
}
|
||||
if !IsZero(p.SIAddr) {
|
||||
add("next server (siaddr)", p.SIAddr.String())
|
||||
}
|
||||
if !IsZero(p.GIAddr) {
|
||||
add("relay agent (giaddr)", p.GIAddr.String())
|
||||
}
|
||||
if p.Broadcast() {
|
||||
add("flags", "broadcast (the client wants replies broadcast)")
|
||||
}
|
||||
if p.Secs != 0 {
|
||||
add("seconds since the client began", strconv.Itoa(int(p.Secs)))
|
||||
}
|
||||
if p.Hops != 0 {
|
||||
add("hops", strconv.Itoa(int(p.Hops)))
|
||||
}
|
||||
if p.SName != "" {
|
||||
add("server name (sname)", p.SName)
|
||||
}
|
||||
if p.File != "" {
|
||||
add("boot file (file)", p.File)
|
||||
}
|
||||
for _, o := range p.Options {
|
||||
options = append(options, Field{Code: int(o.Code), Name: OptionName(o.Code), Value: DescribeOption(o.Code, o.Data)})
|
||||
}
|
||||
return fields, options
|
||||
}
|
||||
Reference in new issue
Block a user