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:
Pat Whittingslow
2026-02-18 20:48:30 +01:00
committed by GitHub
parent fae4552ddb
commit bf2d07d9f0
12 changed files with 1403 additions and 48 deletions
+5 -1
View File
@@ -2,6 +2,7 @@ package dhcpv4
import (
"bytes"
"net/netip"
"testing"
)
@@ -25,7 +26,10 @@ func TestClientServer(t *testing.T) {
t.Errorf("want client state %s, got %s", state.String(), cl.State().String())
}
}
sv.Reset(svAddr, DefaultServerPort)
sv.Configure(ServerConfig{
ServerAddr: svAddr,
Subnet: netip.PrefixFrom(netip.AddrFrom4(svAddr), 24),
})
// CLIENT DISCOVER.
assertClState(StateInit)
var buf [1024]byte
+145 -35
View File
@@ -11,15 +11,33 @@ import (
)
type Server struct {
connID uint64
nextAddr netip.Addr
prefix netip.Prefix
hosts map[[36]byte]serverEntry
vld lneto.Validator
pending int
port uint16
siaddr [4]byte
gwaddr [4]byte
connID uint64
nextAddr netip.Addr
prefix netip.Prefix
hosts map[[36]byte]serverEntry
vld lneto.Validator
pending int
leaseSeconds uint32
port uint16
siaddr [4]byte
gwaddr [4]byte
dns [4]byte
}
// ServerConfig contains configuration parameters for [Server.Configure].
type ServerConfig struct {
// ServerAddr is the DHCP server's own IPv4 address.
ServerAddr [4]byte
// Gateway advertised to clients as default router. Zero value omits the option.
Gateway [4]byte
// DNS server address advertised to clients. Zero value omits the option.
DNS [4]byte
// Subnet defines the network prefix for address allocation and subnet mask responses.
Subnet netip.Prefix
// LeaseSeconds is the lease duration. Zero defaults to 3600.
LeaseSeconds uint32
// Port is the server listening port. Zero defaults to DefaultServerPort.
Port uint16
}
type serverEntry struct {
@@ -35,30 +53,53 @@ type serverEntry struct {
// - Init: Server received discover, pending Offer sent out.
// - Selecting: Server sent out offer, request not received.
// - Requesting: Request received, pending Ack sent out.
// - Bound: Request sent out, no more pending data to be sent.
// - Bound: Ack sent out, no more pending data to be sent.
state ClientState
}
func (sv *Server) Reset(serverAddr [4]byte, port uint16) {
*sv = Server{
connID: sv.connID + 1,
siaddr: serverAddr,
port: port,
hosts: sv.hosts,
nextAddr: netip.AddrFrom4(serverAddr),
// Configure resets and configures the server with the given configuration.
// The connection ID is incremented on each call to invalidate existing connections.
// The hosts map is reused across calls to avoid reallocation.
func (sv *Server) Configure(cfg ServerConfig) error {
svAddr := netip.AddrFrom4(cfg.ServerAddr)
if !cfg.Subnet.IsValid() {
return errors.New("dhcpv4 server: invalid subnet")
} else if !cfg.Subnet.Contains(svAddr) {
return errors.New("dhcpv4 server: server address outside subnet")
}
if sv.hosts == nil {
sv.hosts = make(map[[36]byte]serverEntry)
port := cfg.Port
if port == 0 {
port = DefaultServerPort
}
lease := cfg.LeaseSeconds
if lease == 0 {
lease = 3600
}
hosts := sv.hosts
if hosts == nil {
hosts = make(map[[36]byte]serverEntry)
} else {
for k := range sv.hosts {
delete(sv.hosts, k)
for k := range hosts {
delete(hosts, k)
}
}
*sv = Server{
connID: sv.connID + 1,
siaddr: cfg.ServerAddr,
gwaddr: cfg.Gateway,
dns: cfg.DNS,
prefix: cfg.Subnet,
port: port,
leaseSeconds: lease,
nextAddr: svAddr,
hosts: hosts,
}
return nil
}
func (sv *Server) ConnectionID() *uint64 { return &sv.connID }
func (sv *Server) Protocol() uint64 { return uint64(lneto.IPProtoUDP) }
func (sv *Server) Port() uint16 { return sv.port }
func (sv *Server) LocalPort() uint16 { return sv.port }
func (sv *Server) Demux(carrierData []byte, frameOffset int) error {
isIPLayer := frameOffset >= 28
@@ -103,6 +144,9 @@ func (sv *Server) Demux(carrierData []byte, frameOffset int) error {
}
return nil
})
if err != nil {
return err
}
var clientIDRaw [36]byte
var client serverEntry
var clientExists bool
@@ -115,16 +159,17 @@ func (sv *Server) Demux(carrierData []byte, frameOffset int) error {
switch msgType {
case MsgDiscover:
if clientExists {
err = errors.New("DHCP Discover on initialized client")
break
if clientExists && (client.state == StateInit || client.state == StateRequesting) {
sv.pending-- // Cancel unfulfilled pending response.
}
if len(reqAddr) == 4 {
println("requested", reqAddr[0], reqAddr[1], reqAddr[2], reqAddr[3])
if !clientExists {
addr, ok := sv.allocAddr(reqAddr)
if !ok {
return errors.New("dhcpv4 server: address pool exhausted")
}
client.addr = addr
}
sv.nextAddr = sv.nextAddr.Next()
copy(client.requestlist[:], reqlist)
client.addr = sv.nextAddr.As4()
client.state = StateInit
client.hostname = string(hostname)
client.xid = dfrm.XID()
@@ -137,7 +182,7 @@ func (sv *Server) Demux(carrierData []byte, frameOffset int) error {
case MsgRequest:
if !clientExists {
err = errors.New("request for non existing client?")
err = errors.New("request for non existing client")
} else if dfrm.XID() != client.xid {
err = errors.New("unexpected XID for client")
} else if client.state != StateSelecting && client.state != StateRequesting {
@@ -146,11 +191,22 @@ func (sv *Server) Demux(carrierData []byte, frameOffset int) error {
if err != nil {
break
}
client.state = StateRequesting
sv.pending++
if client.state == StateSelecting {
client.state = StateRequesting
sv.pending++
}
case MsgRelease:
if clientExists {
if client.state == StateInit || client.state == StateRequesting {
sv.pending--
}
delete(sv.hosts, clientIDRaw)
return nil
}
default:
err = errors.New("unhandled message type")
err = fmt.Errorf("unhandled message type %s", msgType.String())
}
if err != nil {
return fmt.Errorf("msgtype=%s client=%+v: %w", msgType.String(), client, err)
@@ -169,7 +225,7 @@ func (sv *Server) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int)
return 0, errOptionNotFit
}
if sv.pending == 0 {
return 0, nil // No pending outgoing frames.a
return 0, nil // No pending outgoing frames.
}
var client serverEntry
@@ -205,6 +261,26 @@ func (sv *Server) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int)
n, _ = EncodeOption(optBuf[nopt:], OptRouter, sv.gwaddr[:]...)
nopt += n
}
if sv.prefix.IsValid() {
bits := uint(sv.prefix.Bits())
mask := ^uint32(0) << (32 - bits)
var maskBuf [4]byte
binary.BigEndian.PutUint32(maskBuf[:], mask)
n, _ = EncodeOption(optBuf[nopt:], OptSubnetMask, maskBuf[:]...)
nopt += n
}
if sv.dns != [4]byte{} {
n, _ = EncodeOption(optBuf[nopt:], OptDNSServers, sv.dns[:]...)
nopt += n
}
if sv.leaseSeconds > 0 {
n, _ = EncodeOption32(optBuf[nopt:], OptIPAddressLeaseTime, sv.leaseSeconds)
nopt += n
n, _ = EncodeOption32(optBuf[nopt:], OptRenewTimeValue, sv.leaseSeconds/2)
nopt += n
n, _ = EncodeOption32(optBuf[nopt:], OptRebindingTimeValue, sv.leaseSeconds*7/8)
nopt += n
}
optBuf[nopt] = byte(OptEnd)
nopt++
@@ -234,6 +310,40 @@ func (sv *Server) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int)
return OptionsOffset + nopt, nil
}
// allocAddr allocates the next available address from the pool.
// If reqAddr is a valid 4-byte address within the subnet and not already assigned,
// it is preferred. Returns false if the pool is exhausted.
func (sv *Server) allocAddr(reqAddr []byte) ([4]byte, bool) {
if len(reqAddr) == 4 {
candidate := netip.AddrFrom4([4]byte(reqAddr))
if sv.prefix.Contains(candidate) && candidate.As4() != sv.siaddr && !sv.isAddrAssigned(candidate) {
return candidate.As4(), true
}
}
sv.nextAddr = sv.nextAddr.Next()
if !sv.prefix.Contains(sv.nextAddr) {
return [4]byte{}, false
}
// Reject broadcast address (all host bits set).
a := sv.nextAddr.As4()
hostBits := uint(32 - sv.prefix.Bits())
hostMask := ^uint32(0) >> (32 - hostBits)
if binary.BigEndian.Uint32(a[:])&hostMask == hostMask {
return [4]byte{}, false
}
return a, true
}
func (sv *Server) isAddrAssigned(addr netip.Addr) bool {
a4 := addr.As4()
for _, v := range sv.hosts {
if v.addr == a4 {
return true
}
}
return false
}
func (sv *Server) getClient(clientID [36]byte) (serverEntry, bool) {
entry, ok := sv.hosts[clientID]
return entry, ok
@@ -257,4 +367,4 @@ func getSrcIPPort(ipCarrier []byte) (srcaddr []byte, port uint16, err error) {
}
port = binary.BigEndian.Uint16(ipCarrier[off:]) // TCP and UDP share same port offsets.
return srcaddr, port, nil
}
}
+358
View File
@@ -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())
}
}