mirror of
https://github.com/soypat/lneto.git
synced 2026-08-09 17:33:43 +00:00
fa609ea54f
* 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>
392 lines
11 KiB
Go
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
|
|
}
|