- interfaces.exclude in steward.yaml (names or shell patterns) leaves interfaces out of GET /interfaces, the web UI's picker and 'steward interface list'. Subnets on them are still served. - With the ping check on, active leases are pinged every minute; a client that answers gets a last-seen time (leases.last_seen, migration 4). Not answering never takes a client offline. - The activity log filters by subnet (--subnet, the Activity page, GET /events?subnet=). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
502 lines
17 KiB
Go
502 lines
17 KiB
Go
package dhcpd
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net"
|
|
"net/netip"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"steward/internal/dhcp4"
|
|
"steward/internal/model"
|
|
"steward/internal/store"
|
|
)
|
|
|
|
type fakeIfaces map[string][]netip.Prefix
|
|
|
|
func (f fakeIfaces) Addrs(name string) ([]netip.Prefix, error) {
|
|
a, ok := f[name]
|
|
if !ok {
|
|
return nil, errors.New("no interface " + name)
|
|
}
|
|
return a, nil
|
|
}
|
|
|
|
type fakePinger struct {
|
|
mu sync.Mutex
|
|
answers map[netip.Addr]bool
|
|
pinged []netip.Addr
|
|
}
|
|
|
|
func (f *fakePinger) Ping(_ context.Context, a netip.Addr, _ time.Duration) bool {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.pinged = append(f.pinged, a)
|
|
return f.answers[a]
|
|
}
|
|
|
|
type harness struct {
|
|
t *testing.T
|
|
s *Server
|
|
st *store.Store
|
|
ping *fakePinger
|
|
clock time.Time
|
|
}
|
|
|
|
func pool(s string) model.Pool {
|
|
p, err := model.ParsePool(s)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return p
|
|
}
|
|
|
|
func newHarness(t *testing.T, edit func(st *model.State)) *harness {
|
|
t.Helper()
|
|
db, err := store.Open(":memory:")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ifs := fakeIfaces{
|
|
"eth0": {netip.MustParsePrefix("10.0.10.2/24")},
|
|
"eth0.20": {netip.MustParsePrefix("10.0.20.1/24")},
|
|
"eth1": {netip.MustParsePrefix("192.168.1.1/24")},
|
|
}
|
|
h := &harness{t: t, st: db, ping: &fakePinger{answers: map[netip.Addr]bool{}}, clock: time.Date(2026, 10, 14, 12, 0, 0, 0, time.UTC)}
|
|
h.s = New(nil, db, ifs, h.ping)
|
|
h.s.now = func() time.Time { return h.clock }
|
|
st := &model.State{Settings: model.DefaultSettings(),
|
|
Subnets: []*model.Subnet{
|
|
{ID: 1, Name: "office", Interface: "eth0", Prefix: netip.MustParsePrefix("10.0.10.0/24"), Pools: []model.Pool{pool("10.0.10.100-10.0.10.103")},
|
|
Options: []model.OptionValue{{Code: 3, Value: "10.0.10.1"}, {Code: 6, Value: "10.0.10.2"}, {Code: 15, Value: "lan"}, {Code: 42, Value: "10.0.10.2"}}},
|
|
{ID: 2, Name: "iot", Interface: "eth0.20", Prefix: netip.MustParsePrefix("10.0.20.0/24"), Pools: []model.Pool{pool("10.0.20.100-10.0.20.200")}, LeaseTime: 3600},
|
|
},
|
|
Reservations: []*model.Reservation{
|
|
{ID: 3, MAC: "aa:00:00:00:00:99", IP: netip.MustParseAddr("10.0.10.20"), Hostname: "nas", Options: []model.OptionValue{{Code: 6, Value: "1.1.1.1"}}},
|
|
{ID: 4, MAC: "aa:00:00:00:00:98", IP: netip.MustParseAddr("10.0.10.101"), Hostname: "printer"},
|
|
}}
|
|
st.Settings.Options = []model.OptionValue{{Code: 6, Value: "9.9.9.9"}}
|
|
if edit != nil {
|
|
edit(st)
|
|
}
|
|
h.s.SetConfig(st)
|
|
return h
|
|
}
|
|
|
|
func mac(n byte) net.HardwareAddr { return net.HardwareAddr{0xaa, 0, 0, 0, 0, n} }
|
|
|
|
func (h *harness) send(iface string, p *dhcp4.Packet) *Outbound {
|
|
h.t.Helper()
|
|
// Through the wire format, as for real.
|
|
q, err := dhcp4.Parse(p.Marshal())
|
|
if err != nil {
|
|
h.t.Fatal(err)
|
|
}
|
|
out := h.s.Handle(context.Background(), iface, q)
|
|
if out != nil {
|
|
r, err := dhcp4.Parse(out.Packet.Marshal())
|
|
if err != nil {
|
|
h.t.Fatal(err)
|
|
}
|
|
out.Packet = r
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (h *harness) discover(iface string, m net.HardwareAddr, opts ...dhcp4.Option) *Outbound {
|
|
p := dhcp4.NewRequest(dhcp4.Discover, m, 1)
|
|
for _, o := range opts {
|
|
p.Set(o.Code, o.Data)
|
|
}
|
|
return h.send(iface, p)
|
|
}
|
|
|
|
// lease runs DISCOVER and REQUEST and returns the ACK.
|
|
func (h *harness) lease(iface string, m net.HardwareAddr) *dhcp4.Packet {
|
|
h.t.Helper()
|
|
off := h.discover(iface, m)
|
|
if off == nil || off.Packet.Type() != dhcp4.Offer {
|
|
h.t.Fatalf("no offer for %s on %s", m, iface)
|
|
}
|
|
sid, _ := off.Packet.IPOption(dhcp4.OptServerID)
|
|
req := dhcp4.NewRequest(dhcp4.Request, m, 2)
|
|
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:]))
|
|
ack := h.send(iface, req)
|
|
if ack == nil || ack.Packet.Type() != dhcp4.Ack {
|
|
h.t.Fatalf("no ACK for %s: %+v", m, ack)
|
|
}
|
|
if ack.Packet.YIAddr != off.Packet.YIAddr {
|
|
h.t.Fatalf("ACK %s differs from offer %s", ack.Packet.YIAddr, off.Packet.YIAddr)
|
|
}
|
|
return ack.Packet
|
|
}
|
|
|
|
func ipOpt(p *dhcp4.Packet, code byte) string {
|
|
return dhcp4.Decode(dhcp4.TypeIPs, p.Get(code))
|
|
}
|
|
|
|
func TestOfferAndAck(t *testing.T) {
|
|
h := newHarness(t, nil)
|
|
off := h.discover("eth0", mac(1))
|
|
if off == nil {
|
|
t.Fatal("no offer")
|
|
}
|
|
p := off.Packet
|
|
if p.YIAddr.String() != "10.0.10.100" || off.Dst.IsValid() {
|
|
t.Errorf("offered %s to %v", p.YIAddr, off.Dst)
|
|
}
|
|
if ipOpt(p, dhcp4.OptServerID) != "10.0.10.2" || ipOpt(p, dhcp4.OptSubnetMask) != "255.255.255.0" || ipOpt(p, dhcp4.OptRouters) != "10.0.10.1" {
|
|
t.Errorf("options: sid %s mask %s routers %s", ipOpt(p, 54), ipOpt(p, 1), ipOpt(p, 3))
|
|
}
|
|
// The subnet's DNS server wins over the settings'.
|
|
if ipOpt(p, dhcp4.OptDNSServers) != "10.0.10.2" || string(p.Get(dhcp4.OptDomainName)) != "lan" {
|
|
t.Errorf("dns %s domain %q", ipOpt(p, 6), p.Get(15))
|
|
}
|
|
if dhcp4.Decode(dhcp4.TypeU32, p.Get(dhcp4.OptLeaseTime)) != "43200" || dhcp4.Decode(dhcp4.TypeU32, p.Get(dhcp4.OptRenewalTime)) != "21600" {
|
|
t.Errorf("lease time %v", p.Get(51))
|
|
}
|
|
ack := h.lease("eth0", mac(1))
|
|
l := h.s.Lease(ack.YIAddr)
|
|
if l == nil || l.State != model.LeaseActive || l.MAC != mac(1).String() || l.Interface != "eth0" || l.SubnetID != 1 || l.Hostname != "client01" {
|
|
t.Fatalf("lease %+v", l)
|
|
}
|
|
// Saved before the ACK.
|
|
saved, _ := h.st.LoadLeases(context.Background())
|
|
if len(saved) != 1 || saved[0].IP != ack.YIAddr {
|
|
t.Errorf("saved %+v", saved)
|
|
}
|
|
}
|
|
|
|
func TestSubnetsPerInterface(t *testing.T) {
|
|
h := newHarness(t, nil)
|
|
if ip := h.lease("eth0.20", mac(1)).YIAddr; !strings.HasPrefix(ip.String(), "10.0.20.") {
|
|
t.Errorf("a client on eth0.20 got %s", ip)
|
|
}
|
|
// The same MAC on the office VLAN gets an office address.
|
|
if ip := h.lease("eth0", mac(1)).YIAddr; !strings.HasPrefix(ip.String(), "10.0.10.") {
|
|
t.Errorf("a client on eth0 got %s", ip)
|
|
}
|
|
// No subnet on eth1: no answer at all.
|
|
if out := h.discover("eth1", mac(2)); out != nil {
|
|
t.Errorf("answered on eth1: %+v", out)
|
|
}
|
|
// Asking eth0.20 for an office address: NAK.
|
|
req := dhcp4.NewRequest(dhcp4.Request, mac(3), 5)
|
|
req.Set(dhcp4.OptRequestedIP, dhcp4.IP(netip.MustParseAddr("10.0.10.100")))
|
|
out := h.send("eth0.20", req)
|
|
if out == nil || out.Packet.Type() != dhcp4.Nak || !strings.Contains(string(out.Packet.Get(dhcp4.OptMessage)), "does not belong on eth0.20") {
|
|
t.Errorf("init-reboot on the wrong VLAN: %+v", out)
|
|
}
|
|
}
|
|
|
|
func TestReservations(t *testing.T) {
|
|
h := newHarness(t, nil)
|
|
ack := h.lease("eth0", mac(0x99))
|
|
if ack.YIAddr.String() != "10.0.10.20" || string(ack.Get(dhcp4.OptHostName)) != "nas" || ipOpt(ack, 6) != "1.1.1.1" {
|
|
t.Errorf("reserved client got %s host %q dns %s", ack.YIAddr, ack.Get(12), ipOpt(ack, 6))
|
|
}
|
|
// 10.0.10.101 is reserved for the printer: nobody else gets it, even
|
|
// though it is in the pool.
|
|
got := map[string]bool{}
|
|
for i := byte(1); i <= 3; i++ {
|
|
got[h.lease("eth0", mac(i)).YIAddr.String()] = true
|
|
}
|
|
if got["10.0.10.101"] || !got["10.0.10.100"] || !got["10.0.10.102"] || !got["10.0.10.103"] {
|
|
t.Errorf("pool addresses handed out: %v", got)
|
|
}
|
|
// The pool is now full (the reserved address doesn't count).
|
|
if out := h.discover("eth0", mac(4)); out != nil {
|
|
t.Errorf("offered %s from a full pool", out.Packet.YIAddr)
|
|
}
|
|
if h.lease("eth0", mac(0x98)).YIAddr.String() != "10.0.10.101" {
|
|
t.Error("the printer didn't get its reservation")
|
|
}
|
|
// A client with a reservation asking for another address gets a NAK
|
|
// that says why.
|
|
req := dhcp4.NewRequest(dhcp4.Request, mac(0x99), 7)
|
|
req.Set(dhcp4.OptRequestedIP, dhcp4.IP(netip.MustParseAddr("10.0.10.102")))
|
|
out := h.send("eth0", req)
|
|
if out == nil || out.Packet.Type() != dhcp4.Nak || !strings.Contains(string(out.Packet.Get(dhcp4.OptMessage)), "reservation for 10.0.10.20") {
|
|
t.Errorf("got %+v", out)
|
|
}
|
|
}
|
|
|
|
func TestReturningClientKeepsAddress(t *testing.T) {
|
|
h := newHarness(t, nil)
|
|
first := h.lease("eth0", mac(1)).YIAddr
|
|
h.lease("eth0", mac(2))
|
|
rel := dhcp4.NewRequest(dhcp4.Release, mac(1), 9)
|
|
rel.CIAddr = first
|
|
if out := h.send("eth0", rel); out != nil {
|
|
t.Error("a release was answered")
|
|
}
|
|
if l := h.s.Lease(first); l.State != model.LeaseReleased {
|
|
t.Errorf("after release: %+v", l)
|
|
}
|
|
h.clock = h.clock.Add(time.Hour)
|
|
if again := h.lease("eth0", mac(1)).YIAddr; again != first {
|
|
t.Errorf("came back to %s, had %s", again, first)
|
|
}
|
|
// Expired leases too.
|
|
h.clock = h.clock.Add(48 * time.Hour)
|
|
if again := h.lease("eth0", mac(1)).YIAddr; again != first {
|
|
t.Errorf("after expiry got %s, had %s", again, first)
|
|
}
|
|
// A new client gets a never-used address before a released one.
|
|
if ip := h.lease("eth0", mac(3)).YIAddr; ip == first || ip.String() == "10.0.10.101" {
|
|
t.Errorf("new client got %s", ip)
|
|
}
|
|
}
|
|
|
|
func TestRenewAndRebind(t *testing.T) {
|
|
h := newHarness(t, nil)
|
|
ack := h.lease("eth0.20", mac(1))
|
|
start := h.s.Lease(ack.YIAddr).Start
|
|
h.clock = h.clock.Add(30 * time.Minute)
|
|
ren := dhcp4.NewRequest(dhcp4.Request, mac(1), 11)
|
|
ren.CIAddr = ack.YIAddr
|
|
out := h.send("eth0.20", ren)
|
|
if out == nil || out.Packet.Type() != dhcp4.Ack || out.Dst != ack.YIAddr || out.Packet.CIAddr != ack.YIAddr {
|
|
t.Fatalf("renewal: %+v", out)
|
|
}
|
|
l := h.s.Lease(ack.YIAddr)
|
|
if l.Hostname != "client01" {
|
|
t.Errorf("a renewal without a host name lost it: %q", l.Hostname)
|
|
}
|
|
if !l.Start.Equal(start) || !l.End.Equal(h.clock.Add(time.Hour)) {
|
|
t.Errorf("renewed lease %v - %v", l.Start, l.End)
|
|
}
|
|
// Another client renewing that address: NAK.
|
|
ren2 := dhcp4.NewRequest(dhcp4.Request, mac(2), 12)
|
|
ren2.CIAddr = ack.YIAddr
|
|
if out := h.send("eth0.20", ren2); out == nil || out.Packet.Type() != dhcp4.Nak {
|
|
t.Errorf("someone else's renewal: %+v", out)
|
|
}
|
|
}
|
|
|
|
func TestInitReboot(t *testing.T) {
|
|
h := newHarness(t, nil)
|
|
ip := h.lease("eth0", mac(1)).YIAddr
|
|
req := dhcp4.NewRequest(dhcp4.Request, mac(1), 20)
|
|
req.Set(dhcp4.OptRequestedIP, dhcp4.IP(ip))
|
|
if out := h.send("eth0", req); out == nil || out.Packet.Type() != dhcp4.Ack || out.Dst.IsValid() {
|
|
t.Errorf("own address after reboot: %+v", out)
|
|
}
|
|
// Someone else's address: NAK.
|
|
req = dhcp4.NewRequest(dhcp4.Request, mac(2), 21)
|
|
req.Set(dhcp4.OptRequestedIP, dhcp4.IP(ip))
|
|
if out := h.send("eth0", req); out == nil || out.Packet.Type() != dhcp4.Nak {
|
|
t.Errorf("another's address: %+v", out)
|
|
}
|
|
// A free pool address from a client Steward hasn't seen (say, after
|
|
// switching from another server): ACK.
|
|
req = dhcp4.NewRequest(dhcp4.Request, mac(3), 22)
|
|
req.Set(dhcp4.OptRequestedIP, dhcp4.IP(netip.MustParseAddr("10.0.10.103")))
|
|
if out := h.send("eth0", req); out == nil || out.Packet.Type() != dhcp4.Ack {
|
|
t.Errorf("free address: %+v", out)
|
|
}
|
|
// An address outside the pools: NAK.
|
|
req = dhcp4.NewRequest(dhcp4.Request, mac(4), 23)
|
|
req.Set(dhcp4.OptRequestedIP, dhcp4.IP(netip.MustParseAddr("10.0.10.50")))
|
|
if out := h.send("eth0", req); out == nil || out.Packet.Type() != dhcp4.Nak {
|
|
t.Errorf("address outside the pools: %+v", out)
|
|
}
|
|
}
|
|
|
|
func TestOtherServerChosen(t *testing.T) {
|
|
h := newHarness(t, nil)
|
|
off := h.discover("eth0", mac(1))
|
|
req := dhcp4.NewRequest(dhcp4.Request, mac(1), 2)
|
|
req.Set(dhcp4.OptServerID, dhcp4.IP(netip.MustParseAddr("10.0.10.254")))
|
|
req.Set(dhcp4.OptRequestedIP, dhcp4.IP(netip.MustParseAddr("10.0.10.77")))
|
|
if out := h.send("eth0", req); out != nil {
|
|
t.Errorf("answered a request to another server: %+v", out)
|
|
}
|
|
// The offer was given up: another client can have the address.
|
|
if out := h.discover("eth0", mac(2)); out == nil || out.Packet.YIAddr != off.Packet.YIAddr {
|
|
t.Errorf("offer not released: %+v", out)
|
|
}
|
|
}
|
|
|
|
func TestDeclineAndPingConflict(t *testing.T) {
|
|
h := newHarness(t, nil)
|
|
ip := h.lease("eth0", mac(1)).YIAddr
|
|
dec := dhcp4.NewRequest(dhcp4.Decline, mac(1), 30)
|
|
dec.Set(dhcp4.OptRequestedIP, dhcp4.IP(ip))
|
|
h.send("eth0", dec)
|
|
if l := h.s.Lease(ip); l.State != model.LeaseDeclined || !l.End.Equal(h.clock.Add(24*time.Hour)) {
|
|
t.Errorf("declined lease %+v", l)
|
|
}
|
|
// Not offered again, not even to the client that declined it.
|
|
if out := h.discover("eth0", mac(1)); out == nil || out.Packet.YIAddr == ip {
|
|
t.Errorf("declined address offered again: %+v", out)
|
|
}
|
|
// An address that answers a ping is skipped and held back.
|
|
h2 := newHarness(t, nil)
|
|
h2.ping.answers[netip.MustParseAddr("10.0.10.100")] = true
|
|
off := h2.discover("eth0", mac(1))
|
|
if off == nil || off.Packet.YIAddr.String() != "10.0.10.102" {
|
|
t.Fatalf("offer after a conflict: %+v", off)
|
|
}
|
|
if l := h2.s.Lease(netip.MustParseAddr("10.0.10.100")); l == nil || l.State != model.LeaseDeclined {
|
|
t.Errorf("conflict not recorded: %+v", l)
|
|
}
|
|
// Reservations aren't pinged (the device may still have its address).
|
|
h2.ping.pinged = nil
|
|
h2.lease("eth0", mac(0x99))
|
|
for _, a := range h2.ping.pinged {
|
|
if a.String() == "10.0.10.20" {
|
|
t.Error("pinged a reserved address")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSharedNetwork(t *testing.T) {
|
|
h := newHarness(t, func(st *model.State) {
|
|
st.Subnets = append(st.Subnets, &model.Subnet{ID: 5, Interface: "eth0", Prefix: netip.MustParsePrefix("10.0.11.0/24"), Pools: []model.Pool{pool("10.0.11.10-10.0.11.11")}})
|
|
})
|
|
got := map[string]bool{}
|
|
for i := byte(1); i <= 5; i++ {
|
|
got[h.lease("eth0", mac(i)).YIAddr.String()] = true
|
|
}
|
|
if !got["10.0.11.10"] || !got["10.0.11.11"] {
|
|
t.Errorf("second subnet not used: %v", got)
|
|
}
|
|
// Its clients are answered from the interface's address (10.0.10.2),
|
|
// with the second subnet's mask.
|
|
l := h.s.Lease(netip.MustParseAddr("10.0.11.10"))
|
|
ren := dhcp4.NewRequest(dhcp4.Request, net.HardwareAddr(mustMAC(l.MAC)), 3)
|
|
ren.CIAddr = l.IP
|
|
out := h.send("eth0", ren)
|
|
if out == nil || ipOpt(out.Packet, 54) != "10.0.10.2" || ipOpt(out.Packet, 1) != "255.255.255.0" {
|
|
t.Errorf("renewal in the second subnet: %+v", out)
|
|
}
|
|
}
|
|
|
|
func mustMAC(s string) net.HardwareAddr {
|
|
m, err := net.ParseMAC(s)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return m
|
|
}
|
|
|
|
func TestParameterRequestList(t *testing.T) {
|
|
h := newHarness(t, nil)
|
|
off := h.discover("eth0", mac(1), dhcp4.Option{Code: dhcp4.OptParamRequest, Data: []byte{42, 1}})
|
|
var codes []byte
|
|
for _, o := range off.Packet.Options {
|
|
codes = append(codes, o.Code)
|
|
}
|
|
// Type, server ID and lease times; then NTP and mask as asked; then
|
|
// the essentials the client didn't ask for.
|
|
want := []byte{53, 54, 51, 58, 59, 42, 1, 3, 6, 15}
|
|
if string(codes) != string(want) {
|
|
t.Errorf("options %v, want %v", codes, want)
|
|
}
|
|
}
|
|
|
|
func TestInformAndRelay(t *testing.T) {
|
|
h := newHarness(t, nil)
|
|
inf := dhcp4.NewRequest(dhcp4.Inform, mac(1), 40)
|
|
inf.CIAddr = netip.MustParseAddr("10.0.10.50")
|
|
out := h.send("eth0", inf)
|
|
if out == nil || out.Packet.Type() != dhcp4.Ack || out.Dst != inf.CIAddr || out.Packet.Has(dhcp4.OptLeaseTime) || ipOpt(out.Packet, 3) != "10.0.10.1" {
|
|
t.Errorf("inform: %+v", out)
|
|
}
|
|
rel := dhcp4.NewRequest(dhcp4.Discover, mac(2), 41)
|
|
rel.GIAddr = netip.MustParseAddr("10.0.30.1")
|
|
if out := h.send("eth0", rel); out != nil {
|
|
t.Error("answered a relayed request")
|
|
}
|
|
}
|
|
|
|
func TestNotAuthoritative(t *testing.T) {
|
|
h := newHarness(t, func(st *model.State) { st.Settings.NotAuthoritative = true })
|
|
req := dhcp4.NewRequest(dhcp4.Request, mac(3), 5)
|
|
req.Set(dhcp4.OptRequestedIP, dhcp4.IP(netip.MustParseAddr("192.168.7.7")))
|
|
if out := h.send("eth0", req); out != nil {
|
|
t.Errorf("NAK without authority: %+v", out)
|
|
}
|
|
}
|
|
|
|
func TestDisabledSubnet(t *testing.T) {
|
|
h := newHarness(t, func(st *model.State) { st.Subnets[1].Disabled = true })
|
|
if out := h.discover("eth0.20", mac(1)); out != nil {
|
|
t.Error("a disabled subnet answered")
|
|
}
|
|
}
|
|
|
|
func TestPrune(t *testing.T) {
|
|
h := newHarness(t, nil)
|
|
ip := h.lease("eth0", mac(1)).YIAddr
|
|
h.clock = h.clock.Add(40 * 24 * time.Hour)
|
|
h.s.Prune(context.Background())
|
|
if h.s.Lease(ip) != nil {
|
|
t.Error("old lease kept")
|
|
}
|
|
if ls, _ := h.st.LoadLeases(context.Background()); len(ls) != 0 {
|
|
t.Errorf("old lease still saved: %+v", ls)
|
|
}
|
|
}
|
|
|
|
func TestDisabledReservation(t *testing.T) {
|
|
// The printer's reservation (10.0.10.101, inside the pool) is
|
|
// disabled: the printer gets a pool address like anyone, and its
|
|
// reserved address can go to others.
|
|
h := newHarness(t, func(st *model.State) { st.Reservations[1].Disabled = true })
|
|
got := map[string]bool{}
|
|
for i := byte(1); i <= 4; i++ {
|
|
got[h.lease("eth0", mac(i)).YIAddr.String()] = true
|
|
}
|
|
if !got["10.0.10.101"] {
|
|
t.Errorf("the disabled reservation's address wasn't handed out: %v", got)
|
|
}
|
|
if out := h.discover("eth0", mac(0x98)); out != nil {
|
|
t.Errorf("the printer got %s from a full pool", out.Packet.YIAddr)
|
|
}
|
|
}
|
|
|
|
func TestLastSeen(t *testing.T) {
|
|
h := newHarness(t, nil)
|
|
up := h.lease("eth0", mac(1)).YIAddr
|
|
quiet := h.lease("eth0", mac(2)).YIAddr
|
|
h.ping.answers[up] = true
|
|
h.s.CheckSeen(context.Background())
|
|
if l := h.s.Lease(up); l.LastSeen == nil || !l.LastSeen.Equal(h.clock) {
|
|
t.Errorf("answering client: %+v", l)
|
|
}
|
|
// One that never answers has no last-seen time, and stays active.
|
|
if l := h.s.Lease(quiet); l.LastSeen != nil || !l.ActiveAt(h.clock) {
|
|
t.Errorf("quiet client: %+v", l)
|
|
}
|
|
// Later, the time stays when it stops answering, and survives a
|
|
// renewal and a restart.
|
|
seen := h.clock
|
|
h.clock = h.clock.Add(10 * time.Minute)
|
|
h.ping.answers[up] = false
|
|
h.s.CheckSeen(context.Background())
|
|
h.lease("eth0", mac(1))
|
|
if l := h.s.Lease(up); l.LastSeen == nil || !l.LastSeen.Equal(seen) {
|
|
t.Errorf("after a renewal: %+v", l)
|
|
}
|
|
ls, _ := h.st.LoadLeases(context.Background())
|
|
for _, l := range ls {
|
|
if l.IP == up && (l.LastSeen == nil || !l.LastSeen.Equal(seen)) {
|
|
t.Errorf("stored: %+v", l)
|
|
}
|
|
}
|
|
// Expired leases aren't pinged.
|
|
h.clock = h.clock.Add(48 * time.Hour)
|
|
h.ping.pinged = nil
|
|
h.s.CheckSeen(context.Background())
|
|
if len(h.ping.pinged) != 0 {
|
|
t.Errorf("pinged %v", h.ping.pinged)
|
|
}
|
|
}
|