dhcp4d: implement lease expiration

This commit is contained in:
Michael Stapelberg 2018-06-09 15:04:31 +02:00
parent ea476bbb04
commit f9c3c23b16
2 changed files with 234 additions and 63 deletions

View File

@ -14,6 +14,7 @@ import (
) )
type Lease struct { type Lease struct {
Num int
Addr net.IP Addr net.IP
HardwareAddr string HardwareAddr string
Hostname string Hostname string
@ -29,6 +30,8 @@ type Handler struct {
leasesHW map[string]*Lease leasesHW map[string]*Lease
leasesIP map[int]*Lease leasesIP map[int]*Lease
timeNow func() time.Time
// Leases is called whenever a new lease is handed out // Leases is called whenever a new lease is handed out
Leases func([]*Lease) Leases func([]*Lease)
} }
@ -57,24 +60,24 @@ func NewHandler(dir string) (*Handler, error) {
dhcp4.OptionDomainName: []byte("lan"), dhcp4.OptionDomainName: []byte("lan"),
dhcp4.OptionDomainSearch: []byte{0x03, 'l', 'a', 'n', 0x00}, dhcp4.OptionDomainSearch: []byte{0x03, 'l', 'a', 'n', 0x00},
}, },
timeNow: time.Now,
}, nil }, nil
} }
func (h *Handler) findLease() int { func (h *Handler) findLease() int {
now := h.timeNow()
if len(h.leasesIP) < h.leaseRange { if len(h.leasesIP) < h.leaseRange {
// Hand out a free lease
// TODO: hash the hwaddr like dnsmasq // TODO: hash the hwaddr like dnsmasq
i := rand.Intn(h.leaseRange) i := rand.Intn(h.leaseRange)
if _, ok := h.leasesIP[i]; !ok { if l, ok := h.leasesIP[i]; !ok || now.After(l.Expiry) {
return i return i
} }
for i := 0; i < h.leaseRange; 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 return i
} }
} }
} }
// TODO: expire the oldest lease
return -1 return -1
} }
@ -88,16 +91,24 @@ func (h *Handler) canLease(reqIP net.IP, hwaddr string) int {
return -1 return -1
} }
if l, exists := h.leasesIP[leaseNum]; exists && l.HardwareAddr != hwaddr { l, ok := h.leasesIP[leaseNum]
return -1 // lease already in use 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? // 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 { 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]) reqIP := net.IP(options[dhcp4.OptionRequestedIPAddress])
if reqIP == nil { if reqIP == nil {
reqIP = net.IP(p.CIAddr()) reqIP = net.IP(p.CIAddr())
@ -105,26 +116,28 @@ func (h *Handler) ServeDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options d
switch msgType { switch msgType {
case dhcp4.Discover: case dhcp4.Discover:
// Find previous lease for this HardwareAddr, if any
// hwAddr := p.CHAddr().String()
// if lease, ok := h.leases[hwAddr]; ok {
// }
free := -1 free := -1
hwAddr := p.CHAddr().String()
// try to offer the requested IP, if any and available
if !bytes.Equal(reqIP.To4(), net.IPv4zero) { if !bytes.Equal(reqIP.To4(), net.IPv4zero) {
free = h.canLease(reqIP, p.CHAddr().String()) free = h.canLease(reqIP, hwAddr)
log.Printf("canLease: %v", free)
} }
// offer previous lease for this HardwareAddr, if any
if lease, ok := h.leasesHW[hwAddr]; ok {
free = lease.Num
}
if free == -1 { if free == -1 {
free = h.findLease() free = h.findLease()
} }
if free == -1 { if free == -1 {
log.Printf("Cannot reply with DHCPOFFER: no more leases available") log.Printf("Cannot reply with DHCPOFFER: no more leases available")
return nil // no free leases return nil // no free leases
} }
log.Printf("start = %v, free = %v", h.start, free)
return dhcp4.ReplyPacket(p, return dhcp4.ReplyPacket(p,
dhcp4.Offer, dhcp4.Offer,
h.serverIP, h.serverIP,
@ -143,11 +156,18 @@ func (h *Handler) ServeDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options d
} }
lease := &Lease{ lease := &Lease{
Num: leaseNum,
Addr: reqIP, Addr: reqIP,
HardwareAddr: p.CHAddr().String(), HardwareAddr: p.CHAddr().String(),
Expiry: time.Now().Add(h.leasePeriod), Expiry: time.Now().Add(h.leasePeriod),
Hostname: string(options[dhcp4.OptionHostName]), 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.leasesIP[leaseNum] = lease
h.leasesHW[lease.HardwareAddr] = lease h.leasesHW[lease.HardwareAddr] = lease
if h.Leases != nil { 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])) 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 return nil
} }

View File

@ -7,10 +7,30 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
"time"
"github.com/krolaw/dhcp4" "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 = ` const goldenInterfaces = `
{ {
"interfaces":[ "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") tmpdir, err := ioutil.TempDir("", "dhcp4dtest")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer os.RemoveAll(tmpdir)
if err := ioutil.WriteFile(filepath.Join(tmpdir, "interfaces.json"), []byte(goldenInterfaces), 0644); err != nil { if err := ioutil.WriteFile(filepath.Join(tmpdir, "interfaces.json"), []byte(goldenInterfaces), 0644); err != nil {
t.Fatal(err) 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 ( var (
addr = net.IP{192, 168, 42, 23} addr = net.IP{192, 168, 42, 23}
hardwareAddr = net.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66} hardwareAddr = net.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66}
hostname = "xps" hostname = "xps"
) )
handler, err := NewHandler(tmpdir)
if err != nil {
t.Fatal(err)
}
leasedCalled := false leasedCalled := false
handler.Leases = func(leases []*Lease) { handler.Leases = func(leases []*Lease) {
if got, want := len(leases), 1; got != want { if got, want := len(leases), 1; got != want {
@ -82,38 +107,17 @@ func TestLease(t *testing.T) {
} }
func TestPreferredAddress(t *testing.T) { func TestPreferredAddress(t *testing.T) {
tmpdir, err := ioutil.TempDir("", "dhcp4dtest") handler, cleanup := testHandler(t)
if err != nil { defer cleanup()
t.Fatal(err)
}
defer os.RemoveAll(tmpdir)
if err := ioutil.WriteFile(filepath.Join(tmpdir, "interfaces.json"), []byte(goldenInterfaces), 0644); err != nil {
t.Fatal(err)
}
var ( var (
addr = net.IP{192, 168, 42, 23} addr = net.IP{192, 168, 42, 23}
hardwareAddr = net.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66} hardwareAddr = net.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66}
hostname = "xps" hostname = "xps"
) )
handler, err := NewHandler(tmpdir)
if err != nil {
t.Fatal(err)
}
t.Run("no requested IP", func(t *testing.T) { t.Run("no requested IP", func(t *testing.T) {
p := dhcp4.RequestPacket( p := request(net.IPv4zero, hardwareAddr)
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),
},
},
)
resp := handler.ServeDHCP(p, dhcp4.Discover, p.ParseOptions()) resp := handler.ServeDHCP(p, dhcp4.Discover, p.ParseOptions())
if got, want := resp.YIAddr().To4(), addr.To4(); bytes.Equal(got, want) { if got, want := resp.YIAddr().To4(), addr.To4(); bytes.Equal(got, want) {
t.Errorf("DHCPOFFER for wrong IP: got %v, want %v", 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) { t.Run("requested CIAddr", func(t *testing.T) {
p := dhcp4.RequestPacket( p := request(addr, hardwareAddr)
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),
},
},
)
resp := handler.ServeDHCP(p, dhcp4.Discover, p.ParseOptions()) resp := handler.ServeDHCP(p, dhcp4.Discover, p.ParseOptions())
if got, want := resp.YIAddr().To4(), addr.To4(); !bytes.Equal(got, want) { if got, want := resp.YIAddr().To4(), addr.To4(); !bytes.Equal(got, want) {
t.Errorf("DHCPOFFER for wrong IP: got %v, want %v", 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) { t.Run("requested option", func(t *testing.T) {
//p := request(net.IPv4zero, hardwareAddr)
p := dhcp4.RequestPacket( p := dhcp4.RequestPacket(
dhcp4.Discover, dhcp4.Discover,
hardwareAddr, // MAC address 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