Files
lneto/dhcp/dhcpv4/server.go
T
Ron Evans fa609ea54f fix: two fixes for dhcp/ap (#136)
* feature: ipv4 broadcast support

* stackip4 method receiver varname

* Incorporate strict test timing for TestStackGoTCPDialRetriesPendingControl (#135)

* rewrite tinygo failing test to be more real-time

* use channels to scheduler stack

* fix flakiness by increasing timeout and move implementation to top of file

* fix(dhcp): use chaddr as lookup key and patch Ethernet dst on Offer/Ack

- Client lookup in Demux() now keys on chaddr when no OptClientIdentifier
  is present. It previously used ciaddr which is 0.0.0.0 during initial lease
  acquisition, causing MsgRequest to fail to match any client (RFC 2131 §4.3.1)
- In Encapsulate(), overwrite Ethernet dst with client.hwaddr when packet is
  embedded in an IP frame (offsetToIP >= 14), per RFC 2131 §4.1. It previously
  sent to gwmac which clients without an ARP entry could not receive

Signed-off-by: deadprogram <ron@hybridgroup.com>

* fix: accept 255.255.255.255 aka bradcast dst in demux4

This fixes a problem with accept 255.255.255.255 dst in demux4 which previously dropped when stack had a static IP.

Signed-off-by: deadprogram <ron@hybridgroup.com>

---------

Signed-off-by: deadprogram <ron@hybridgroup.com>
Co-authored-by: Patricio Whittingslow <graded.sp@gmail.com>
2026-06-24 22:16:53 -03:00

392 lines
11 KiB
Go

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 {
// No explicit client identifier: use chaddr as the stable lookup key.
// This lets the server correlate Discover→Offer→Request even when
// ciaddr=0.0.0.0 (client has no IP yet), which is the normal case
// for first-time lease acquisition (RFC 2131 §4.3.1).
chaddr := *dfrm.CHAddrAs6()
copy(clientIDRaw[:], chaddr[:])
client, clientExists = sv.getClient(clientIDRaw)
if !clientExists {
// Fallback: look up by ciaddr for clients that did send ciaddr.
ciaddr := *dfrm.CIAddr()
if ciaddr != ([4]byte{}) {
client, clientIDRaw, clientExists = sv.getClientByIP(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
}
// Per RFC 2131 §4.1: unicast Offer/Ack to chaddr because the client
// does not yet have the offered IP, so no ARP entry exists.
// The Ethernet layer sets dst=gwmac before calling us; overwrite it
// here so the frame reaches the client via its hardware address.
// offsetToIP==14 means the Ethernet header is at carrierData[0:14].
if offsetToIP >= 14 {
copy(carrierData[offsetToIP-14:offsetToIP-8], client.hwaddr[:])
}
}
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
}