- A reservation can have its own lease time (--lease-time, the reservation dialog), over the subnet's. Schema migration 6. - A reservation with a MAC address and a client ID matches either one (a dual-boot machine sends two client IDs from one MAC); before, it was found by MAC and then refused. A lease of the reserved address by the client's other identity counts as its own. - The Leases page shows a client's client ID; reserving a lease uses both, or one with --by / the dialog's choice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
141 lines
3.5 KiB
Go
141 lines
3.5 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 the reservation is the client's: by its client
|
|
// identifier or its MAC address, either one when the reservation has both
|
|
// (a dual-boot machine sends different client IDs from one MAC address).
|
|
func (cl client) matches(r *model.Reservation) bool {
|
|
return r.ClientID != "" && r.ClientID == cl.id || r.MAC != "" && r.MAC == cl.mac
|
|
}
|
|
|
|
func (cl client) String() string {
|
|
if cl.id != "" {
|
|
return cl.mac + " (client id " + cl.id + ")"
|
|
}
|
|
return cl.mac
|
|
}
|