Files
lneto/dhcp/dhcpv4/allocator_test.go
2026-06-20 18:20:18 -03:00

165 lines
4.6 KiB
Go

package dhcpv4
import (
"net/netip"
"testing"
"time"
"github.com/soypat/lneto/dhcp"
"github.com/soypat/lneto/ipv4"
)
// recordingAllocator is a test [dhcp.Allocator] that hands out a fixed address,
// records whether the server-derived options reached its AppendOptions hook, and
// appends a marker option of its own.
type recordingAllocator struct {
addr netip.Addr
sawRouter bool
appended bool
}
func (a *recordingAllocator) binding() dhcp.Binding {
return dhcp.Binding{
T1: 30,
T2: 52,
Leases: []dhcp.Lease{{Addr: a.addr, Preferred: 60, Valid: 60}},
}
}
func (a *recordingAllocator) Offer(dhcp.Request) (dhcp.Binding, error) { return a.binding(), nil }
func (a *recordingAllocator) Commit(dhcp.Request) (dhcp.Binding, error) { return a.binding(), nil }
func (a *recordingAllocator) Release([]byte) error { return nil }
func (a *recordingAllocator) Decline(dhcp.Request) error { return nil }
func (a *recordingAllocator) AppendOptions(dst []byte, _ []byte, _ dhcp.Binding) ([]byte, error) {
// dst already contains the server-derived options. Confirm the router
// option the server was configured to send is present.
for i := 0; i+1 < len(dst); {
op := OptNum(dst[i])
if op == OptEnd {
break
}
if op == OptRouter {
a.sawRouter = true
}
i += 2 + int(dst[i+1])
}
// Append our own option into the remaining capacity.
tail := dst[len(dst):cap(dst)]
n, err := EncodeOptionString(tail, OptDomainName, "example")
if err != nil {
return dst, err
}
a.appended = true
return dst[:len(dst)+n], nil
}
// TestServerCustomAllocator verifies the server delegates address assignment to
// an injected allocator, that the allocator's AppendOptions hook sees the
// server-derived options, and that options it appends reach the client.
func TestServerCustomAllocator(t *testing.T) {
svAddr := [4]byte{192, 168, 1, 1}
want := netip.AddrFrom4([4]byte{10, 9, 8, 7})
alloc := &recordingAllocator{addr: want}
var sv Server
err := sv.Configure(ServerConfig{
ServerAddr: svAddr,
Gateway: [4]byte{192, 168, 1, 254},
Subnet: ipv4.PrefixFrom(svAddr, 24),
Allocator: alloc,
})
if err != nil {
t.Fatal(err)
}
var cl Client
if err := cl.BeginRequest(42, RequestConfig{ClientHardwareAddr: [6]byte{1, 2, 3, 4, 5, 6}}); err != nil {
t.Fatal(err)
}
var buf [1024]byte
n, err := cl.Encapsulate(buf[:], -1, 0)
if err != nil {
t.Fatal(err)
}
if err := sv.Demux(buf[:n], 0); err != nil {
t.Fatal(err)
}
n, err = sv.Encapsulate(buf[:], -1, 0)
if err != nil {
t.Fatal(err)
} else if n == 0 {
t.Fatal("no offer emitted")
}
frm, _ := NewFrame(buf[:n])
if got := *frm.YIAddr(); got != want.As4() {
t.Errorf("offered address: got %v want %v", got, want)
}
if !alloc.sawRouter {
t.Error("allocator AppendOptions did not see the server-derived router option")
}
var foundDomain bool
frm.ForEachOption(func(_ int, opt OptNum, data []byte) error {
if opt == OptDomainName && string(data) == "example" {
foundDomain = true
}
return nil
})
if !foundDomain {
t.Error("allocator-appended domain option did not reach the emitted frame")
}
}
// TestMapAllocatorExpiration verifies that the default allocator reclaims an
// expired lease back into the pool once its valid lifetime has elapsed.
func TestMapAllocatorExpiration(t *testing.T) {
svAddr := [4]byte{192, 168, 1, 1}
now := time.Unix(1_000_000, 0)
clock := func() time.Time { return now }
a, err := NewMapAllocator(AllocatorConfig{
ServerAddr: svAddr,
Subnet: ipv4.PrefixFrom(svAddr, 24),
LeaseSeconds: 10,
Now: clock,
})
if err != nil {
t.Fatal(err)
}
// Client 1 acquires and commits an address.
b1, err := a.Offer(dhcp.Request{ClientID: []byte{1}})
if err != nil {
t.Fatal(err)
}
addr1, _ := b1.Addr()
if _, err := a.Commit(dhcp.Request{ClientID: []byte{1}}); err != nil {
t.Fatal(err)
}
// While client 1 holds the lease, another client requesting the same
// address must not be granted it.
b2, err := a.Offer(dhcp.Request{ClientID: []byte{2}, Requested: addr1})
if err != nil {
t.Fatal(err)
}
if addr2, _ := b2.Addr(); addr2 == addr1 {
t.Fatalf("addr %v handed out while still leased to client 1", addr1)
}
if err := a.Release([]byte{2}); err != nil {
t.Fatal(err)
}
// Advance time past client 1's lease; its address must be reclaimable.
now = now.Add(20 * time.Second)
b3, err := a.Offer(dhcp.Request{ClientID: []byte{3}, Requested: addr1})
if err != nil {
t.Fatal(err)
}
if addr3, _ := b3.Addr(); addr3 != addr1 {
t.Errorf("expired address not reclaimed: got %v want %v", addr3, addr1)
}
}