mirror of
https://github.com/soypat/lneto.git
synced 2026-08-20 06:29:03 +00:00
fix CI and rework package structure (#111)
This commit is contained in:
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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]]
|
||||
}
|
||||
Reference in New Issue
Block a user