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:
1 parent
abb2ee0fd7
commit
4fd72998ec
20 files changed
+252
-65
No files matched your search
@@ -125,13 +125,11 @@ type client struct {
|
||||
id string // client identifier (option 61) in hex, or ""
|
||||
}
|
||||
|
||||
// matches reports whether a reservation is for the client: by client
|
||||
// identifier when the reservation has one, else by MAC address.
|
||||
// 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 {
|
||||
if r.ClientID != "" {
|
||||
return r.ClientID == cl.id
|
||||
}
|
||||
return r.MAC == cl.mac
|
||||
return r.ClientID != "" && r.ClientID == cl.id || r.MAC != "" && r.MAC == cl.mac
|
||||
}
|
||||
|
||||
func (cl client) String() string {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user