mirror of
https://github.com/soypat/lneto.git
synced 2026-08-21 06:59:04 +00:00
HTTP server example and DHCP Server rewrite (#38)
* improvements to dhcpv4 server * begin adding examples/httpserver * add better VLAN tagging methods * pcap: add error printing; fix bug in CRC * ipv4: ToS and Flags construction and flag manipulation improvements * add dhcp interception to httptap and improve httpserver example
This commit is contained in:
@@ -0,0 +1,358 @@
|
||||
package dhcpv4
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testServerConfig(svAddr [4]byte) ServerConfig {
|
||||
return ServerConfig{
|
||||
ServerAddr: svAddr,
|
||||
Subnet: netip.PrefixFrom(netip.AddrFrom4(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 := 0; i < nClients; i++ {
|
||||
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 := byte(0); i < 2; i++ {
|
||||
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 := byte(0); i < 2; i++ {
|
||||
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: netip.PrefixFrom(netip.AddrFrom4(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: netip.PrefixFrom(netip.AddrFrom4([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())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user