// Package dhcpd is Steward's DHCPv4 server: it listens on the interfaces // that have subnets, answers requests from those subnets only, and keeps // the leases (docs/design.md, "Answering DHCP"). package dhcpd import ( "context" "errors" "log/slog" "net" "net/netip" "sort" "sync" "sync/atomic" "time" "steward/internal/model" ) // LeaseStore persists leases. type LeaseStore interface { LoadLeases(ctx context.Context) ([]*model.Lease, error) PutLease(ctx context.Context, l *model.Lease) error DeleteLease(ctx context.Context, ip netip.Addr) error PruneLeases(ctx context.Context, before time.Time) (int64, error) SetLastSeen(ctx context.Context, seen map[netip.Addr]time.Time) error } // Interfaces reports a network interface's IPv4 addresses with their // prefix lengths (10.0.10.1/24); an error when it doesn't exist. type Interfaces interface { Addrs(name string) ([]netip.Prefix, error) } // Pinger checks whether an address answers (conflict detection, and // clients' last-seen times). type Pinger interface { Ping(ctx context.Context, a netip.Addr, timeout time.Duration) bool } // How long an offer holds its address for the client. const offerHold = time.Minute // Leases that ended this long ago are deleted (until then, a client that // comes back gets its old address if it is free). const keepEnded = 30 * 24 * time.Hour // Server answers DHCP requests. type Server struct { log *slog.Logger store LeaseStore ifaces Interfaces pinger Pinger now func() time.Time // ServerPort and ClientPort are 67 and 68 (tests use others). ServerPort, ClientPort int cfg atomic.Pointer[config] mu sync.Mutex leases map[netip.Addr]*model.Lease offers map[netip.Addr]offer events events warned sync.Map // throttled log lines: key -> time.Time listen listeners changed chan struct{} } type offer struct { cl client expires time.Time } // New makes a server. ifaces and pinger may be nil (the host's interfaces; // no ping check). func New(log *slog.Logger, st LeaseStore, ifaces Interfaces, pinger Pinger) *Server { if log == nil { log = slog.Default() } if ifaces == nil { ifaces = &HostInterfaces{} } s := &Server{log: log, store: st, ifaces: ifaces, pinger: pinger, now: time.Now, ServerPort: 67, ClientPort: 68, leases: map[netip.Addr]*model.Lease{}, offers: map[netip.Addr]offer{}, changed: make(chan struct{}, 1)} s.cfg.Store(compile(&model.State{Settings: model.DefaultSettings()})) s.events.max = 1000 return s } // LoadLeases reads the leases from the store (at start). func (s *Server) LoadLeases(ctx context.Context) error { ls, err := s.store.LoadLeases(ctx) if err != nil { return err } s.mu.Lock() defer s.mu.Unlock() for _, l := range ls { s.leases[l.IP] = l } return nil } // SetConfig gives the server a new configuration. Listeners follow it. func (s *Server) SetConfig(st *model.State) { s.cfg.Store(compile(st)) select { case s.changed <- struct{}{}: default: } } // Leases returns a copy of every lease, by address. func (s *Server) Leases() []*model.Lease { s.mu.Lock() defer s.mu.Unlock() out := make([]*model.Lease, 0, len(s.leases)) for _, l := range s.leases { c := *l out = append(out, &c) } sort.Slice(out, func(i, j int) bool { return out[i].IP.Less(out[j].IP) }) return out } // Lease returns one lease. func (s *Server) Lease(ip netip.Addr) *model.Lease { s.mu.Lock() defer s.mu.Unlock() if l := s.leases[ip]; l != nil { c := *l return &c } return nil } // ErrNoLease is returned for an address without a lease. var ErrNoLease = errors.New("no lease for that address") // DeleteLease forgets a lease: the address is free at once (the client // may still be using it until it next renews, and then gets a NAK). func (s *Server) DeleteLease(ctx context.Context, ip netip.Addr) error { s.mu.Lock() defer s.mu.Unlock() if s.leases[ip] == nil { return ErrNoLease } if err := s.store.DeleteLease(ctx, ip); err != nil { return err } delete(s.leases, ip) delete(s.offers, ip) return nil } // ImportLeases adds leases (from another server), replacing what is known // for their addresses. func (s *Server) ImportLeases(ctx context.Context, ls []*model.Lease) error { s.mu.Lock() defer s.mu.Unlock() for _, l := range ls { if err := s.store.PutLease(ctx, l); err != nil { return err } c := *l s.leases[l.IP] = &c } return nil } // Prune deletes leases that ended long ago, and forgets old offers. func (s *Server) Prune(ctx context.Context) { now := s.now() s.mu.Lock() defer s.mu.Unlock() cut := now.Add(-keepEnded) n := 0 for ip, l := range s.leases { if l.End.Before(cut) { delete(s.leases, ip) n++ } } for ip, o := range s.offers { if now.After(o.expires) { delete(s.offers, ip) } } if n > 0 { if _, err := s.store.PruneLeases(ctx, cut); err != nil { s.log.Warn("pruning old leases failed", "err", err) } } } // putLease saves a lease, then keeps it (s.mu held). func (s *Server) putLease(ctx context.Context, l *model.Lease) error { l.Updated = s.now() if err := s.store.PutLease(ctx, l); err != nil { return err } c := *l s.leases[l.IP] = &c return nil } // warnOnce logs a warning at most once per interval for a key. func (s *Server) warnOnce(key string, every time.Duration, msg string, args ...any) { now := s.now() if v, ok := s.warned.Load(key); ok && now.Sub(v.(time.Time)) < every { return } s.warned.Store(key, now) s.log.Warn(msg, args...) } // ---- interfaces // HostInterfaces reads this machine's interfaces (cached briefly, since // every request asks). type HostInterfaces struct { mu sync.Mutex at time.Time cache map[string][]netip.Prefix err error } // Addrs implements Interfaces. func (h *HostInterfaces) Addrs(name string) ([]netip.Prefix, error) { all, err := h.All() if err != nil { return nil, err } a, ok := all[name] if !ok { return nil, errors.New("no interface " + name) } return a, nil } // All returns every interface with its IPv4 prefixes. func (h *HostInterfaces) All() (map[string][]netip.Prefix, error) { h.mu.Lock() defer h.mu.Unlock() if time.Since(h.at) < 5*time.Second && (h.cache != nil || h.err != nil) { return h.cache, h.err } h.at = time.Now() ifs, err := net.Interfaces() if err != nil { h.cache, h.err = nil, err return nil, err } m := map[string][]netip.Prefix{} for _, i := range ifs { var ps []netip.Prefix addrs, _ := i.Addrs() for _, a := range addrs { if n, ok := a.(*net.IPNet); ok { if ip4 := n.IP.To4(); ip4 != nil { bits, _ := n.Mask.Size() ps = append(ps, netip.PrefixFrom(netip.AddrFrom4([4]byte(ip4)), bits)) } } } m[i.Name] = ps } h.cache, h.err = m, nil return m, nil } // serverAddr is the interface's address to answer a subnet's clients // from: its address in the subnet, else its first IPv4 address. func (s *Server) serverAddr(iface string, sub *subnet) (netip.Addr, error) { addrs, err := s.ifaces.Addrs(iface) if err != nil { return netip.Addr{}, err } for _, a := range addrs { if sub != nil && sub.Prefix.Contains(a.Addr()) { return a.Addr(), nil } } if len(addrs) > 0 { return addrs[0].Addr(), nil } return netip.Addr{}, errors.New("interface " + iface + " has no IPv4 address") } // ownAddrs is the set of the interface's addresses. func (s *Server) ownAddrs(iface string) map[netip.Addr]bool { addrs, _ := s.ifaces.Addrs(iface) m := make(map[netip.Addr]bool, len(addrs)) for _, p := range addrs { m[p.Addr()] = true } return m } // isOwnAddr reports whether a is one of the interface's addresses. func (s *Server) isOwnAddr(iface string, a netip.Addr) bool { addrs, _ := s.ifaces.Addrs(iface) for _, p := range addrs { if p.Addr() == a { return true } } return false } // ---- events // Event is one DHCP message in or out, for the activity log. type Event struct { Time time.Time `json:"time"` Interface string `json:"interface"` Type string `json:"type"` // DHCPDISCOVER, DHCPOFFER, ... MAC string `json:"mac"` IP string `json:"ip,omitempty"` Hostname string `json:"hostname,omitempty"` Detail string `json:"detail,omitempty"` } type events struct { mu sync.Mutex max int list []Event counts map[string]uint64 } func (e *events) add(ev Event) { e.mu.Lock() defer e.mu.Unlock() e.list = append(e.list, ev) if len(e.list) > e.max { e.list = append([]Event(nil), e.list[len(e.list)-e.max:]...) } if e.counts == nil { e.counts = map[string]uint64{} } e.counts[ev.Type]++ } // Events returns the latest events, newest first, optionally only those // keep accepts. func (s *Server) Events(limit int, keep func(Event) bool) []Event { s.events.mu.Lock() defer s.events.mu.Unlock() out := []Event{} for i := len(s.events.list) - 1; i >= 0 && (limit <= 0 || len(out) < limit); i-- { ev := s.events.list[i] if keep != nil && !keep(ev) { continue } out = append(out, ev) } return out } // Counts returns how many messages of each type were seen and sent since // the start. func (s *Server) Counts() map[string]uint64 { s.events.mu.Lock() defer s.events.mu.Unlock() out := map[string]uint64{} for k, v := range s.events.counts { out[k] = v } return out }