Steward 0.1.0 (in development): a DHCPv4 server with a web UI, API and CLI
Subnets served per interface, pools, reservations, options at three levels, leases in SQLite saved before the ACK, ping and decline conflict checks, validated changes with history and undo, users and tokens, an audit log, a Kea importer, a sandboxed systemd unit and Debian packages. Tested with real udhcpc and dhcpcd clients in Docker (scripts/e2e.sh). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
3c0808591f
111 files changed
+17851
No files matched your search
@@ -0,0 +1,446 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user