mirror of
https://github.com/soypat/lneto.git
synced 2026-07-26 10:38:47 +00:00
a2970b923d
* add ipv6 to xnet.StackAsync * dns improvements * improve DNS workings of StackAsync * add tentative ICMPv6 * work on prefixes and fix some small bugs, plan UDP/TCP6 * fix bugs in StackAsync and ipv4.Prefix.Contains * update arpsubtable * completely remove legacy internet.StackIP for StackIPv4/v6 * ipv4/ipv6 tcp/udp * add TCP6/UDP6 dialing APIs * add xnet.Stack6 interface * more ipv6 integration into StackAsync; various tweaks to lneto and documentation+TODOs * add stack6 tests * replace netip.Prefix with ipv4.Prefix where it makes sense
371 lines
9.6 KiB
Go
371 lines
9.6 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 {
|
|
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
|
|
}
|