Reservation lease time; reservations by MAC address or client ID

- 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>
This commit is contained in:
abrown38andClaude Opus 5.5 committed 2026-10-18 20:26:24 +01:00
1 parent abb2ee0fd7
commit 4fd72998ec
20 files changed
+252 -65

No files matched your search

+21 -9
View File
@@ -190,8 +190,9 @@ func (s *Server) usable(cfg *config, own map[netip.Addr]bool, sub *subnet, ip ne
if !sub.Prefix.Contains(ip) || ip == sub.network || ip == sub.broadcast || own[ip] {
return false
}
if r := cfg.resByIP[ip]; r != nil {
if !cl.matches(r) {
res := cfg.resByIP[ip]
if res != nil {
if !cl.matches(res) {
return false
}
} else {
@@ -211,7 +212,10 @@ func (s *Server) usable(cfg *config, own map[netip.Addr]bool, sub *subnet, ip ne
return false
}
}
if l := s.leases[ip]; l != nil && l.HeldAt(now) && (l.State == model.LeaseDeclined || !l.Owns(cl.mac, cl.id)) {
// A lease of the reserved address by the reservation's client under
// its other identity (the other OS of a dual-boot machine) is its own.
if l := s.leases[ip]; l != nil && l.HeldAt(now) && (l.State == model.LeaseDeclined ||
!l.Owns(cl.mac, cl.id) && !(res != nil && (client{mac: l.MAC, id: l.ClientID}).matches(res))) {
return false
}
if o, ok := s.offers[ip]; ok && now.Before(o.expires) && o.cl != cl {
@@ -296,8 +300,12 @@ func (s *Server) ack(ctx context.Context, cfg *config, iface string, p *dhcp4.Pa
host = l.Hostname
}
}
res := cfg.reservationFor(sub.ID, cl)
if res != nil && res.IP != ip {
res = nil
}
l := &model.Lease{IP: ip, SubnetID: sub.ID, MAC: cl.mac, ClientID: cl.id, Hostname: host, State: model.LeaseActive,
Start: start, End: now.Add(sub.leaseTime), Interface: iface, LastSeen: seen}
Start: start, End: now.Add(leaseTime(sub, res)), Interface: iface, LastSeen: seen}
err := s.putLease(ctx, l)
delete(s.offers, ip)
s.mu.Unlock()
@@ -312,10 +320,6 @@ func (s *Server) ack(ctx context.Context, cfg *config, iface string, p *dhcp4.Pa
s.warnOnce("sid "+iface, 10*time.Minute, "can't answer DHCP: "+err.Error(), "interface", iface)
return nil
}
res := cfg.reservationFor(sub.ID, cl)
if res != nil && res.IP != ip {
res = nil
}
r := p.Reply(dhcp4.Ack)
r.YIAddr = ip
r.CIAddr = p.CIAddr
@@ -413,6 +417,14 @@ func (s *Server) inform(cfg *config, iface string, p *dhcp4.Packet, cl client) *
return &Outbound{Packet: r, Dst: p.CIAddr}
}
// leaseTime is a lease's length: the reservation's, else the subnet's.
func leaseTime(sub *subnet, res *model.Reservation) time.Duration {
if res != nil && res.LeaseTime > 0 {
return time.Duration(res.LeaseTime) * time.Second
}
return sub.leaseTime
}
// ---- options
// Options always sent when configured, even when the client doesn't ask.
@@ -425,7 +437,7 @@ var essential = []byte{dhcp4.OptSubnetMask, dhcp4.OptRouters, dhcp4.OptDNSServer
func (s *Server) addOptions(r, req *dhcp4.Packet, sub *subnet, res *model.Reservation, sid netip.Addr, lease bool) {
r.Set(dhcp4.OptServerID, dhcp4.IP(sid))
if lease {
lt := uint32(sub.leaseTime / time.Second)
lt := uint32(leaseTime(sub, res) / time.Second)
r.Set(dhcp4.OptLeaseTime, dhcp4.U32(lt))
r.Set(dhcp4.OptRenewalTime, dhcp4.U32(lt/2))
r.Set(dhcp4.OptRebindingTime, dhcp4.U32(lt*7/8))