- 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>
168 lines
4.8 KiB
Go
168 lines
4.8 KiB
Go
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
|
|
}
|