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] } // admits says whether the subnet answers the client (its allow and deny // lists), and if not why. func (c *config) admits(sub *subnet, cl client) (bool, string) { return sub.Admits(cl.mac, cl.id, c.reservationFor(sub.ID, cl) != nil) } // 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 }