From f9c3c23b167561e839928935b007cbce4409bb40 Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Sat, 9 Jun 2018 15:04:31 +0200 Subject: [PATCH] dhcp4d: implement lease expiration --- internal/dhcp4d/dhcp4d.go | 56 +++++--- internal/dhcp4d/dhcp4d_test.go | 241 +++++++++++++++++++++++++++------ 2 files changed, 234 insertions(+), 63 deletions(-) diff --git a/internal/dhcp4d/dhcp4d.go b/internal/dhcp4d/dhcp4d.go index 504478c..fa5855d 100644 --- a/internal/dhcp4d/dhcp4d.go +++ b/internal/dhcp4d/dhcp4d.go @@ -14,6 +14,7 @@ import ( ) type Lease struct { + Num int Addr net.IP HardwareAddr string Hostname string @@ -29,6 +30,8 @@ type Handler struct { leasesHW map[string]*Lease leasesIP map[int]*Lease + timeNow func() time.Time + // Leases is called whenever a new lease is handed out Leases func([]*Lease) } @@ -57,24 +60,24 @@ func NewHandler(dir string) (*Handler, error) { dhcp4.OptionDomainName: []byte("lan"), dhcp4.OptionDomainSearch: []byte{0x03, 'l', 'a', 'n', 0x00}, }, + timeNow: time.Now, }, nil } func (h *Handler) findLease() int { + now := h.timeNow() if len(h.leasesIP) < h.leaseRange { - // Hand out a free lease // TODO: hash the hwaddr like dnsmasq i := rand.Intn(h.leaseRange) - if _, ok := h.leasesIP[i]; !ok { + if l, ok := h.leasesIP[i]; !ok || now.After(l.Expiry) { return i } for i := 0; i < h.leaseRange; i++ { - if _, ok := h.leasesIP[i]; !ok { + if l, ok := h.leasesIP[i]; !ok || now.After(l.Expiry) { return i } } } - // TODO: expire the oldest lease return -1 } @@ -88,16 +91,24 @@ func (h *Handler) canLease(reqIP net.IP, hwaddr string) int { return -1 } - if l, exists := h.leasesIP[leaseNum]; exists && l.HardwareAddr != hwaddr { - return -1 // lease already in use + l, ok := h.leasesIP[leaseNum] + if !ok { + return leaseNum // lease available } - return leaseNum + if l.HardwareAddr == hwaddr { + return leaseNum // lease already owned by requestor + } + + if h.timeNow().After(l.Expiry) { + return leaseNum // lease expired + } + + return -1 // lease unavailable } // TODO: is ServeDHCP always run from the same goroutine, or do we need locking? func (h *Handler) ServeDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options dhcp4.Options) dhcp4.Packet { - log.Printf("got DHCP packet: %+v, msgType: %v, options: %v", p, msgType, options) reqIP := net.IP(options[dhcp4.OptionRequestedIPAddress]) if reqIP == nil { reqIP = net.IP(p.CIAddr()) @@ -105,26 +116,28 @@ func (h *Handler) ServeDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options d switch msgType { case dhcp4.Discover: - // Find previous lease for this HardwareAddr, if any - // hwAddr := p.CHAddr().String() - // if lease, ok := h.leases[hwAddr]; ok { - - // } - free := -1 + hwAddr := p.CHAddr().String() + + // try to offer the requested IP, if any and available if !bytes.Equal(reqIP.To4(), net.IPv4zero) { - free = h.canLease(reqIP, p.CHAddr().String()) - log.Printf("canLease: %v", free) + free = h.canLease(reqIP, hwAddr) } + + // offer previous lease for this HardwareAddr, if any + if lease, ok := h.leasesHW[hwAddr]; ok { + free = lease.Num + } + if free == -1 { free = h.findLease() } + if free == -1 { log.Printf("Cannot reply with DHCPOFFER: no more leases available") return nil // no free leases } - log.Printf("start = %v, free = %v", h.start, free) return dhcp4.ReplyPacket(p, dhcp4.Offer, h.serverIP, @@ -143,11 +156,18 @@ func (h *Handler) ServeDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options d } lease := &Lease{ + Num: leaseNum, Addr: reqIP, HardwareAddr: p.CHAddr().String(), Expiry: time.Now().Add(h.leasePeriod), Hostname: string(options[dhcp4.OptionHostName]), } + + // Release any old leases for this client + if l, ok := h.leasesHW[lease.HardwareAddr]; ok { + delete(h.leasesIP, l.Num) + } + h.leasesIP[leaseNum] = lease h.leasesHW[lease.HardwareAddr] = lease if h.Leases != nil { @@ -161,7 +181,5 @@ func (h *Handler) ServeDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options d h.options.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList])) } - // 1970/01/01 01:00:04 got DHCP packet: [1 1 6 0 142 216 238 39 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 164 76 200 233 19 71 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 99 130 83 99 53 1 3 54 4 192 168 42 1 50 4 192 168 42 33 12 3 120 112 115 55 18 1 28 2 3 15 6 119 12 44 47 26 121 42 121 249 33 252 42 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0], msgType: Request, options: map[OptionDHCPMessageType:[3] OptionServerIdentifier:[192 168 42 1] OptionHostName:[120 112 115] OptionParameterRequestList:[1 28 2 3 15 6 119 12 44 47 26 121 42 121 249 33 252 42] OptionRequestedIPAddress:[192 168 42 33]] - return nil } diff --git a/internal/dhcp4d/dhcp4d_test.go b/internal/dhcp4d/dhcp4d_test.go index e951922..c619190 100644 --- a/internal/dhcp4d/dhcp4d_test.go +++ b/internal/dhcp4d/dhcp4d_test.go @@ -7,10 +7,30 @@ import ( "os" "path/filepath" "testing" + "time" "github.com/krolaw/dhcp4" ) +func packet(mt dhcp4.MessageType, addr net.IP, hwaddr net.HardwareAddr, opts []dhcp4.Option) dhcp4.Packet { + return dhcp4.RequestPacket( + mt, + hwaddr, // MAC address + addr, // requested IP address + []byte{0xaa, 0xbb, 0xcc, 0xdd}, // transaction ID + false, // broadcast, + opts, + ) +} + +func request(addr net.IP, hwaddr net.HardwareAddr, opts ...dhcp4.Option) dhcp4.Packet { + return packet(dhcp4.Request, addr, hwaddr, opts) +} + +func discover(addr net.IP, hwaddr net.HardwareAddr, opts ...dhcp4.Option) dhcp4.Packet { + return packet(dhcp4.Discover, addr, hwaddr, opts) +} + const goldenInterfaces = ` { "interfaces":[ @@ -27,24 +47,29 @@ const goldenInterfaces = ` } ` -func TestLease(t *testing.T) { +func testHandler(t *testing.T) (_ *Handler, cleanup func()) { tmpdir, err := ioutil.TempDir("", "dhcp4dtest") if err != nil { t.Fatal(err) } - defer os.RemoveAll(tmpdir) if err := ioutil.WriteFile(filepath.Join(tmpdir, "interfaces.json"), []byte(goldenInterfaces), 0644); err != nil { t.Fatal(err) } + handler, err := NewHandler(tmpdir) + if err != nil { + t.Fatal(err) + } + return handler, func() { os.RemoveAll(tmpdir) } +} + +func TestLease(t *testing.T) { + handler, cleanup := testHandler(t) + defer cleanup() var ( addr = net.IP{192, 168, 42, 23} hardwareAddr = net.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66} hostname = "xps" ) - handler, err := NewHandler(tmpdir) - if err != nil { - t.Fatal(err) - } leasedCalled := false handler.Leases = func(leases []*Lease) { if got, want := len(leases), 1; got != want { @@ -82,38 +107,17 @@ func TestLease(t *testing.T) { } func TestPreferredAddress(t *testing.T) { - tmpdir, err := ioutil.TempDir("", "dhcp4dtest") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tmpdir) - if err := ioutil.WriteFile(filepath.Join(tmpdir, "interfaces.json"), []byte(goldenInterfaces), 0644); err != nil { - t.Fatal(err) - } + handler, cleanup := testHandler(t) + defer cleanup() + var ( addr = net.IP{192, 168, 42, 23} hardwareAddr = net.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66} hostname = "xps" ) - handler, err := NewHandler(tmpdir) - if err != nil { - t.Fatal(err) - } t.Run("no requested IP", func(t *testing.T) { - p := dhcp4.RequestPacket( - dhcp4.Discover, - hardwareAddr, // MAC address - net.IPv4zero, // requested IP address - []byte{0xaa, 0xbb, 0xcc, 0xdd}, // transaction ID - false, // broadcast, - []dhcp4.Option{ - { - Code: dhcp4.OptionHostName, - Value: []byte(hostname), - }, - }, - ) + p := request(net.IPv4zero, hardwareAddr) resp := handler.ServeDHCP(p, dhcp4.Discover, p.ParseOptions()) if got, want := resp.YIAddr().To4(), addr.To4(); bytes.Equal(got, want) { t.Errorf("DHCPOFFER for wrong IP: got %v, want %v", got, want) @@ -121,19 +125,7 @@ func TestPreferredAddress(t *testing.T) { }) t.Run("requested CIAddr", func(t *testing.T) { - p := dhcp4.RequestPacket( - dhcp4.Discover, - hardwareAddr, // MAC address - addr, // requested IP address - []byte{0xaa, 0xbb, 0xcc, 0xdd}, // transaction ID - false, // broadcast, - []dhcp4.Option{ - { - Code: dhcp4.OptionHostName, - Value: []byte(hostname), - }, - }, - ) + p := request(addr, hardwareAddr) resp := handler.ServeDHCP(p, dhcp4.Discover, p.ParseOptions()) if got, want := resp.YIAddr().To4(), addr.To4(); !bytes.Equal(got, want) { t.Errorf("DHCPOFFER for wrong IP: got %v, want %v", got, want) @@ -141,6 +133,7 @@ func TestPreferredAddress(t *testing.T) { }) t.Run("requested option", func(t *testing.T) { + //p := request(net.IPv4zero, hardwareAddr) p := dhcp4.RequestPacket( dhcp4.Discover, hardwareAddr, // MAC address @@ -164,3 +157,163 @@ func TestPreferredAddress(t *testing.T) { } }) } + +func TestPoolBoundaries(t *testing.T) { + handler, cleanup := testHandler(t) + defer cleanup() + + var ( + addr = net.IP{192, 168, 42, 23} + hardwareAddr = net.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66} + ) + + for _, last := range []byte{1, 202} { + addr[len(addr)-1] = last + p := request(addr, hardwareAddr) + resp := handler.ServeDHCP(p, dhcp4.Request, p.ParseOptions()) + opts := resp.ParseOptions() + if got, want := dhcp4.MessageType(opts[dhcp4.OptionDHCPMessageType][0]), dhcp4.NAK; got != want { + t.Errorf("DHCPREQUEST resulted in unexpected message type: got %v, want %v", got, want) + } + } + +} + +func TestPreviousLease(t *testing.T) { + handler, cleanup := testHandler(t) + defer cleanup() + + var ( + addr1 = net.IP{192, 168, 42, 23} + addr2 = net.IP{192, 168, 42, 42} + hardwareAddr1 = net.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66} + hardwareAddr2 = net.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x77} + ) + + p := request(addr1, hardwareAddr1) + resp := handler.ServeDHCP(p, dhcp4.Request, p.ParseOptions()) + if got, want := resp.YIAddr().To4(), addr1.To4(); !bytes.Equal(got, want) { + t.Errorf("DHCPREQUEST resulted in wrong IP: got %v, want %v", got, want) + } + + p = request(addr1, hardwareAddr2) + resp = handler.ServeDHCP(p, dhcp4.Request, p.ParseOptions()) + opts := resp.ParseOptions() + if got, want := dhcp4.MessageType(opts[dhcp4.OptionDHCPMessageType][0]), dhcp4.NAK; got != want { + t.Errorf("DHCPREQUEST resulted in unexpected message type: got %v, want %v", got, want) + } + + p = discover(net.IPv4zero, hardwareAddr1) + resp = handler.ServeDHCP(p, dhcp4.Discover, p.ParseOptions()) + if got, want := resp.YIAddr().To4(), addr1.To4(); !bytes.Equal(got, want) { + t.Errorf("DHCPOFFER for wrong IP: got %v, want %v", got, want) + } + + // Free addr1 by requesting addr2 + p = request(addr2, hardwareAddr1) + resp = handler.ServeDHCP(p, dhcp4.Request, p.ParseOptions()) + if got, want := resp.YIAddr().To4(), addr2.To4(); !bytes.Equal(got, want) { + t.Errorf("DHCPREQUEST resulted in wrong IP: got %v, want %v", got, want) + } + + // Verify addr1 is now available to other clients + p = request(addr1, hardwareAddr2) + resp = handler.ServeDHCP(p, dhcp4.Request, p.ParseOptions()) + if got, want := resp.YIAddr().To4(), addr1.To4(); !bytes.Equal(got, want) { + t.Errorf("DHCPREQUEST resulted in wrong IP: got %v, want %v", got, want) + } +} + +func TestExpiration(t *testing.T) { + handler, cleanup := testHandler(t) + defer cleanup() + now := time.Now() + handler.timeNow = func() time.Time { return now } + + var ( + addr = net.IP{192, 168, 42, 23} + hardwareAddr = net.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66} + ) + + t.Run("allocate entire pool", func(t *testing.T) { + // 1 is the DHCP server, + for i := 1; i < 1+200; i++ { + addr[len(addr)-1] = byte(1 + (i % 254)) // avoid .0 (net) and .255 (broadcast) + hardwareAddr[len(hardwareAddr)-1] = addr[len(addr)-1] + p := request(addr, hardwareAddr) + resp := handler.ServeDHCP(p, dhcp4.Request, p.ParseOptions()) + if got, want := resp.YIAddr().To4(), addr.To4(); !bytes.Equal(got, want) { + t.Errorf("DHCPREQUEST resulted in wrong IP: got %v, want %v", got, want) + } + } + }) + + t.Run("re-allocate", func(t *testing.T) { + // 1 is the DHCP server, + for i := 1; i < 1+200; i++ { + addr[len(addr)-1] = byte(1 + (i % 254)) // avoid .0 (net) and .255 (broadcast) + hardwareAddr[len(hardwareAddr)-1] = addr[len(addr)-1] + p := request(addr, hardwareAddr) + resp := handler.ServeDHCP(p, dhcp4.Request, p.ParseOptions()) + if got, want := resp.YIAddr().To4(), addr.To4(); !bytes.Equal(got, want) { + t.Errorf("DHCPREQUEST resulted in wrong IP: got %v, want %v", got, want) + } + } + }) + + t.Run("full", func(t *testing.T) { + // 1 is the DHCP server, + for i := 1; i < 1+200; i++ { + addr[len(addr)-1] = byte(1 + (i % 254)) // avoid .0 (net) and .255 (broadcast) + hardwareAddr[len(hardwareAddr)-1] = addr[len(addr)-1] - 1 + p := request(addr, hardwareAddr) + resp := handler.ServeDHCP(p, dhcp4.Request, p.ParseOptions()) + opts := resp.ParseOptions() + if got, want := dhcp4.MessageType(opts[dhcp4.OptionDHCPMessageType][0]), dhcp4.NAK; got != want { + t.Errorf("DHCPREQUEST resulted in unexpected message type: got %v, want %v", got, want) + } + } + + hardwareAddr[len(hardwareAddr)-1] = 0 + p := discover(addr, hardwareAddr) + resp := handler.ServeDHCP(p, dhcp4.Discover, p.ParseOptions()) + if resp != nil { + t.Errorf("DHCPDISCOVER(%v) resulted in unexpected offer of %v", addr, resp.YIAddr()) + } + }) + + now = now.Add(3 * time.Hour) + + t.Run("re-allocate after expiration", func(t *testing.T) { + // 1 is the DHCP server, + for i := 1; i < 1+200; i++ { + addr[len(addr)-1] = byte(1 + (i % 254)) // avoid .0 (net) and .255 (broadcast) + p := request(addr, hardwareAddr) + resp := handler.ServeDHCP(p, dhcp4.Request, p.ParseOptions()) + if got, want := resp.YIAddr().To4(), addr.To4(); !bytes.Equal(got, want) { + t.Errorf("DHCPREQUEST resulted in wrong IP: got %v, want %v", got, want) + } + } + }) +} + +func TestServerID(t *testing.T) { + handler, cleanup := testHandler(t) + defer cleanup() + + var ( + addr = net.IP{192, 168, 42, 23} + hardwareAddr = net.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66} + ) + + p := request(addr, hardwareAddr, dhcp4.Option{ + Code: dhcp4.OptionServerIdentifier, + Value: net.IP{192, 168, 1, 1}, + }) + resp := handler.ServeDHCP(p, dhcp4.Request, p.ParseOptions()) + if resp != nil { + t.Errorf("DHCPDISCOVER(%v) resulted in unexpected offer of %v", addr, resp.YIAddr()) + } +} + +// TODO: test persistent storage