router7/internal/dhcp4d/dhcp4d_test.go

560 lines
17 KiB
Go

// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package dhcp4d
import (
"encoding/binary"
"io/ioutil"
"net"
"os"
"path/filepath"
"testing"
"time"
"github.com/krolaw/dhcp4"
)
func messageType(p dhcp4.Packet) dhcp4.MessageType {
opts := p.ParseOptions()
return dhcp4.MessageType(opts[dhcp4.OptionDHCPMessageType][0])
}
func newPacket(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 newPacket(dhcp4.Request, addr, hwaddr, opts)
}
func discover(addr net.IP, hwaddr net.HardwareAddr, opts ...dhcp4.Option) dhcp4.Packet {
return newPacket(dhcp4.Discover, addr, hwaddr, opts)
}
func decline(addr net.IP, hwaddr net.HardwareAddr, opts ...dhcp4.Option) dhcp4.Packet {
return newPacket(dhcp4.Decline, addr, hwaddr, opts)
}
const goldenInterfaces = `
{
"interfaces":[
{
"hardware_addr": "02:73:53:00:ca:fe",
"name": "uplink0"
},
{
"hardware_addr": "02:73:53:00:b0:0c",
"name": "lan0",
"addr": "192.168.42.1/24"
}
]
}
`
type noopSink struct{}
func (*noopSink) LocalAddr() net.Addr { return nil }
func (*noopSink) Close() error { return nil }
func (*noopSink) WriteTo(b []byte, addr net.Addr) (n int, err error) { return len(b), nil }
func (*noopSink) SetDeadline(t time.Time) error { return nil }
func (*noopSink) SetReadDeadline(t time.Time) error { return nil }
func (*noopSink) SetWriteDeadline(t time.Time) error { return nil }
func (*noopSink) ReadFrom(buf []byte) (int, net.Addr, error) { return 0, nil, nil }
func testHandler(t *testing.T) (_ *Handler, cleanup func()) {
tmpdir, err := ioutil.TempDir("", "dhcp4dtest")
if err != nil {
t.Fatal(err)
}
if err := ioutil.WriteFile(filepath.Join(tmpdir, "interfaces.json"), []byte(goldenInterfaces), 0644); err != nil {
t.Fatal(err)
}
handler, err := NewHandler(
tmpdir,
&net.Interface{
HardwareAddr: net.HardwareAddr([]byte{0x11, 0x22, 0x33, 0x44, 0x55, 0x66}),
},
"lan0",
&noopSink{},
nil,
)
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"
)
leasedCalled := false
handler.Leases = func(leases []*Lease, latest *Lease) {
if got, want := len(leases), 1; got != want {
t.Fatalf("unexpected number of leases: got %d, want %d", got, want)
}
l := leases[0]
if got, want := l.Addr, addr; !got.Equal(want) {
t.Fatalf("unexpected lease.Addr: got %v, want %v", got, want)
}
if got, want := l.HardwareAddr, hardwareAddr.String(); got != want {
t.Fatalf("unexpected lease.HardwareAddr: got %q, want %q", got, want)
}
if got, want := l.Hostname, hostname; got != want {
t.Fatalf("unexpected lease.Hostname: got %q, want %q", got, want)
}
leasedCalled = true
}
p := dhcp4.RequestPacket(
dhcp4.Request,
hardwareAddr, // MAC address
addr, // requested IP address
[]byte{0xaa, 0xbb, 0xcc, 0xdd}, // transaction ID
false, // broadcast,
[]dhcp4.Option{
{
Code: dhcp4.OptionHostName,
Value: []byte(hostname),
},
},
)
handler.serveDHCP(p, dhcp4.Request, p.ParseOptions())
if !leasedCalled {
t.Fatalf("leased callback not called")
}
}
func TestPreferredAddress(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"
)
t.Run("no requested IP", func(t *testing.T) {
p := request(net.IPv4zero, hardwareAddr)
resp := handler.serveDHCP(p, dhcp4.Discover, p.ParseOptions())
if got, want := resp.YIAddr().To4(), addr.To4(); got.Equal(want) {
t.Errorf("DHCPOFFER for wrong IP: got %v, want anything else", got)
}
})
t.Run("requested CIAddr", func(t *testing.T) {
p := request(addr, hardwareAddr)
resp := handler.serveDHCP(p, dhcp4.Discover, p.ParseOptions())
if got, want := resp.YIAddr().To4(), addr.To4(); !got.Equal(want) {
t.Errorf("DHCPOFFER for wrong IP: got %v, want %v", got, want)
}
})
t.Run("requested option", func(t *testing.T) {
// p := request(net.IPv4zero, hardwareAddr)
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),
},
{
Code: dhcp4.OptionRequestedIPAddress,
Value: addr,
},
},
)
resp := handler.serveDHCP(p, dhcp4.Discover, p.ParseOptions())
if got, want := resp.YIAddr().To4(), addr.To4(); !got.Equal(want) {
t.Errorf("DHCPOFFER for wrong IP: got %v, want %v", got, want)
}
})
}
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, 242} {
addr[len(addr)-1] = last
p := request(addr, hardwareAddr)
resp := handler.serveDHCP(p, dhcp4.Request, p.ParseOptions())
if got, want := messageType(resp), 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(); !got.Equal(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())
if got, want := messageType(resp), 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(); !got.Equal(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(); !got.Equal(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(); !got.Equal(want) {
t.Errorf("DHCPREQUEST resulted in wrong IP: got %v, want %v", got, want)
}
}
func TestPermanentLease(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}
)
handler.SetLeases([]*Lease{
{
Num: 2,
Addr: addr,
HardwareAddr: hardwareAddr.String(),
},
})
p := request(addr, hardwareAddr)
resp := handler.serveDHCP(p, dhcp4.Request, p.ParseOptions())
if got, want := resp.YIAddr().To4(), addr.To4(); !got.Equal(want) {
t.Errorf("DHCPREQUEST resulted in wrong IP: got %v, want %v", got, want)
}
now = now.Add(3 * time.Hour)
hardwareAddr[len(hardwareAddr)-1] = 0x77
p = request(addr, hardwareAddr)
resp = handler.serveDHCP(p, dhcp4.Request, p.ParseOptions())
if got, want := messageType(resp), dhcp4.NAK; got != want {
t.Errorf("DHCPREQUEST resulted in unexpected message type: 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+230; 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(); !got.Equal(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+230; 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(); !got.Equal(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+230; 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())
if got, want := messageType(resp), 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+230; 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(); !got.Equal(want) {
t.Errorf("DHCPREQUEST resulted in wrong IP: got %v, want %v", got, want)
}
}
})
}
func TestRequestExpired(t *testing.T) {
handler, cleanup := testHandler(t)
defer cleanup()
now := time.Now()
handler.timeNow = func() time.Time { return now }
addr := net.IP{192, 168, 42, 23}
hardwareAddr := map[string]net.HardwareAddr{
"xps": net.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66},
"mbp": net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff},
}
t.Run("mbp grabs an address", func(t *testing.T) {
p := request(addr, hardwareAddr["mbp"])
resp := handler.serveDHCP(p, dhcp4.Request, p.ParseOptions())
if got, want := messageType(resp), dhcp4.ACK; got != want {
t.Errorf("DHCPREQUEST resulted in unexpected message type: got %v, want %v", got, want)
}
})
now = now.Add(3 * time.Hour)
t.Run("xps grabs the same address", func(t *testing.T) {
p := request(addr, hardwareAddr["xps"])
resp := handler.serveDHCP(p, dhcp4.Request, p.ParseOptions())
if got, want := messageType(resp), dhcp4.ACK; got != want {
t.Errorf("DHCPREQUEST resulted in unexpected message type: got %v, want %v", got, want)
}
})
t.Run("mbp requests its old address", func(t *testing.T) {
p := request(addr, hardwareAddr["mbp"])
resp := handler.serveDHCP(p, dhcp4.Request, p.ParseOptions())
if got, want := messageType(resp), dhcp4.NAK; got != want {
t.Errorf("DHCPREQUEST resulted in unexpected message type: got %v, want %v", got, want)
}
})
t.Run("mbp requests any", func(t *testing.T) {
p := request(addr, hardwareAddr["mbp"])
resp := handler.serveDHCP(p, dhcp4.Discover, p.ParseOptions())
if got, want := resp.YIAddr().To4(), addr.To4(); got.Equal(want) {
t.Errorf("DHCPOFFER for wrong IP: got offered %v (in use!)", got)
}
})
}
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())
}
}
func TestPersistentStorage(t *testing.T) {
handler, cleanup := testHandler(t)
defer cleanup()
var (
addr = net.IP{192, 168, 42, 4}
hardwareAddr = net.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66}
)
handler.SetLeases([]*Lease{
{
Num: 2,
Addr: addr,
HardwareAddr: hardwareAddr.String(),
},
})
p := request(net.IPv4zero, hardwareAddr)
resp := handler.serveDHCP(p, dhcp4.Discover, p.ParseOptions())
if got, want := resp.YIAddr().To4(), addr.To4(); !got.Equal(want) {
t.Errorf("DHCPOFFER for wrong IP: got %v, want %v", got, want)
}
}
func TestMinimumLeaseTime(t *testing.T) {
handler, cleanup := testHandler(t)
defer cleanup()
addr := net.IP{192, 168, 42, 23}
for _, tt := range []struct {
hwaddr net.HardwareAddr
wantLeaseTime time.Duration
}{
{
hwaddr: net.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66},
wantLeaseTime: 20 * time.Minute,
},
{
// Nintendo MAC address range for Nintendo Switch specific minimum
// lease time quirk:
hwaddr: net.HardwareAddr{0x7c, 0xbb, 0x8a, 0x11, 0x22, 0x33},
wantLeaseTime: 1 * time.Hour,
},
} {
p := discover(addr, tt.hwaddr)
resp := handler.serveDHCP(p, dhcp4.Discover, p.ParseOptions())
if resp == nil {
t.Fatalf("DHCPDISCOVER(%v) = nil", tt.hwaddr)
}
if got, want := messageType(resp), dhcp4.Offer; got != want {
t.Errorf("DHCPDISCOVER resulted in unexpected message type: got %v, want %v", got, want)
}
opts := resp.ParseOptions()
leaseTimeBytes, ok := opts[dhcp4.OptionIPAddressLeaseTime]
if !ok {
t.Fatalf("DHCPDISCOVER(%v): lease time option not set", tt.hwaddr)
}
leaseTimeSecs := binary.BigEndian.Uint32(leaseTimeBytes)
want := uint32(tt.wantLeaseTime.Seconds())
if got := leaseTimeSecs; got != want {
t.Errorf("unexpected lease time for hwaddr %v: got %d, want %d", tt.hwaddr, got, want)
}
}
}
func TestClientDecline(t *testing.T) {
handler, cleanup := testHandler(t)
defer cleanup()
now := time.Now()
handler.timeNow = func() time.Time { return now }
addr := net.IP{192, 168, 42, 23}
hardwareAddr := net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}
// macbook requests a new lease
t.Run("mbp grabs an address", func(t *testing.T) {
p := request(addr, hardwareAddr)
resp := handler.serveDHCP(p, dhcp4.Request, p.ParseOptions())
if got, want := messageType(resp), dhcp4.ACK; got != want {
t.Errorf("DHCPREQUEST resulted in unexpected message type: got %v, want %v", got, want)
}
})
// macbook lease expires
now = now.Add(3 * time.Hour)
t.Run("mbp requests previous address", func(t *testing.T) {
p := request(addr, hardwareAddr)
resp := handler.serveDHCP(p, dhcp4.Discover, p.ParseOptions())
if got, want := resp.YIAddr().To4(), addr.To4(); !got.Equal(want) {
t.Errorf("DHCPOFFER for wrong IP: got %v, want %v", got, want)
}
p = request(addr, hardwareAddr)
resp = handler.serveDHCP(p, dhcp4.Request, p.ParseOptions())
if got, want := messageType(resp), dhcp4.ACK; got != want {
t.Errorf("DHCPREQUEST resulted in unexpected message type: got %v, want %v", got, want)
}
})
t.Run("mbp declines address", func(t *testing.T) {
p := decline(addr, hardwareAddr)
resp := handler.serveDHCP(p, dhcp4.Decline, p.ParseOptions())
if resp != nil {
t.Fatalf("DHCPDECLINE was unexpectedly answered: %v", messageType(resp))
}
})
t.Run("mbp requests any address", func(t *testing.T) {
p := request(net.IPv4zero, hardwareAddr)
resp := handler.serveDHCP(p, dhcp4.Discover, p.ParseOptions())
if got, want := resp.YIAddr().To4(), addr.To4(); got.Equal(want) {
t.Errorf("DHCPOFFER returned unexpected address: %v", got)
}
})
}