fix CI and rework package structure (#111)

This commit is contained in:
Pat Whittingslow
2026-05-13 15:44:30 -03:00
committed by GitHub
parent a2970b923d
commit b95eb03aa5
17 changed files with 10 additions and 10 deletions
+400
View File
@@ -0,0 +1,400 @@
package dhcpv4
import (
"encoding/binary"
"io"
"log/slog"
"math"
"math/bits"
"net"
"net/netip"
"github.com/soypat/lneto"
"github.com/soypat/lneto/internal"
"github.com/soypat/lneto/ipv4"
)
type Client struct {
connID uint64
reqHostname string
clientID []byte
hostname []byte
dns []netip.Addr
ntps []netip.Addr
svIPtos ipv4.ToS
tRenew uint32
tRebind uint32
tIPLease uint32
currentXID uint32
state ClientState
clientMAC [6]byte
offer addr4
svip addr4 // OptServerIdentification.
siip addr4 // SIAddr.
reqIP addr4
router addr4
subnet addr4
broadcast addr4
gateway addr4
auxbuf [64]byte
}
type addr4 struct {
addr [4]byte
valid bool
}
func (a *addr4) unpack() ([4]byte, bool) {
return a.addr, a.valid
}
func (a *addr4) setmaybe(data []byte) {
if len(data) == 4 {
a.set4([4]byte(data[:]))
} else {
a.valid = false
}
}
func (a *addr4) set4(addr [4]byte) {
a.valid = true
a.addr = addr
}
type RequestConfig struct {
RequestedAddr [4]byte
ClientHardwareAddr [6]byte
// Optional hostname to request.
Hostname string
ClientID string
}
// Reset clears all DHCP state and disconnects from Stack (increments ConnectionID).
func (c *Client) Reset() {
c.reset(0)
}
func (c *Client) BeginRequest(xid uint32, cfg RequestConfig) error {
if len(cfg.Hostname) > 36 {
return lneto.ErrInvalidConfig
} else if c.state != StateInit && c.state != 0 {
return lneto.ErrInvalidConfig
} else if xid == 0 {
return lneto.ErrInvalidConfig
} else if len(cfg.ClientID) > 32 {
return lneto.ErrInvalidConfig
}
c.reset(xid)
c.state = StateInit
c.currentXID = xid
c.reqHostname = cfg.Hostname
c.reqIP = addr4{addr: cfg.RequestedAddr, valid: !internal.IsZeroed(cfg.RequestedAddr[:]...)} // TODO(pato): what's lighter? Comparing the [4]byte or ...byte
c.clientMAC = cfg.ClientHardwareAddr
if cfg.ClientID != "" {
c.clientID = append(c.clientID[:0], cfg.ClientID...)
} else {
c.clientID = append(c.clientID[:0], c.clientMAC[:]...)
}
return nil
}
func (c *Client) Protocol() uint64 { return uint64(lneto.IPProtoUDP) }
func (c *Client) LocalPort() uint16 { return DefaultClientPort }
func (c *Client) ConnectionID() *uint64 { return &c.connID }
func (c *Client) setIP(carrierFrame []byte, offsetToIP int) {
if offsetToIP < 0 {
return // No IP layer present.
}
ifrm, _ := ipv4.NewFrame(carrierFrame[offsetToIP:])
ifrm.SetID((uint16(c.currentXID) ^ uint16(c.currentXID>>16)) + uint16(c.state))
if c.state > StateInit {
// Match server ToS since some routers drop DHCP requests if no ToS set apparently?
ifrm.SetToS(c.svIPtos)
}
src := ifrm.SourceAddr()
for i := range src {
src[i] = 0
}
dst := ifrm.DestinationAddr()[:]
for i := range dst {
dst[i] = 255
}
}
func (c *Client) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int) (int, error) {
if c.isClosed() {
return 0, net.ErrClosed
} else if c.state == StateSelecting && !c.offer.valid {
return 0, nil // No offer received yet.
} else if c.state == StateBound {
return 0, nil // Done!
} else if c.state == StateRequesting {
return 0, nil // Currently awaiting ACK.
}
dst := carrierData[offsetToFrame:]
frm, err := NewFrame(dst)
if err != nil {
return 0, err
}
opts := frm.OptionsPayload()
if len(opts) < 255 {
return 0, lneto.ErrShortBuffer
}
var nextState ClientState
var numOpts int
switch c.state {
case StateInit:
// Send out discover.
n, _ := EncodeOption(opts[numOpts:], OptMessageType, byte(MsgDiscover))
numOpts += n
n, _ = EncodeOption(opts[numOpts:], OptParameterRequestList, defaultParamReqList...)
numOpts += n
maxlen := min(len(dst), math.MaxUint16)
n, _ = EncodeOption16(opts[numOpts:], OptMaximumMessageSize, uint16(maxlen))
numOpts += n
if c.reqIP.valid {
n, _ = EncodeOption(opts[numOpts:], OptRequestedIPaddress, c.reqIP.addr[:]...)
numOpts += n
}
nextState = StateSelecting
case StateSelecting:
if !c.offer.valid {
return 0, nil // Offer not yet received.
}
// Send out request, we know we've received an offer by now.
n, _ := EncodeOption(opts[numOpts:], OptMessageType, byte(MsgRequest))
numOpts += n
n, _ = EncodeOption(opts[numOpts:], OptRequestedIPaddress, c.offer.addr[:]...)
numOpts += n
n, _ = EncodeOption(opts[numOpts:], OptServerIdentification, c.svip.addr[:]...)
numOpts += n
nextState = StateRequesting
default:
internal.LogAttrs(nil, slog.LevelError, "dhcpv4:unhandled-state", slog.String("state", c.state.String()))
return 0, lneto.ErrBug
}
n, _ := EncodeOption(opts[numOpts:], OptClientIdentifier, c.clientID...)
numOpts += n
if len(c.reqHostname) > 0 {
n, err := EncodeOptionString(opts[numOpts:], OptHostName, c.reqHostname)
numOpts += n
if err != nil {
return 0, err
}
}
opts[numOpts] = byte(OptEnd)
numOpts++
c.setHeader(frm)
c.setIP(carrierData, offsetToIP)
c.state = nextState
return OptionsOffset + numOpts, nil
}
func (c *Client) Demux(carrierData []byte, frameOffset int) error {
if c.isClosed() {
return net.ErrClosed
}
pkt := carrierData[frameOffset:]
frm, err := NewFrame(pkt)
if err != nil {
return err
} else if frm.XID() != c.currentXID {
return lneto.ErrMismatch
} else if frm.MagicCookie() != MagicCookie {
return lneto.ErrInvalidField
}
msgType := c.getMessageType(frm)
if msgType == MsgNack {
return lneto.ErrPacketDrop
}
msgOK := msgType == MsgOffer || msgType == MsgAck
if !msgOK {
internal.LogAttrs(nil, slog.LevelError, "invalid DHCP message", slog.Uint64("type", uint64(msgType)))
return lneto.ErrBug
}
err = c.setOptions(frm)
if err != nil {
return err
}
switch c.state {
case StateSelecting:
if msgType == MsgOffer && !c.offer.valid {
// Lock in on this offer.
c.gateway.set4(*frm.GIAddr())
c.offer.set4(*frm.YIAddr())
c.siip.set4(*frm.SIAddr())
}
case StateRequesting:
if msgType == MsgAck {
c.state = StateBound
}
default:
internal.LogAttrs(nil, slog.LevelError, "dhcpv4:unexpected-recv-state", slog.String("state", c.state.String()))
return lneto.ErrBug
}
if frameOffset > 28 && c.svIPtos == 0 {
ifrm, _ := ipv4.NewFrame(carrierData)
c.svIPtos = ifrm.ToS()
}
return nil
}
func (c *Client) getMessageType(frm Frame) MessageType {
c.auxbuf[0] = 255
ptrMsgType := &c.auxbuf[0]
frm.ForEachOption(func(_ int, opt OptNum, data []byte) error {
if opt == OptMessageType && len(data) == 1 {
*ptrMsgType = data[0]
return io.EOF
}
return nil
})
return MessageType(*ptrMsgType)
}
func (c *Client) setOptions(frm Frame) error {
err := frm.ForEachOption(func(_ int, opt OptNum, data []byte) error {
switch opt {
case OptRenewTimeValue:
c.tRenew = maybeU32(data)
case OptIPAddressLeaseTime:
c.tIPLease = maybeU32(data)
case OptRebindingTimeValue:
c.tRebind = maybeU32(data)
case OptServerIdentification:
c.svip.setmaybe(data)
case OptRouter:
c.router.setmaybe(data)
case OptBroadcastAddress:
c.broadcast.setmaybe(data)
case OptSubnetMask:
c.subnet.setmaybe(data)
case OptHostName:
if len(data) < maxHostSize {
c.hostname = append(c.hostname[:0], data...)
}
case OptDNSServers:
if len(c.dns) > 0 || len(data)%4 != 0 {
return nil // No DNS parsing if already got in previous exchange.
}
for i := 0; i < len(data); i += 4 {
c.dns = append(c.dns, netip.AddrFrom4([4]byte(data[i:i+4])))
}
case OptNTPServersAddresses:
if len(c.ntps) > 0 || len(data)%4 != 0 {
return nil
}
for i := 0; i < len(data); i += 4 {
c.ntps = append(c.ntps, netip.AddrFrom4([4]byte(data[i:i+4])))
}
}
return nil
})
return err
}
func (c *Client) isClosed() bool { return c.state == 0 || c.currentXID == 0 }
func (c *Client) setHeader(frm Frame) {
frm.ClearHeader()
frm.SetOp(OpRequest)
frm.SetXID(c.currentXID)
frm.SetHardware(1, 6, 0)
frm.SetSecs(1)
if c.state.HasIP() {
*frm.CIAddr() = c.offer.addr
}
if c.state == StateInit {
siaddr := frm.SIAddr()[:]
for i := range siaddr {
siaddr[i] = 255
}
} else {
if !c.siip.valid {
*frm.SIAddr() = c.svip.addr
} else {
*frm.SIAddr() = c.siip.addr
}
}
*frm.YIAddr() = c.offer.addr
copy(frm.CHAddrAs6()[:], c.clientMAC[:])
frm.SetMagicCookie(MagicCookie)
}
func (c *Client) reset(xid uint32) {
*c = Client{
connID: c.connID + 1,
reqHostname: c.reqHostname,
currentXID: xid,
reqIP: c.reqIP,
clientMAC: c.clientMAC,
clientID: c.clientID,
dns: c.dns[:0],
ntps: c.ntps[:0],
hostname: c.hostname[:0],
}
}
func (d *Client) State() ClientState { return d.state }
func (d *Client) BroadcastAddr() ([4]byte, bool) { return d.broadcast.unpack() }
func (d *Client) AssignedAddr() ([4]byte, bool) { return d.offer.unpack() }
func (d *Client) ServerAddr() ([4]byte, bool) { return d.svip.unpack() }
func (d *Client) RouterAddr() ([4]byte, bool) { return d.router.unpack() }
func (d *Client) GatewayAddr() ([4]byte, bool) { return d.gateway.unpack() }
func (d *Client) Subnet() ([4]byte, bool) { return d.subnet.unpack() }
func (d *Client) RebindingSeconds() uint32 { return d.tRebind }
func (d *Client) RenewalSeconds() uint32 { return d.tRenew }
func (d *Client) IPLeaseSeconds() uint32 { return d.tIPLease }
func (d *Client) AppendDNSServers(dst []netip.Addr) []netip.Addr { return append(dst, d.dns...) }
func (d *Client) NumDNSServers() int { return len(d.dns) }
func (d *Client) DNSServerFirst() netip.Addr {
if len(d.dns) < 1 {
return netip.Addr{}
}
return d.dns[0]
}
func (d *Client) SubnetPrefix() ipv4.Prefix {
if !d.offer.valid {
return ipv4.Prefix{}
}
return ipv4.PrefixFrom(d.offer.addr, d.SubnetCIDRBits())
}
func (d *Client) SubnetCIDRBits() uint8 {
if !d.subnet.valid {
return 0
}
v := binary.BigEndian.Uint32(d.subnet.addr[:])
return 32 - uint8(bits.TrailingZeros32(v))
}
var defaultParamReqList = []byte{
byte(OptSubnetMask),
byte(OptTimeOffset),
byte(OptRouter),
byte(OptInterfaceMTUSize),
byte(OptBroadcastAddress),
byte(OptDNSServers),
byte(OptDomainName),
byte(OptNTPServersAddresses),
}
func maybeU32(b []byte) uint32 {
if len(b) != 4 {
return 0
}
return binary.BigEndian.Uint32(b)
}
+160
View File
@@ -0,0 +1,160 @@
package dhcpv4
import (
"unsafe"
"github.com/soypat/lneto"
)
//go:generate stringer -type=OptNum,Op,MessageType,ClientState -linecomment -output stringers.go
// ClientState transition table during request:
//
// StateInit -> | Send out Discover | -> StateSelecting
// StateSelecting -> |Accept Offer+Request| -> StateRequesting
// StateRequesting-> | Receive Ack | -> StateBound
type ClientState uint8
const (
_ ClientState = iota
// On clean slate boot, abort, NAK or decline enter the INIT state.
StateInit // init
// After sending out a Discover enter SELECTING.
StateSelecting // selecting
// After receiving a worthy offer and sending out request for offer enter REQUESTING.
StateRequesting // requesting
// On ACK to Request enter BOUND.
StateBound // bound
StateRenewing // renewing
StateRebinding // rebinding
StateInitReboot // init-reboot
StateRebooting // rebooting
)
// HasIP returns true if the state indicates the Client has an IP address assigned by server.
func (state ClientState) HasIP() bool {
return state == StateBound || state == StateRenewing || state == StateRebinding
}
func EncodeOptionString(dst []byte, opt OptNum, data string) (int, error) {
bdata := unsafe.Slice(unsafe.StringData(data), len(data))
return EncodeOption(dst, opt, bdata...)
}
func EncodeOption16(dst []byte, opt OptNum, v uint16) (int, error) {
// See binary.BigEndian.PutUint16()
return EncodeOption(dst, opt, byte(v>>8), byte(v))
}
func EncodeOption32(dst []byte, opt OptNum, v uint32) (int, error) {
// See binary.BigEndian.PutUint32()
return EncodeOption(dst, opt, byte(v>>24), byte(v>>16), byte(v>>8), byte(v))
}
func EncodeOption(dst []byte, opt OptNum, data ...byte) (int, error) {
if len(data) > 255 {
return 0, lneto.ErrInvalidLengthField
} else if len(dst) < 2+len(data) {
return 0, lneto.ErrShortBuffer
}
_ = dst[2+len(data)]
dst[0] = byte(opt)
dst[1] = byte(len(data))
copy(dst[2:], data)
return 2 + len(data), nil
}
type OptNum uint8
// DHCP options. Taken from https://help.sonicwall.com/help/sw/eng/6800/26/2/3/content/Network_DHCP_Server.042.12.htm.
const (
OptEnd OptNum = 255 // end options
OptWordAligned OptNum = 0 // word-aligned
OptSubnetMask OptNum = 1 // subnet mask
OptTimeOffset OptNum = 2 // Time offset in seconds from UTC
OptRouter OptNum = 3 // N/4 router addresses
OptTimeServers OptNum = 4 // N/4 time server addresses
OptNameServers OptNum = 5 // N/4 IEN-116 server addresses
OptDNSServers OptNum = 6 // N/4 DNS server addresses
OptLogServers OptNum = 7 // N/4 logging server addresses
OptCookieServers OptNum = 8 // N/4 quote server addresses
OptLPRServers OptNum = 9 // N/4 printer server addresses
OptImpressServers OptNum = 10 // N/4 impress server addresses
OptRLPServers OptNum = 11 // N/4 RLP server addresses
OptHostName OptNum = 12 // Hostname string
OptBootFileSize OptNum = 13 // Size of boot file in 512 byte chunks
OptMeritDumpFile OptNum = 14 // Client to dump and name of file to dump to
OptDomainName OptNum = 15 // The DNS domain name of the client
OptSwapServer OptNum = 16 // Swap server addresses
OptRootPath OptNum = 17 // Path name for root disk
OptExtensionFile OptNum = 18 // Patch name for more BOOTP info
OptIPLayerForwarding OptNum = 19 // Enable or disable IP forwarding
OptSrcrouteenabler OptNum = 20 // Enable or disable source routing
OptPolicyFilter OptNum = 21 // Routing policy filters
OptMaximumDGReassemblySize OptNum = 22 // Maximum datagram reassembly size
OptDefaultIPTTL OptNum = 23 // Default IP time-to-live
OptPathMTUAgingTimeout OptNum = 24 // Path MTU aging timeout
OptMTUPlateau OptNum = 25 // Path MTU plateau table
OptInterfaceMTUSize OptNum = 26 // Interface MTU size
OptAllSubnetsAreLocal OptNum = 27 // All subnets are local
OptBroadcastAddress OptNum = 28 // Broadcast address
OptPerformMaskDiscovery OptNum = 29 // Perform mask discovery
OptProvideMasktoOthers OptNum = 30 // Provide mask to others
OptPerformRouterDiscovery OptNum = 31 // Perform router discovery
OptRouterSolicitationAddress OptNum = 32 // Router solicitation address
OptStaticRoutingTable OptNum = 33 // Static routing table
OptTrailerEncapsulation OptNum = 34 // Trailer encapsulation
OptARPCacheTimeout OptNum = 35 // ARP cache timeout
OptEthernetEncapsulation OptNum = 36 // Ethernet encapsulation
OptDefaultTCPTimetoLive OptNum = 37 // Default TCP time to live
OptTCPKeepaliveInterval OptNum = 38 // TCP keepalive interval
OptTCPKeepaliveGarbage OptNum = 39 // TCP keepalive garbage
OptNISDomainName OptNum = 40 // NIS domain name
OptNISServerAddresses OptNum = 41 // NIS server addresses
OptNTPServersAddresses OptNum = 42 // NTP servers addresses
OptVendorSpecificInformation OptNum = 43 // Vendor specific information
OptNetBIOSNameServer OptNum = 44 // NetBIOS name server
OptNetBIOSDatagramDistribution OptNum = 45 // NetBIOS datagram distribution
OptNetBIOSNodeType OptNum = 46 // NetBIOS node type
OptNetBIOSScope OptNum = 47 // NetBIOS scope
OptXWindowFontServer OptNum = 48 // X window font server
OptXWindowDisplayManager OptNum = 49 // X window display manager
OptRequestedIPaddress OptNum = 50 // Requested IP address
OptIPAddressLeaseTime OptNum = 51 // IP address lease time
OptOptionOverload OptNum = 52 // Overload “sname” or “file”
OptMessageType OptNum = 53 // DHCP message type.
OptServerIdentification OptNum = 54 // DHCP server identification
OptParameterRequestList OptNum = 55 // Parameter request list
OptMessage OptNum = 56 // DHCP error message
OptMaximumMessageSize OptNum = 57 // DHCP maximum message size
OptRenewTimeValue OptNum = 58 // DHCP renewal (T1) time
OptRebindingTimeValue OptNum = 59 // DHCP rebinding (T2) time
OptClientIdentifier OptNum = 60 // Client identifier
OptClientIdentifier1 OptNum = 61 // Client identifier(1)
)
type Op byte
const (
opUndefined Op = iota // undefined
OpRequest // request
OpReply // reply
)
type MessageType uint8
const (
msg MessageType = iota // undefined
MsgDiscover // discover
MsgOffer // offer
MsgRequest // request
MsgDecline // decline
MsgAck // ack
MsgNack // nak
MsgRelease // release
MsgInform // inform
)
type Flags uint16
+524
View File
@@ -0,0 +1,524 @@
package dhcpv4
import (
"testing"
"github.com/soypat/lneto/internal"
"github.com/soypat/lneto/ipv4"
)
func TestClientServer(t *testing.T) {
svAddr := [4]byte{192, 168, 1, 1}
clAddr := svAddr
clAddr[3]++
var sv Server
var cl Client
err := cl.BeginRequest(123, RequestConfig{
RequestedAddr: clAddr,
ClientHardwareAddr: [6]byte{1, 2, 3, 4, 5, 6},
Hostname: "lneto",
})
if err != nil {
t.Fatal(err)
}
assertClState := func(state ClientState) {
t.Helper()
if state != cl.State() {
t.Errorf("want client state %s, got %s", state.String(), cl.State().String())
}
}
sv.Configure(ServerConfig{
ServerAddr: svAddr,
Subnet: ipv4.PrefixFrom(svAddr, 24),
})
// CLIENT DISCOVER.
assertClState(StateInit)
var buf [1024]byte
n, err := cl.Encapsulate(buf[:], -1, 0)
if err != nil {
t.Fatal(err)
} else if n == 0 {
t.Fatal("no client discover")
}
assertClState(StateSelecting)
err = sv.Demux(buf[:n], 0)
if err != nil {
t.Fatal(err)
}
// SERVER REPLY OFFER
n, err = sv.Encapsulate(buf[:], -1, 0)
if err != nil {
t.Fatal(err)
} else if n == 0 {
t.Fatal("no server offer")
}
err = cl.Demux(buf[:n], 0)
if err != nil {
t.Fatal(err)
}
assertClState(StateSelecting)
// CLIENT SEND OUT REQUEST.
n, err = cl.Encapsulate(buf[:], -1, 0)
if err != nil {
t.Fatal(err)
} else if n == 0 {
t.Fatal("no client request")
}
assertClState(StateRequesting)
err = sv.Demux(buf[:n], 0)
if err != nil {
t.Fatal(err)
}
// SERVER REPLIES WITH ACK.
n, err = sv.Encapsulate(buf[:], -1, 0)
if err != nil {
t.Fatal(err)
} else if n == 0 {
t.Fatal("no server reply")
}
err = cl.Demux(buf[:n], 0)
if err != nil {
t.Fatal(err)
}
assertClState(StateBound)
}
func TestExample(t *testing.T) {
const (
xid = 1
offerLease = 9001
)
var cl Client
clientHwaddr := [6]byte{0, 0, 0, 0, 0, 1}
clientReqAddr := [4]byte{192, 168, 1, 2}
clientHostname := "client"
serverIP := [4]byte{192, 168, 1, 1}
subnetMask := [4]byte{255, 255, 255, 0}
routerAddr := [4]byte{192, 168, 1, 0}
dnsAddr := [4]byte{192, 168, 1, 255}
cl.BeginRequest(xid, RequestConfig{
RequestedAddr: clientReqAddr,
ClientHardwareAddr: clientHwaddr,
Hostname: clientHostname,
})
buf := make([]byte, 2048)
buf2 := make([]byte, len(buf))
n, err := cl.Encapsulate(buf, -1, 0)
if err != nil {
t.Fatal(err)
} else if n <= 0 {
t.Fatal("no data sent out by client after starting request")
}
n, err = cl.Encapsulate(buf2, -1, 0)
if err != nil {
t.Error("client encaps double tap after discover:", err)
}
// Fabricate server OFFER response.
dfrm, _ := NewFrame(buf)
dfrm.ClearHeader()
dfrm.SetOp(OpReply)
dfrm.SetHardware(1, 6, 0)
dfrm.SetFlags(0)
dfrm.SetXID(xid)
dfrm.SetSecs(1)
*dfrm.YIAddr() = clientReqAddr
copy(dfrm.CHAddr()[:], clientHwaddr[:])
dfrm.SetMagicCookie(MagicCookie)
ntot := 0
nopt, _ := EncodeOption(buf[OptionsOffset+ntot:], OptMessageType, byte(MsgOffer))
ntot += nopt
nopt, _ = EncodeOption(buf[OptionsOffset+ntot:], OptServerIdentification, serverIP[:]...)
ntot += nopt
nopt, _ = EncodeOption32(buf[OptionsOffset+ntot:], OptServerIdentification, offerLease)
ntot += nopt
nopt, _ = EncodeOption(buf[OptionsOffset+ntot:], OptSubnetMask, subnetMask[:]...)
ntot += nopt
nopt, _ = EncodeOption(buf[OptionsOffset+ntot:], OptRouter, routerAddr[:]...)
ntot += nopt
nopt, _ = EncodeOption(buf[OptionsOffset+ntot:], OptDNSServers, dnsAddr[:]...)
ntot += nopt
nopt, _ = EncodeOption(buf[OptionsOffset+ntot:], OptEnd, dnsAddr[:]...)
ntot += nopt
err = cl.Demux(buf[:OptionsOffset+ntot], 0)
if err != nil {
t.Fatal(err)
}
n, err = cl.Encapsulate(buf[:], -1, 0)
if err != nil {
t.Fatal(err)
} else if n <= 0 {
t.Fatal("no data written from client in response to offer")
}
n, err = cl.Encapsulate(buf[:], -1, 0)
if err != nil {
t.Error("encapsulate double tap after request:", err)
} else if n > 0 {
t.Error("encapsulate double tap got data!", n)
}
}
// TestRequestedIPAddressOption verifies that when a client has a valid requested IP,
// the OptRequestedIPaddress option is included in the DISCOVER message.
// This tests for the bug where the condition was inverted (!c.reqIP.valid instead of c.reqIP.valid).
func TestRequestedIPAddressOption(t *testing.T) {
var cl Client
requestedAddr := [4]byte{192, 168, 1, 100}
err := cl.BeginRequest(12345, RequestConfig{
RequestedAddr: requestedAddr,
ClientHardwareAddr: [6]byte{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff},
})
if err != nil {
t.Fatal(err)
}
buf := make([]byte, 1024)
n, err := cl.Encapsulate(buf, -1, 0)
if err != nil {
t.Fatal(err)
}
if n == 0 {
t.Fatal("no data encapsulated")
}
// Parse the frame and look for OptRequestedIPaddress
frm, err := NewFrame(buf[:n])
if err != nil {
t.Fatal(err)
}
var foundRequestedIP bool
var foundIPValue [4]byte
err = frm.ForEachOption(func(_ int, opt OptNum, data []byte) error {
if opt == OptRequestedIPaddress {
foundRequestedIP = true
if len(data) == 4 {
copy(foundIPValue[:], data)
}
}
return nil
})
if err != nil {
t.Fatal(err)
}
if !foundRequestedIP {
t.Error("OptRequestedIPaddress not found in DISCOVER message when reqIP.valid is true")
} else if foundIPValue != requestedAddr {
t.Errorf("OptRequestedIPaddress has wrong value: got %v, want %v", foundIPValue, requestedAddr)
}
}
// TestForEachOptionBoundsCheck verifies that ForEachOption properly validates
// buffer bounds and doesn't panic on malformed options with lengths that extend
// past the buffer end.
func TestForEachOptionBoundsCheck(t *testing.T) {
// Create a minimal valid frame buffer
buf := make([]byte, OptionsOffset+10)
frm, err := NewFrame(buf)
if err != nil {
t.Fatal(err)
}
frm.SetMagicCookie(MagicCookie)
testCases := []struct {
name string
options []byte
wantErr bool
}{
{
name: "valid option",
options: []byte{byte(OptHostName), 4, 't', 'e', 's', 't', byte(OptEnd)},
wantErr: false,
},
{
name: "option length exceeds buffer",
options: []byte{byte(OptHostName), 100, 't', 'e', 's', 't'}, // claims 100 bytes but only 4 available
wantErr: true,
},
{
name: "option length exactly at buffer end",
options: []byte{byte(OptHostName), 255}, // claims 255 bytes, way past end
wantErr: true,
},
{
name: "option length causes ptr+2+optlen overflow",
options: []byte{byte(OptHostName), 8, 'a', 'b', 'c'}, // claims 8 bytes but only 3 available
wantErr: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Create fresh buffer for each test
testBuf := make([]byte, OptionsOffset+len(tc.options))
testFrm, _ := NewFrame(testBuf)
testFrm.SetMagicCookie(MagicCookie)
copy(testBuf[OptionsOffset:], tc.options)
// Use recover to catch panics
var panicked bool
var gotErr error
func() {
defer func() {
if r := recover(); r != nil {
panicked = true
}
}()
gotErr = testFrm.ForEachOption(func(_ int, opt OptNum, data []byte) error {
// Access the data to trigger potential panic
_ = len(data)
if len(data) > 0 {
_ = data[0]
}
return nil
})
}()
if panicked {
t.Errorf("ForEachOption panicked on malformed input %q", tc.name)
}
if tc.wantErr && gotErr == nil {
t.Errorf("ForEachOption should return error for %q, got nil", tc.name)
}
if !tc.wantErr && gotErr != nil {
t.Errorf("ForEachOption should not return error for %q, got %v", tc.name, gotErr)
}
})
}
}
// TestGetMessageTypeChecksOptNum verifies that getMessageType correctly identifies
// the DHCP message type by checking for OptMessageType specifically, not just
// any single-byte option.
func TestGetMessageTypeChecksOptNum(t *testing.T) {
var cl Client
err := cl.BeginRequest(1, RequestConfig{
ClientHardwareAddr: [6]byte{1, 2, 3, 4, 5, 6},
})
if err != nil {
t.Fatal(err)
}
// Create a frame with a single-byte option BEFORE OptMessageType
buf := make([]byte, 512)
frm, _ := NewFrame(buf)
frm.SetMagicCookie(MagicCookie)
frm.SetXID(1)
opts := buf[OptionsOffset:]
n := 0
// Add a single-byte option that is NOT OptMessageType first
// OptOptionOverload (52) can be a single byte value
opts[n] = byte(OptOptionOverload)
opts[n+1] = 1
opts[n+2] = 3 // value 3 means both sname and file contain options
n += 3
// Now add the actual message type
opts[n] = byte(OptMessageType)
opts[n+1] = 1
opts[n+2] = byte(MsgOffer)
n += 3
opts[n] = byte(OptEnd)
// getMessageType should return MsgOffer, not MessageType(3)
msgType := cl.getMessageType(frm)
// If the bug exists (not checking opt == OptMessageType), it will return
// MessageType(3) which is MsgRequest, not MsgOffer
if msgType != MsgOffer {
t.Errorf("getMessageType returned %v (%d), want MsgOffer (%d); "+
"likely not checking for OptMessageType specifically",
msgType, msgType, MsgOffer)
}
}
// TestGetMessageTypeWithMultipleSingleByteOptions tests that getMessageType
// returns the correct message type even when multiple single-byte options exist.
func TestGetMessageTypeWithMultipleSingleByteOptions(t *testing.T) {
var cl Client
err := cl.BeginRequest(42, RequestConfig{
ClientHardwareAddr: [6]byte{1, 2, 3, 4, 5, 6},
})
if err != nil {
t.Fatal(err)
}
testCases := []struct {
name string
buildOptions func([]byte) int
expectedMsgType MessageType
}{
{
name: "message type first",
buildOptions: func(opts []byte) int {
n := 0
n += writeOption(opts[n:], OptMessageType, byte(MsgAck))
n += writeOption(opts[n:], OptOptionOverload, byte(1))
opts[n] = byte(OptEnd)
return n + 1
},
expectedMsgType: MsgAck,
},
{
name: "message type after other single-byte option",
buildOptions: func(opts []byte) int {
n := 0
n += writeOption(opts[n:], OptOptionOverload, byte(2))
n += writeOption(opts[n:], OptMessageType, byte(MsgNack))
opts[n] = byte(OptEnd)
return n + 1
},
expectedMsgType: MsgNack,
},
{
name: "message type between multi-byte options",
buildOptions: func(opts []byte) int {
n := 0
n += writeOption(opts[n:], OptHostName, 't', 'e', 's', 't')
n += writeOption(opts[n:], OptMessageType, byte(MsgDiscover))
n += writeOption(opts[n:], OptRouter, 192, 168, 1, 1)
opts[n] = byte(OptEnd)
return n + 1
},
expectedMsgType: MsgDiscover,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
buf := make([]byte, 512)
frm, _ := NewFrame(buf)
frm.SetMagicCookie(MagicCookie)
frm.SetXID(42)
opts := buf[OptionsOffset:]
tc.buildOptions(opts)
msgType := cl.getMessageType(frm)
if msgType != tc.expectedMsgType {
t.Errorf("got message type %v (%d), want %v (%d)",
msgType, msgType, tc.expectedMsgType, tc.expectedMsgType)
}
})
}
}
// writeOption is a test helper that writes a DHCP option and returns bytes written.
func writeOption(dst []byte, opt OptNum, data ...byte) int {
dst[0] = byte(opt)
dst[1] = byte(len(data))
copy(dst[2:], data)
return 2 + len(data)
}
// TestForEachOptionEdgeCases tests additional edge cases for bounds checking.
func TestForEachOptionEdgeCases(t *testing.T) {
t.Run("empty options section", func(t *testing.T) {
buf := make([]byte, OptionsOffset)
frm, _ := NewFrame(buf)
frm.SetMagicCookie(MagicCookie)
err := frm.ForEachOption(func(_ int, opt OptNum, data []byte) error {
return nil
})
// Should return errNoOptions for empty options
if err == nil {
t.Error("expected error for empty options section")
}
})
t.Run("only end option", func(t *testing.T) {
buf := make([]byte, OptionsOffset+1)
frm, _ := NewFrame(buf)
frm.SetMagicCookie(MagicCookie)
buf[OptionsOffset] = byte(OptEnd)
var called bool
err := frm.ForEachOption(func(_ int, opt OptNum, data []byte) error {
called = true
return nil
})
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if called {
t.Error("callback should not be called for OptEnd")
}
})
t.Run("truncated option header", func(t *testing.T) {
// Buffer has option type but no length byte
buf := make([]byte, OptionsOffset+1)
frm, _ := NewFrame(buf)
frm.SetMagicCookie(MagicCookie)
buf[OptionsOffset] = byte(OptHostName) // Not OptEnd, so it needs a length
var panicked bool
func() {
defer func() {
if r := recover(); r != nil {
panicked = true
}
}()
frm.ForEachOption(func(_ int, opt OptNum, data []byte) error {
return nil
})
}()
if panicked {
t.Error("ForEachOption panicked on truncated option header")
}
})
}
// TestRequestedIPNotSentWhenInvalid verifies that OptRequestedIPaddress is NOT
// sent when reqIP is not valid (zero address with valid=false).
func TestRequestedIPNotSentWhenInvalid(t *testing.T) {
var cl Client
// Begin request with zero address - this still sets valid=true in current impl
err := cl.BeginRequest(99999, RequestConfig{
RequestedAddr: [4]byte{0, 0, 0, 0}, // Zero but will be marked valid
ClientHardwareAddr: [6]byte{1, 2, 3, 4, 5, 6},
})
if err != nil {
t.Fatal(err)
}
buf := make([]byte, 1024)
n, err := cl.Encapsulate(buf, -1, 0)
if err != nil {
t.Fatal(err)
}
frm, _ := NewFrame(buf[:n])
var foundRequestedIP bool
var ipValue [4]byte
frm.ForEachOption(func(_ int, opt OptNum, data []byte) error {
if opt == OptRequestedIPaddress {
foundRequestedIP = true
if len(data) == 4 {
copy(ipValue[:], data)
}
}
return nil
})
// With the bug fixed, when a valid requested IP is set (even 0.0.0.0),
// it should be included. This test documents expected behavior.
if foundRequestedIP {
// Verify the value matches what was requested
if !internal.BytesEqual(ipValue[:], []byte{0, 0, 0, 0}) {
t.Errorf("unexpected requested IP value: %v", ipValue)
}
}
}
+165
View File
@@ -0,0 +1,165 @@
package dhcpv4
import (
"encoding/binary"
"github.com/soypat/lneto"
)
const (
maxHostSize = 16 // max size for hostname.
sizeSName = 64 // Server name, part of BOOTP too.
sizeBootFile = 128 // Boot file name, Legacy.
sizeHeader = 44
// Magic Cookie offset measured from the start of the UDP payload.
magicCookieOffset = sizeHeader + sizeSName + sizeBootFile
// Expected Magic Cookie value.
MagicCookie uint32 = 0x63825363
// DHCP Options offset measured from the start of the UDP payload.
OptionsOffset = magicCookieOffset + 4
DefaultClientPort = 68
DefaultServerPort = 67
)
// NewFrame returns a new DHCPv4 Frame with data set to buf.
// An error is returned if the buffer size is smaller than 240.
func NewFrame(buf []byte) (Frame, error) {
if len(buf) < OptionsOffset {
return Frame{}, lneto.ErrTruncatedFrame
}
return Frame{buf: buf}, nil
}
func PayloadIsDHCPv4(payload []byte) bool {
return len(payload) >= OptionsOffset && binary.BigEndian.Uint32(payload[magicCookieOffset:]) == MagicCookie
}
// Frame encapsulates the raw data of a DHCP packet
// and provides methods for manipulating, validating and
// retrieving fields and payload data. See [RFC2131].
//
// [RFC2131]: https://tools.ietf.org/html/rfc2131
type Frame struct {
buf []byte
}
// OptionsPayload returns the options portion of the DHCP frame. May be zero lengthed.
func (frm Frame) OptionsPayload() []byte {
return frm.buf[OptionsOffset:]
}
func (frm Frame) Op() Op { return Op(frm.buf[0]) }
func (frm Frame) SetOp(op Op) { frm.buf[0] = byte(op) }
func (frm Frame) Hardware() (Type, Len, Ops uint8) {
return frm.buf[1], frm.buf[2], frm.buf[3]
}
func (frm Frame) SetHardware(Type, Len, Ops uint8) {
frm.buf[1], frm.buf[2], frm.buf[3] = Type, Len, Ops
}
// XID is the transaction ID. Is unique and constant for a DHCP request/response exchange of packets.
func (frm Frame) XID() uint32 { return binary.BigEndian.Uint32(frm.buf[4:8]) }
func (frm Frame) SetXID(xid uint32) { binary.BigEndian.PutUint32(frm.buf[4:8], xid) }
// Secs is seconds elapsed.
func (frm Frame) Secs() uint16 { return binary.BigEndian.Uint16(frm.buf[8:10]) }
func (frm Frame) SetSecs(secs uint16) { binary.BigEndian.PutUint16(frm.buf[8:10], secs) }
func (frm Frame) Flags() Flags { return Flags(binary.BigEndian.Uint16(frm.buf[10:12])) }
func (frm Frame) SetFlags(flags Flags) { binary.BigEndian.PutUint16(frm.buf[10:12], uint16(flags)) }
// CIAddr is the client IP address. If the client has not obtained an IP
// address yet, this field is set to 0.
func (frm Frame) CIAddr() *[4]byte {
return (*[4]byte)(frm.buf[12:16])
}
// YIAddr is the IP address offered by the server to the client. Your (client) IP Address.
func (frm Frame) YIAddr() *[4]byte {
return (*[4]byte)(frm.buf[16:20])
}
// SIAddr is the IP address of the next server to use in bootstrap. This
// field is used in DHCPOFFER and DHCPACK messages.
func (frm Frame) SIAddr() *[4]byte {
return (*[4]byte)(frm.buf[20:24])
}
// GIAddr is the gateway IP address. Is also known as the Relay Agent IP Address.
func (frm Frame) GIAddr() *[4]byte {
return (*[4]byte)(frm.buf[24:28])
}
// CHAddrAs6 returns [Frame.CHAddr] but limited to first 6 bytes.
func (frm Frame) CHAddrAs6() *[6]byte {
return (*[6]byte)(frm.buf[28 : 28+6])
}
// CHAddr is the client hardware address. Can be up to 16 bytes in length but
// is usually 6 bytes for Ethernet.
func (frm Frame) CHAddr() *[16]byte {
return (*[16]byte)(frm.buf[28:44])
}
// MagicCookie returns the magic cookie of the header. Expect this to always be [MagicCookie].
func (frm Frame) MagicCookie() uint32 { return binary.BigEndian.Uint32(frm.buf[magicCookieOffset:]) }
// SetMagicCookie sets the MagicCookie. Call this with [MagicCookie] to create a valid DHCP header.
func (frm Frame) SetMagicCookie(cookie uint32) {
binary.BigEndian.PutUint32(frm.buf[magicCookieOffset:], cookie)
}
// ClearHeader zeros out the header contents.
func (frm Frame) ClearHeader() {
for i := range frm.buf[:OptionsOffset] {
frm.buf[i] = 0
}
}
// ForEachOption iterates over all DHCPv4 options returning an error on a malformed option or when user provided callback returns an error.
// If the user provided callback is nil then only option buffer validation is performed.
func (frm Frame) ForEachOption(fn func(off int, opt OptNum, data []byte) error) error {
// Parse DHCP options.
ptr := OptionsOffset
if ptr > len(frm.buf) {
return lneto.ErrTruncatedFrame
} else if len(frm.buf[ptr:]) == 0 {
return lneto.ErrInvalidField
}
callback := fn != nil
for ptr+1 < len(frm.buf) {
optnum := OptNum(frm.buf[ptr])
if optnum == 0xff {
break
} else if optnum == OptWordAligned {
ptr++
continue
}
optlen := int(frm.buf[ptr+1])
if ptr+2+optlen > len(frm.buf) {
return lneto.ErrInvalidLengthField
}
if callback {
optionData := frm.buf[ptr+2 : ptr+2+optlen]
if err := fn(ptr, optnum, optionData); err != nil {
return err
}
}
ptr += optlen + 2
}
return nil
}
//
// Validation API.
//
func (frm Frame) ValidateSize(vld *lneto.Validator) {
err := frm.ForEachOption(nil) // Does all necessary validation.
if err != nil {
vld.AddError(lneto.ErrInvalidLengthField)
}
}
+370
View File
@@ -0,0 +1,370 @@
package dhcpv4
import (
"encoding/binary"
"errors"
"fmt"
"github.com/soypat/lneto"
"github.com/soypat/lneto/internal"
"github.com/soypat/lneto/ipv4"
)
var errOptionNotFit = errors.New("DHCPv4: options dont fit")
type Server struct {
connID uint64
nextAddr [4]byte
subnet ipv4.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 ipv4.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 {
hostname string
xid uint32
port uint16
addr [4]byte
requestlist [10]byte
hwaddr [6]byte
clientIdlen uint8
// Possible states:
// - 0: No entry/uninitialized
// - Init: Server received discover, pending Offer sent out.
// - Selecting: Server sent out offer, request not received.
// - Requesting: Request received, pending Ack sent out.
// - Bound: Ack sent out, no more pending data to be sent.
state ClientState
}
// 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 {
if !cfg.Subnet.IsValid() {
return errors.New("dhcpv4 server: invalid subnet")
} else if !cfg.Subnet.Contains(cfg.ServerAddr) {
return errors.New("dhcpv4 server: server address outside subnet")
}
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 hosts {
delete(hosts, k)
}
}
*sv = Server{
connID: sv.connID + 1,
siaddr: cfg.ServerAddr,
gwaddr: cfg.Gateway,
dns: cfg.DNS,
subnet: cfg.Subnet,
port: port,
leaseSeconds: lease,
nextAddr: cfg.Subnet.Next(cfg.ServerAddr),
hosts: hosts,
}
return nil
}
func (sv *Server) ConnectionID() *uint64 { return &sv.connID }
func (sv *Server) Protocol() uint64 { return uint64(lneto.IPProtoUDP) }
func (sv *Server) LocalPort() uint16 { return sv.port }
func (sv *Server) Demux(carrierData []byte, frameOffset int) error {
isIPLayer := frameOffset >= 28
dhcpData := carrierData[frameOffset:]
dfrm, err := NewFrame(dhcpData)
if err != nil {
return err
}
dfrm.ValidateSize(&sv.vld)
if sv.vld.HasError() {
return sv.vld.ErrPop()
}
var msgType MessageType
var clientID []byte
var reqlist []byte
var reqAddr []byte
var hostname []byte
err = dfrm.ForEachOption(func(off int, op OptNum, data []byte) error {
switch op {
case OptMessageType:
if len(data) == 1 {
msgType = MessageType(data[0])
}
case OptHostName:
if len(data) <= 36 {
hostname = data
}
case OptClientIdentifier:
if len(data) <= 36 {
clientID = data
}
case OptParameterRequestList:
if len(data) > 36 {
return errors.New("too many request options")
}
reqlist = data
case OptRequestedIPaddress:
if len(data) == 4 {
reqAddr = data
}
}
return nil
})
if err != nil {
return err
}
var clientIDRaw [36]byte
var client serverEntry
var clientExists bool
if len(clientID) == 0 {
client, clientIDRaw, clientExists = sv.getClientByIP(*dfrm.CIAddr())
} else {
copy(clientIDRaw[:], clientID)
client, clientExists = sv.getClient(clientIDRaw)
}
switch msgType {
case MsgDiscover:
if clientExists && (client.state == StateInit || client.state == StateRequesting) {
sv.pending-- // Cancel unfulfilled pending response.
}
if !clientExists {
addr, ok := sv.allocAddr(reqAddr)
if !ok {
return errors.New("dhcpv4 server: address pool exhausted")
}
client.addr = addr
}
copy(client.requestlist[:], reqlist)
client.state = StateInit
client.hostname = string(hostname)
client.xid = dfrm.XID()
client.hwaddr = *dfrm.CHAddrAs6()
if isIPLayer {
_, client.port, _ = getSrcIPPort(carrierData)
}
client.clientIdlen = uint8(len(clientID))
sv.pending++
case MsgRequest:
if !clientExists {
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 {
err = errors.New("DHCP request unexpected state")
}
if err != nil {
break
}
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 = fmt.Errorf("unhandled message type %s", msgType.String())
}
if err != nil {
return fmt.Errorf("msgtype=%s client=%+v: %w", msgType.String(), client, err)
}
sv.hosts[clientIDRaw] = client
return nil
}
func (sv *Server) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int) (int, error) {
carrierIsIP := offsetToIP >= 0
dfrm, err := NewFrame(carrierData[offsetToFrame:])
optBuf := dfrm.OptionsPayload()[:]
if err != nil {
return 0, err
} else if len(optBuf) < 255 {
return 0, errOptionNotFit
}
if sv.pending == 0 {
return 0, nil // No pending outgoing frames.
}
var client serverEntry
var clientID [36]byte
for k, v := range sv.hosts {
pending := v.state == StateInit || v.state == StateRequesting
if pending {
client = v
clientID = k
break
}
}
if client.state == 0 {
return 0, nil // Nothing to do.
}
futureState := ClientState(0)
var nopt int
switch client.state {
case StateInit:
futureState = StateSelecting
nopt, err = EncodeOption(optBuf[nopt:], OptMessageType, byte(MsgOffer))
case StateRequesting:
futureState = StateBound
nopt, err = EncodeOption(optBuf[nopt:], OptMessageType, byte(MsgAck))
*dfrm.CIAddr() = client.addr
}
if err != nil {
return 0, err
}
n, _ := EncodeOption(optBuf[nopt:], OptServerIdentification, sv.siaddr[:]...)
nopt += n
if sv.gwaddr != [4]byte{} {
n, _ = EncodeOption(optBuf[nopt:], OptRouter, sv.gwaddr[:]...)
nopt += n
}
if sv.subnet.IsValid() {
bits := uint(sv.subnet.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++
dfrm.ClearHeader()
dfrm.SetOp(OpReply)
dfrm.SetHardware(1, 6, 0)
dfrm.SetXID(client.xid)
dfrm.SetSecs(0)
dfrm.SetFlags(0)
*dfrm.YIAddr() = client.addr // Offer here.
*dfrm.SIAddr() = sv.siaddr
*dfrm.GIAddr() = sv.gwaddr
copy(dfrm.CHAddrAs6()[:], client.hwaddr[:])
dfrm.SetMagicCookie(MagicCookie)
if carrierIsIP {
err = internal.SetIPAddrs(carrierData[offsetToIP:], 0, sv.siaddr[:], client.addr[:])
if err != nil {
return 0, err
}
}
client.state = futureState
// Set server state.
sv.hosts[clientID] = client
sv.pending--
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 := [4]byte(reqAddr)
if sv.subnet.Contains(candidate) && candidate != sv.siaddr && !sv.isAddrAssigned(candidate) {
return candidate, true
}
}
// Reject broadcast address (all host bits set).
a := sv.nextAddr
sv.nextAddr = sv.subnet.Next(sv.nextAddr)
if sv.nextAddr == sv.siaddr {
sv.nextAddr = sv.subnet.Next(sv.nextAddr)
}
hostBits := uint(32 - sv.subnet.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 [4]byte) bool {
for _, v := range sv.hosts {
if v.addr == addr {
return true
}
}
return false
}
func (sv *Server) getClient(clientID [36]byte) (serverEntry, bool) {
entry, ok := sv.hosts[clientID]
return entry, ok
}
func (sv *Server) getClientByIP(ip [4]byte) (serverEntry, [36]byte, bool) {
for k, v := range sv.hosts {
if v.addr == ip {
return v, k, true
}
}
return serverEntry{}, [36]byte{}, false
}
func getSrcIPPort(ipCarrier []byte) (srcaddr []byte, port uint16, err error) {
srcaddr, _, _, off, err := internal.GetIPAddr(ipCarrier)
if err != nil {
return srcaddr, port, err
} else if len(ipCarrier[off:]) < 2 {
return srcaddr, port, errors.New("getSrcIPPort got only IP layer")
}
port = binary.BigEndian.Uint16(ipCarrier[off:]) // TCP and UDP share same port offsets.
return srcaddr, port, nil
}
+359
View File
@@ -0,0 +1,359 @@
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())
}
}
+163
View File
@@ -0,0 +1,163 @@
// Code generated by "stringer -type=OptNum,Op,MessageType,ClientState -linecomment -output stringers.go"; DO NOT EDIT.
package dhcpv4
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[OptEnd-255]
_ = x[OptWordAligned-0]
_ = x[OptSubnetMask-1]
_ = x[OptTimeOffset-2]
_ = x[OptRouter-3]
_ = x[OptTimeServers-4]
_ = x[OptNameServers-5]
_ = x[OptDNSServers-6]
_ = x[OptLogServers-7]
_ = x[OptCookieServers-8]
_ = x[OptLPRServers-9]
_ = x[OptImpressServers-10]
_ = x[OptRLPServers-11]
_ = x[OptHostName-12]
_ = x[OptBootFileSize-13]
_ = x[OptMeritDumpFile-14]
_ = x[OptDomainName-15]
_ = x[OptSwapServer-16]
_ = x[OptRootPath-17]
_ = x[OptExtensionFile-18]
_ = x[OptIPLayerForwarding-19]
_ = x[OptSrcrouteenabler-20]
_ = x[OptPolicyFilter-21]
_ = x[OptMaximumDGReassemblySize-22]
_ = x[OptDefaultIPTTL-23]
_ = x[OptPathMTUAgingTimeout-24]
_ = x[OptMTUPlateau-25]
_ = x[OptInterfaceMTUSize-26]
_ = x[OptAllSubnetsAreLocal-27]
_ = x[OptBroadcastAddress-28]
_ = x[OptPerformMaskDiscovery-29]
_ = x[OptProvideMasktoOthers-30]
_ = x[OptPerformRouterDiscovery-31]
_ = x[OptRouterSolicitationAddress-32]
_ = x[OptStaticRoutingTable-33]
_ = x[OptTrailerEncapsulation-34]
_ = x[OptARPCacheTimeout-35]
_ = x[OptEthernetEncapsulation-36]
_ = x[OptDefaultTCPTimetoLive-37]
_ = x[OptTCPKeepaliveInterval-38]
_ = x[OptTCPKeepaliveGarbage-39]
_ = x[OptNISDomainName-40]
_ = x[OptNISServerAddresses-41]
_ = x[OptNTPServersAddresses-42]
_ = x[OptVendorSpecificInformation-43]
_ = x[OptNetBIOSNameServer-44]
_ = x[OptNetBIOSDatagramDistribution-45]
_ = x[OptNetBIOSNodeType-46]
_ = x[OptNetBIOSScope-47]
_ = x[OptXWindowFontServer-48]
_ = x[OptXWindowDisplayManager-49]
_ = x[OptRequestedIPaddress-50]
_ = x[OptIPAddressLeaseTime-51]
_ = x[OptOptionOverload-52]
_ = x[OptMessageType-53]
_ = x[OptServerIdentification-54]
_ = x[OptParameterRequestList-55]
_ = x[OptMessage-56]
_ = x[OptMaximumMessageSize-57]
_ = x[OptRenewTimeValue-58]
_ = x[OptRebindingTimeValue-59]
_ = x[OptClientIdentifier-60]
_ = x[OptClientIdentifier1-61]
}
const (
_OptNum_name_0 = "word-alignedsubnet maskTime offset in seconds from UTCN/4 router addressesN/4 time server addressesN/4 IEN-116 server addressesN/4 DNS server addressesN/4 logging server addressesN/4 quote server addressesN/4 printer server addressesN/4 impress server addressesN/4 RLP server addressesHostname stringSize of boot file in 512 byte chunksClient to dump and name of file to dump toThe DNS domain name of the clientSwap server addressesPath name for root diskPatch name for more BOOTP infoEnable or disable IP forwardingEnable or disable source routingRouting policy filtersMaximum datagram reassembly sizeDefault IP time-to-livePath MTU aging timeoutPath MTU plateau tableInterface MTU sizeAll subnets are localBroadcast addressPerform mask discoveryProvide mask to othersPerform router discoveryRouter solicitation addressStatic routing tableTrailer encapsulationARP cache timeoutEthernet encapsulationDefault TCP time to liveTCP keepalive intervalTCP keepalive garbageNIS domain nameNIS server addressesNTP servers addressesVendor specific informationNetBIOS name serverNetBIOS datagram distributionNetBIOS node typeNetBIOS scopeX window font serverX window display managerRequested IP addressIP address lease timeOverload “sname” or “file”DHCP message type.DHCP server identificationParameter request listDHCP error messageDHCP maximum message sizeDHCP renewal (T1) timeDHCP rebinding (T2) timeClient identifierClient identifier(1)"
_OptNum_name_1 = "end options"
)
var (
_OptNum_index_0 = [...]uint16{0, 12, 23, 54, 74, 99, 127, 151, 179, 205, 233, 261, 285, 300, 336, 378, 411, 432, 455, 485, 516, 548, 570, 602, 625, 647, 669, 687, 708, 725, 747, 769, 793, 820, 840, 861, 878, 900, 924, 946, 967, 982, 1002, 1023, 1050, 1069, 1098, 1115, 1128, 1148, 1172, 1192, 1213, 1247, 1265, 1291, 1313, 1331, 1356, 1378, 1402, 1419, 1439}
)
func (i OptNum) String() string {
switch {
case i <= 61:
return _OptNum_name_0[_OptNum_index_0[i]:_OptNum_index_0[i+1]]
case i == 255:
return _OptNum_name_1
default:
return "OptNum(" + strconv.FormatInt(int64(i), 10) + ")"
}
}
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[opUndefined-0]
_ = x[OpRequest-1]
_ = x[OpReply-2]
}
const _Op_name = "undefinedrequestreply"
var _Op_index = [...]uint8{0, 9, 16, 21}
func (i Op) String() string {
if i >= Op(len(_Op_index)-1) {
return "Op(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _Op_name[_Op_index[i]:_Op_index[i+1]]
}
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[msg-0]
_ = x[MsgDiscover-1]
_ = x[MsgOffer-2]
_ = x[MsgRequest-3]
_ = x[MsgDecline-4]
_ = x[MsgAck-5]
_ = x[MsgNack-6]
_ = x[MsgRelease-7]
_ = x[MsgInform-8]
}
const _MessageType_name = "undefineddiscoverofferrequestdeclineacknakreleaseinform"
var _MessageType_index = [...]uint8{0, 9, 17, 22, 29, 36, 39, 42, 49, 55}
func (i MessageType) String() string {
if i >= MessageType(len(_MessageType_index)-1) {
return "MessageType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _MessageType_name[_MessageType_index[i]:_MessageType_index[i+1]]
}
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[StateInit-1]
_ = x[StateSelecting-2]
_ = x[StateRequesting-3]
_ = x[StateBound-4]
_ = x[StateRenewing-5]
_ = x[StateRebinding-6]
_ = x[StateInitReboot-7]
_ = x[StateRebooting-8]
}
const _ClientState_name = "initselectingrequestingboundrenewingrebindinginit-rebootrebooting"
var _ClientState_index = [...]uint8{0, 4, 13, 23, 28, 36, 45, 56, 65}
func (i ClientState) String() string {
i -= 1
if i >= ClientState(len(_ClientState_index)-1) {
return "ClientState(" + strconv.FormatInt(int64(i+1), 10) + ")"
}
return _ClientState_name[_ClientState_index[i]:_ClientState_index[i+1]]
}