Files
steward/internal/dhcpd/config.go
T
abrown38andClaude Opus 5.5 6ad1bb9a0d
release / release (push) Successful in 5m53s
Disable reservations; subnet enable/disable buttons; page-head CSS fix
- A disabled reservation is kept but ignored: the client gets a pool
  address like any other (steward reservation disable|enable, buttons and
  a checkbox in the web UI). Schema migration 3.
- Subnets get Disable/Enable buttons on their page and in the list.
- A rule left without its selector in style.css broke the page heading's
  layout, so the New ... buttons sat cramped under the description.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 16:02:15 +01:00

143 lines
3.4 KiB
Go

package dhcpd
import (
"net/netip"
"time"
"steward/internal/dhcp4"
"steward/internal/model"
)
// config is a configuration prepared for answering requests: subnets by
// interface, reservations indexed, options encoded.
type config struct {
state *model.State
byIface map[string][]*subnet
// resByIP holds every reservation by address.
resByIP map[netip.Addr]*model.Reservation
// resByClient holds reservations per subnet by "mac aa:..." or
// "id 01aa...".
resByClient map[int64]map[string]*model.Reservation
settings model.Settings
}
type subnet struct {
*model.Subnet
network, broadcast netip.Addr
leaseTime time.Duration
// options are the encoded options of the settings and the subnet,
// the subnet's winning.
options map[byte][]byte
}
func compile(st *model.State) *config {
c := &config{
state: st,
byIface: map[string][]*subnet{},
resByIP: map[netip.Addr]*model.Reservation{},
resByClient: map[int64]map[string]*model.Reservation{},
settings: st.Settings,
}
global := encodeOptions(st.Settings.Options, nil)
for _, s := range st.Subnets {
if s.Disabled || s.Interface == "" || !s.Prefix.Addr().Is4() {
continue
}
lt := s.LeaseTime
if lt <= 0 {
lt = st.Settings.EffectiveLeaseTime()
}
sc := &subnet{Subnet: s, network: s.Prefix.Addr(), broadcast: model.Last(s.Prefix), leaseTime: time.Duration(lt) * time.Second,
options: encodeOptions(s.Options, global)}
c.byIface[s.Interface] = append(c.byIface[s.Interface], sc)
}
for _, r := range st.Reservations {
if r.Disabled {
continue
}
c.resByIP[r.IP] = r
s := st.SubnetFor(r.IP)
if s == nil {
continue
}
if c.resByClient[s.ID] == nil {
c.resByClient[s.ID] = map[string]*model.Reservation{}
}
if r.ClientID != "" {
c.resByClient[s.ID]["id "+r.ClientID] = r
}
if r.MAC != "" {
c.resByClient[s.ID]["mac "+r.MAC] = r
}
}
return c
}
// encodeOptions encodes options over a base set (copied).
func encodeOptions(opts []model.OptionValue, base map[byte][]byte) map[byte][]byte {
out := map[byte][]byte{}
for k, v := range base {
out[k] = v
}
for _, o := range opts {
if o.Code <= 0 || o.Code >= 255 {
continue
}
def := dhcp4.ByCode(byte(o.Code))
typ := def.Type
if !dhcp4.Known(byte(o.Code)) && o.Type != "" {
typ = o.Type
}
if b, err := dhcp4.Encode(typ, o.Value); err == nil {
out[byte(o.Code)] = b
}
}
return out
}
// reservationFor finds the client's reservation in a subnet.
func (c *config) reservationFor(subnetID int64, cl client) *model.Reservation {
m := c.resByClient[subnetID]
if m == nil {
return nil
}
if cl.id != "" {
if r := m["id "+cl.id]; r != nil {
return r
}
}
return m["mac "+cl.mac]
}
// subnetFor finds the interface's subnet holding a.
func (c *config) subnetFor(iface string, a netip.Addr) *subnet {
for _, s := range c.byIface[iface] {
if s.Prefix.Contains(a) {
return s
}
}
return nil
}
// client identifies who sent a request.
type client struct {
mac string // aa:bb:cc:dd:ee:ff
id string // client identifier (option 61) in hex, or ""
}
// matches reports whether a reservation is for the client: by client
// identifier when the reservation has one, else by MAC address.
func (cl client) matches(r *model.Reservation) bool {
if r.ClientID != "" {
return r.ClientID == cl.id
}
return r.MAC == cl.mac
}
func (cl client) String() string {
if cl.id != "" {
return cl.mac + " (client id " + cl.id + ")"
}
return cl.mac
}