mirror of
https://github.com/soypat/lneto.git
synced 2026-07-26 10:38:47 +00:00
a2970b923d
* add ipv6 to xnet.StackAsync * dns improvements * improve DNS workings of StackAsync * add tentative ICMPv6 * work on prefixes and fix some small bugs, plan UDP/TCP6 * fix bugs in StackAsync and ipv4.Prefix.Contains * update arpsubtable * completely remove legacy internet.StackIP for StackIPv4/v6 * ipv4/ipv6 tcp/udp * add TCP6/UDP6 dialing APIs * add xnet.Stack6 interface * more ipv6 integration into StackAsync; various tweaks to lneto and documentation+TODOs * add stack6 tests * replace netip.Prefix with ipv4.Prefix where it makes sense
360 lines
9.5 KiB
Go
360 lines
9.5 KiB
Go
package dhcpv4
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/soypat/lneto/ipv4"
|
|
)
|
|
|
|
func testServerConfig(svAddr [4]byte) ServerConfig {
|
|
return ServerConfig{
|
|
ServerAddr: svAddr,
|
|
Subnet: ipv4.PrefixFrom(svAddr, 24),
|
|
}
|
|
}
|
|
|
|
// TestServerMultipleClients verifies the server can handle multiple clients
|
|
// going through the full DORA flow independently.
|
|
func TestServerMultipleClients(t *testing.T) {
|
|
svAddr := [4]byte{192, 168, 1, 1}
|
|
var sv Server
|
|
sv.Configure(testServerConfig(svAddr))
|
|
|
|
const nClients = 3
|
|
var clients [nClients]Client
|
|
var bufs [nClients][1024]byte
|
|
|
|
for i := range clients {
|
|
err := clients[i].BeginRequest(uint32(100+i), RequestConfig{
|
|
ClientHardwareAddr: [6]byte{0, 0, 0, 0, 0, byte(i + 1)},
|
|
Hostname: "host",
|
|
ClientID: string([]byte{byte(i + 1)}),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("client %d BeginRequest: %v", i, err)
|
|
}
|
|
}
|
|
|
|
// Phase 1: All clients send DISCOVER.
|
|
for i := range clients {
|
|
n, err := clients[i].Encapsulate(bufs[i][:], -1, 0)
|
|
if err != nil {
|
|
t.Fatalf("client %d discover encapsulate: %v", i, err)
|
|
}
|
|
err = sv.Demux(bufs[i][:n], 0)
|
|
if err != nil {
|
|
t.Fatalf("client %d discover demux: %v", i, err)
|
|
}
|
|
}
|
|
|
|
// Route server responses to the correct client by XID (map iteration is non-deterministic).
|
|
clientByXID := make(map[uint32]int)
|
|
for i := range clients {
|
|
clientByXID[uint32(100+i)] = i
|
|
}
|
|
|
|
// Phase 2: Server sends all OFFERs, clients receive.
|
|
var assignedAddrs [nClients][4]byte
|
|
for range clients {
|
|
var buf [1024]byte
|
|
n, err := sv.Encapsulate(buf[:], -1, 0)
|
|
if err != nil {
|
|
t.Fatalf("offer encapsulate: %v", err)
|
|
} else if n == 0 {
|
|
t.Fatal("no offer from server")
|
|
}
|
|
frm, _ := NewFrame(buf[:n])
|
|
ci := clientByXID[frm.XID()]
|
|
assignedAddrs[ci] = *frm.YIAddr()
|
|
err = clients[ci].Demux(buf[:n], 0)
|
|
if err != nil {
|
|
t.Fatalf("client %d offer demux: %v", ci, err)
|
|
}
|
|
}
|
|
|
|
// Phase 3: All clients send REQUEST.
|
|
for i := range clients {
|
|
n, err := clients[i].Encapsulate(bufs[i][:], -1, 0)
|
|
if err != nil {
|
|
t.Fatalf("client %d request encapsulate: %v", i, err)
|
|
} else if n == 0 {
|
|
t.Fatalf("client %d: no request data", i)
|
|
}
|
|
err = sv.Demux(bufs[i][:n], 0)
|
|
if err != nil {
|
|
t.Fatalf("client %d request demux: %v", i, err)
|
|
}
|
|
}
|
|
|
|
// Phase 4: Server sends all ACKs, clients receive.
|
|
for range clients {
|
|
var buf [1024]byte
|
|
n, err := sv.Encapsulate(buf[:], -1, 0)
|
|
if err != nil {
|
|
t.Fatalf("ack encapsulate: %v", err)
|
|
} else if n == 0 {
|
|
t.Fatal("no ack from server")
|
|
}
|
|
frm, _ := NewFrame(buf[:n])
|
|
ci := clientByXID[frm.XID()]
|
|
err = clients[ci].Demux(buf[:n], 0)
|
|
if err != nil {
|
|
t.Fatalf("client %d ack demux: %v", ci, err)
|
|
}
|
|
if clients[ci].State() != StateBound {
|
|
t.Errorf("client %d: want StateBound, got %s", ci, clients[ci].State())
|
|
}
|
|
}
|
|
|
|
// All assigned addresses must be unique.
|
|
for i := range nClients {
|
|
for j := i + 1; j < nClients; j++ {
|
|
if assignedAddrs[i] == assignedAddrs[j] {
|
|
t.Errorf("clients %d and %d got same address %v", i, j, assignedAddrs[i])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestServerSequentialAddressAllocation verifies that the server allocates
|
|
// addresses sequentially starting from serverAddr+1.
|
|
func TestServerSequentialAddressAllocation(t *testing.T) {
|
|
svAddr := [4]byte{192, 168, 1, 1}
|
|
var sv Server
|
|
sv.Configure(testServerConfig(svAddr))
|
|
|
|
// Build raw DISCOVER frames for two clients.
|
|
for i := range byte(2) {
|
|
var buf [512]byte
|
|
frm, _ := NewFrame(buf[:])
|
|
frm.ClearHeader()
|
|
frm.SetOp(OpRequest)
|
|
frm.SetHardware(1, 6, 0)
|
|
frm.SetXID(uint32(200 + i))
|
|
frm.SetSecs(1)
|
|
copy(frm.CHAddrAs6()[:], []byte{0, 0, 0, 0, 0, 10 + i})
|
|
frm.SetMagicCookie(MagicCookie)
|
|
opts := buf[OptionsOffset:]
|
|
n := writeOption(opts, OptMessageType, byte(MsgDiscover))
|
|
n += writeOption(opts[n:], OptClientIdentifier, 10+i)
|
|
opts[n] = byte(OptEnd)
|
|
n++
|
|
|
|
err := sv.Demux(buf[:OptionsOffset+n], 0)
|
|
if err != nil {
|
|
t.Fatalf("discover %d: %v", i, err)
|
|
}
|
|
}
|
|
|
|
// Encapsulate both OFFERs and verify addresses are in expected range.
|
|
var seen [2][4]byte
|
|
for i := range byte(2) {
|
|
var buf [512]byte
|
|
n, err := sv.Encapsulate(buf[:], -1, 0)
|
|
if err != nil {
|
|
t.Fatalf("offer %d encapsulate: %v", i, err)
|
|
} else if n == 0 {
|
|
t.Fatalf("offer %d: no data", i)
|
|
}
|
|
frm, _ := NewFrame(buf[:n])
|
|
seen[i] = *frm.YIAddr()
|
|
if seen[i][0] != 192 || seen[i][1] != 168 || seen[i][2] != 1 {
|
|
t.Errorf("offer %d: unexpected subnet in %v", i, seen[i])
|
|
}
|
|
if seen[i][3] != 2 && seen[i][3] != 3 {
|
|
t.Errorf("offer %d: expected .2 or .3, got .%d", i, seen[i][3])
|
|
}
|
|
}
|
|
if seen[0] == seen[1] {
|
|
t.Errorf("both offers got same address %v", seen[0])
|
|
}
|
|
}
|
|
|
|
// TestServerOfferContainsOptions verifies that server OFFER responses
|
|
// contain the expected DHCP options from the ServerConfig.
|
|
func TestServerOfferContainsOptions(t *testing.T) {
|
|
svAddr := [4]byte{192, 168, 1, 1}
|
|
gwAddr := [4]byte{192, 168, 1, 254}
|
|
dnsAddr := [4]byte{8, 8, 8, 8}
|
|
var sv Server
|
|
sv.Configure(ServerConfig{
|
|
ServerAddr: svAddr,
|
|
Gateway: gwAddr,
|
|
DNS: dnsAddr,
|
|
Subnet: ipv4.PrefixFrom(svAddr, 24),
|
|
LeaseSeconds: 7200,
|
|
})
|
|
|
|
var cl Client
|
|
err := cl.BeginRequest(500, RequestConfig{
|
|
ClientHardwareAddr: [6]byte{0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var buf [1024]byte
|
|
n, err := cl.Encapsulate(buf[:], -1, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = sv.Demux(buf[:n], 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
n, err = sv.Encapsulate(buf[:], -1, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
frm, _ := NewFrame(buf[:n])
|
|
|
|
var gotServerID, gotRouter, gotSubnet, gotDNS [4]byte
|
|
var gotLease, gotRenew, gotRebind uint32
|
|
var foundServerID, foundRouter, foundSubnet, foundDNS, foundLease bool
|
|
frm.ForEachOption(func(_ int, opt OptNum, data []byte) error {
|
|
switch opt {
|
|
case OptServerIdentification:
|
|
if len(data) == 4 {
|
|
foundServerID = true
|
|
copy(gotServerID[:], data)
|
|
}
|
|
case OptRouter:
|
|
if len(data) == 4 {
|
|
foundRouter = true
|
|
copy(gotRouter[:], data)
|
|
}
|
|
case OptSubnetMask:
|
|
if len(data) == 4 {
|
|
foundSubnet = true
|
|
copy(gotSubnet[:], data)
|
|
}
|
|
case OptDNSServers:
|
|
if len(data) == 4 {
|
|
foundDNS = true
|
|
copy(gotDNS[:], data)
|
|
}
|
|
case OptIPAddressLeaseTime:
|
|
if len(data) == 4 {
|
|
foundLease = true
|
|
gotLease = maybeU32(data)
|
|
}
|
|
case OptRenewTimeValue:
|
|
gotRenew = maybeU32(data)
|
|
case OptRebindingTimeValue:
|
|
gotRebind = maybeU32(data)
|
|
}
|
|
return nil
|
|
})
|
|
if !foundServerID || gotServerID != svAddr {
|
|
t.Errorf("server ID: found=%v got=%v want=%v", foundServerID, gotServerID, svAddr)
|
|
}
|
|
if !foundRouter || gotRouter != gwAddr {
|
|
t.Errorf("router: found=%v got=%v want=%v", foundRouter, gotRouter, gwAddr)
|
|
}
|
|
if !foundSubnet || gotSubnet != [4]byte{255, 255, 255, 0} {
|
|
t.Errorf("subnet: found=%v got=%v want=255.255.255.0", foundSubnet, gotSubnet)
|
|
}
|
|
if !foundDNS || gotDNS != dnsAddr {
|
|
t.Errorf("DNS: found=%v got=%v want=%v", foundDNS, gotDNS, dnsAddr)
|
|
}
|
|
if !foundLease || gotLease != 7200 {
|
|
t.Errorf("lease: found=%v got=%v want=7200", foundLease, gotLease)
|
|
}
|
|
if gotRenew != 3600 {
|
|
t.Errorf("renew T1: got %d want 3600", gotRenew)
|
|
}
|
|
if gotRebind != 6300 {
|
|
t.Errorf("rebind T2: got %d want 6300", gotRebind)
|
|
}
|
|
}
|
|
|
|
// TestServerEncapsulateNoPending verifies Encapsulate returns 0 bytes
|
|
// when there are no pending responses.
|
|
func TestServerEncapsulateNoPending(t *testing.T) {
|
|
var sv Server
|
|
sv.Configure(testServerConfig([4]byte{192, 168, 1, 1}))
|
|
|
|
var buf [512]byte
|
|
n, err := sv.Encapsulate(buf[:], -1, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n != 0 {
|
|
t.Errorf("expected 0 bytes from empty server, got %d", n)
|
|
}
|
|
}
|
|
|
|
// TestServerConfigValidation verifies that Configure rejects invalid configurations.
|
|
func TestServerConfigValidation(t *testing.T) {
|
|
var sv Server
|
|
err := sv.Configure(ServerConfig{
|
|
ServerAddr: [4]byte{192, 168, 1, 1},
|
|
})
|
|
if err == nil {
|
|
t.Error("expected error for zero subnet")
|
|
}
|
|
err = sv.Configure(ServerConfig{
|
|
ServerAddr: [4]byte{10, 0, 0, 1},
|
|
Subnet: ipv4.PrefixFrom([4]byte{192, 168, 1, 0}, 24),
|
|
})
|
|
if err == nil {
|
|
t.Error("expected error for server address outside subnet")
|
|
}
|
|
}
|
|
|
|
// TestServerRediscover verifies that a client that was previously bound
|
|
// can send a fresh DISCOVER and get re-served.
|
|
func TestServerRediscover(t *testing.T) {
|
|
svAddr := [4]byte{192, 168, 1, 1}
|
|
var sv Server
|
|
sv.Configure(testServerConfig(svAddr))
|
|
|
|
// First DORA cycle.
|
|
var cl Client
|
|
cl.BeginRequest(1, RequestConfig{
|
|
ClientHardwareAddr: [6]byte{1, 2, 3, 4, 5, 6},
|
|
ClientID: "rediscover-client",
|
|
})
|
|
var buf [1024]byte
|
|
n, _ := cl.Encapsulate(buf[:], -1, 0)
|
|
sv.Demux(buf[:n], 0)
|
|
n, _ = sv.Encapsulate(buf[:], -1, 0)
|
|
cl.Demux(buf[:n], 0)
|
|
n, _ = cl.Encapsulate(buf[:], -1, 0)
|
|
sv.Demux(buf[:n], 0)
|
|
n, _ = sv.Encapsulate(buf[:], -1, 0)
|
|
cl.Demux(buf[:n], 0)
|
|
if cl.State() != StateBound {
|
|
t.Fatalf("first DORA: want StateBound, got %s", cl.State())
|
|
}
|
|
|
|
// Client reboots and sends fresh DISCOVER.
|
|
cl.Reset()
|
|
cl.BeginRequest(2, RequestConfig{
|
|
ClientHardwareAddr: [6]byte{1, 2, 3, 4, 5, 6},
|
|
ClientID: "rediscover-client",
|
|
})
|
|
n, _ = cl.Encapsulate(buf[:], -1, 0)
|
|
err := sv.Demux(buf[:n], 0)
|
|
if err != nil {
|
|
t.Fatalf("rediscover demux: %v", err)
|
|
}
|
|
n, _ = sv.Encapsulate(buf[:], -1, 0)
|
|
if n == 0 {
|
|
t.Fatal("no offer after rediscover")
|
|
}
|
|
err = cl.Demux(buf[:n], 0)
|
|
if err != nil {
|
|
t.Fatalf("rediscover offer demux: %v", err)
|
|
}
|
|
// Complete the second DORA.
|
|
n, _ = cl.Encapsulate(buf[:], -1, 0)
|
|
sv.Demux(buf[:n], 0)
|
|
n, _ = sv.Encapsulate(buf[:], -1, 0)
|
|
cl.Demux(buf[:n], 0)
|
|
if cl.State() != StateBound {
|
|
t.Errorf("second DORA: want StateBound, got %s", cl.State())
|
|
}
|
|
}
|