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

+34 -2
View File
@@ -2,6 +2,7 @@ package dhcpd
import (
"context"
"encoding/binary"
"errors"
"net"
"net/netip"
@@ -115,9 +116,9 @@ func (h *harness) discover(iface string, m net.HardwareAddr, opts ...dhcp4.Optio
}
// lease runs DISCOVER and REQUEST and returns the ACK.
func (h *harness) lease(iface string, m net.HardwareAddr) *dhcp4.Packet {
func (h *harness) lease(iface string, m net.HardwareAddr, opts ...dhcp4.Option) *dhcp4.Packet {
h.t.Helper()
off := h.discover(iface, m)
off := h.discover(iface, m, opts...)
if off == nil || off.Packet.Type() != dhcp4.Offer {
h.t.Fatalf("no offer for %s on %s", m, iface)
}
@@ -126,6 +127,9 @@ func (h *harness) lease(iface string, m net.HardwareAddr) *dhcp4.Packet {
req.Set(dhcp4.OptServerID, dhcp4.IP(sid))
req.Set(dhcp4.OptRequestedIP, dhcp4.IP(off.Packet.YIAddr))
req.Set(dhcp4.OptHostName, []byte("client"+m.String()[15:]))
for _, o := range opts {
req.Set(o.Code, o.Data)
}
ack := h.send(iface, req)
if ack == nil || ack.Packet.Type() != dhcp4.Ack {
h.t.Fatalf("no ACK for %s: %+v", m, ack)
@@ -580,3 +584,31 @@ func TestAddressOrder(t *testing.T) {
t.Errorf("a full subnet offered %s", out.Packet.YIAddr)
}
}
func TestReservationLeaseTimeAndClientID(t *testing.T) {
h := newHarness(t, func(st *model.State) {
st.Reservations[0].LeaseTime = 600
st.Reservations[0].ClientID = "ff0102"
})
u32 := func(p *dhcp4.Packet) uint32 { return binary.BigEndian.Uint32(p.Get(dhcp4.OptLeaseTime)) }
id := func(hex ...byte) dhcp4.Option { return dhcp4.Option{Code: dhcp4.OptClientID, Data: hex} }
// By its client ID, from any MAC address, with its own lease time.
ack := h.lease("eth0", mac(0x50), id(0xff, 1, 2))
if ack.YIAddr.String() != "10.0.10.20" || u32(ack) != 600 {
t.Errorf("by client id: %s %d", ack.YIAddr, u32(ack))
}
if l := h.s.Lease(ack.YIAddr); !l.End.Equal(h.clock.Add(10 * time.Minute)) {
t.Errorf("lease end %v", l.End)
}
// By its MAC address with another client ID (the machine's other OS):
// the same address, though the first identity's lease is still on.
ack = h.lease("eth0", mac(0x99), id(1, 0xaa, 0, 0, 0, 0, 0x99))
if ack.YIAddr.String() != "10.0.10.20" {
t.Errorf("by MAC: %s", ack.YIAddr)
}
// Anyone else gets a pool address, with the subnet's lease time.
ack = h.lease("eth0", mac(0x51), id(0xff, 9, 9))
if ack.YIAddr.String() == "10.0.10.20" || u32(ack) != 43200 {
t.Errorf("stranger: %s %d", ack.YIAddr, u32(ack))
}
}