mirror of
https://github.com/soypat/lneto.git
synced 2026-08-17 13:23:30 +00:00
7d5830d7ab
* begin adding udp.MuxHandler * add udp MuxHandlerSIMO/MIMO * add tcp rx shutdown * icmpv6 client * icmpv6 Client shared NDP/Echo preparation * icmpv6 client ndp/echo split * icmpv6 client ndp/echo split done * icmpv6 adjustments * add dhcpv6 stubs * dhcpv4 preliminary revision * add dns.NextLabel * dns label name tweaks * dns begin work on TCP client * add dnstcp package * apply gofmt changes * add udp mux tests * clean up, remove StackBig for now * remove dnstcp so as to merged confident parts and we continue dnstcp work elsewhere
305 lines
9.5 KiB
Go
305 lines
9.5 KiB
Go
package dhcpv6
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"net"
|
|
"net/netip"
|
|
|
|
"github.com/soypat/lneto"
|
|
"github.com/soypat/lneto/internal"
|
|
)
|
|
|
|
// RequestConfig holds the parameters for starting a DHCPv6 exchange.
|
|
type RequestConfig struct {
|
|
// ClientHardwareAddr is the client's Ethernet MAC address.
|
|
// It is used to construct the client DUID-LL and IAID.
|
|
ClientHardwareAddr [6]byte
|
|
}
|
|
|
|
// Client is a stateful DHCPv6 client implementing the [lneto.StackNode] interface.
|
|
// It manages the Solicit→Advertise→Request→Reply exchange (RFC 8415 §18).
|
|
//
|
|
// Typical usage:
|
|
//
|
|
// var cl Client
|
|
// cl.BeginRequest(xid, RequestConfig{ClientHardwareAddr: mac})
|
|
// // drive Encapsulate / Demux calls via the network stack
|
|
type Client struct {
|
|
connID uint64
|
|
state ClientState
|
|
xid uint32 // lower 24 bits used
|
|
|
|
// duid is the client's DUID-LL. Client owns the backing array; it is set
|
|
// once from the MAC in BeginRequest and carried across resets unchanged.
|
|
duid []byte
|
|
// serverDUID is the selected server's DUID. Client owns the backing array;
|
|
// it is cleared (len=0) on reset so capacity is reused without allocation.
|
|
serverDUID []byte
|
|
|
|
// dns accumulates DNS recursive name server addresses (OptDNSServers).
|
|
// Client owns the backing array; cleared on reset, capacity reused.
|
|
dns []netip.Addr
|
|
|
|
assignedAddr [16]byte
|
|
assignedAddrValid bool
|
|
|
|
// iaid is derived from the first 4 bytes of the client MAC.
|
|
iaid [4]byte
|
|
|
|
// IA_NA timers from the server's Advertise/Reply.
|
|
t1, t2 uint32
|
|
preferredLifetime uint32
|
|
validLifetime uint32
|
|
|
|
clientMAC [6]byte
|
|
|
|
// auxbuf is a scratch buffer used during Encapsulate to avoid allocations.
|
|
auxbuf [128]byte
|
|
}
|
|
|
|
// BeginRequest initialises a new DHCPv6 exchange with the given 24-bit transaction ID.
|
|
// It must be called before any Encapsulate or Demux calls.
|
|
func (c *Client) BeginRequest(xid uint32, cfg RequestConfig) error {
|
|
if xid == 0 {
|
|
return lneto.ErrInvalidConfig
|
|
} else if c.state != StateInit && c.state != 0 {
|
|
return lneto.ErrInvalidConfig
|
|
} else if internal.IsZeroed(cfg.ClientHardwareAddr[:]...) {
|
|
return lneto.ErrInvalidConfig
|
|
}
|
|
c.clientMAC = cfg.ClientHardwareAddr
|
|
c.iaid = [4]byte(cfg.ClientHardwareAddr[:4])
|
|
c.xid = xid & 0xFFFFFF
|
|
c.reset()
|
|
c.duid = AppendDUIDLL(c.duid[:0], cfg.ClientHardwareAddr)
|
|
c.state = StateInit
|
|
return nil
|
|
}
|
|
|
|
// reset clears exchange state while preserving slice backing arrays and the
|
|
// connection ID is incremented to invalidate any existing stack registrations.
|
|
func (c *Client) reset() {
|
|
*c = Client{
|
|
connID: c.connID + 1,
|
|
xid: c.xid,
|
|
clientMAC: c.clientMAC,
|
|
iaid: c.iaid,
|
|
duid: c.duid,
|
|
serverDUID: c.serverDUID[:0],
|
|
dns: c.dns[:0],
|
|
}
|
|
}
|
|
|
|
// Encapsulate writes the next outgoing DHCPv6 message into carrierData[offsetToFrame:].
|
|
// Returns the number of bytes written or 0 if there is nothing to send in the current state.
|
|
// Implements [lneto.StackNode].
|
|
func (c *Client) Encapsulate(carrierData []byte, _, offsetToFrame int) (int, error) {
|
|
if c.isClosed() {
|
|
return 0, net.ErrClosed
|
|
}
|
|
dst := carrierData[offsetToFrame:]
|
|
if len(dst) < OptionsOffset+128 {
|
|
return 0, lneto.ErrShortBuffer
|
|
}
|
|
frm, err := NewFrame(dst)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
var numOpts int
|
|
var nextState ClientState
|
|
|
|
switch c.state {
|
|
case StateInit:
|
|
frm.SetMsgType(MsgSolicit)
|
|
frm.SetTransactionID(c.xid)
|
|
n, _ := EncodeOption(dst[OptionsOffset+numOpts:], OptClientID, c.duid...)
|
|
numOpts += n
|
|
n, _ = EncodeOptionIANA(dst[OptionsOffset+numOpts:], c.iaid, 0, 0, nil)
|
|
numOpts += n
|
|
n, _ = EncodeOption(dst[OptionsOffset+numOpts:], OptORO, defaultOptRequestList...)
|
|
numOpts += n
|
|
n, _ = EncodeOption16(dst[OptionsOffset+numOpts:], OptElapsedTime, 0)
|
|
numOpts += n
|
|
nextState = StateSoliciting
|
|
|
|
case StateRequesting:
|
|
frm.SetMsgType(MsgRequest)
|
|
frm.SetTransactionID(c.xid)
|
|
n, _ := EncodeOption(dst[OptionsOffset+numOpts:], OptClientID, c.duid...)
|
|
numOpts += n
|
|
n, _ = EncodeOption(dst[OptionsOffset+numOpts:], OptServerID, c.serverDUID...)
|
|
numOpts += n
|
|
auxN, _ := EncodeOptionIAAddr(c.auxbuf[:], c.assignedAddr, 0, 0)
|
|
n, _ = EncodeOptionIANA(dst[OptionsOffset+numOpts:], c.iaid, 0, 0, c.auxbuf[:auxN])
|
|
numOpts += n
|
|
n, _ = EncodeOption(dst[OptionsOffset+numOpts:], OptORO, defaultOptRequestList...)
|
|
numOpts += n
|
|
n, _ = EncodeOption16(dst[OptionsOffset+numOpts:], OptElapsedTime, 0)
|
|
numOpts += n
|
|
nextState = StateRequesting // retransmittable; Demux(Reply) advances to Bound
|
|
|
|
case StateRenewing:
|
|
frm.SetMsgType(MsgRenew)
|
|
frm.SetTransactionID(c.xid)
|
|
n, _ := EncodeOption(dst[OptionsOffset+numOpts:], OptClientID, c.duid...)
|
|
numOpts += n
|
|
n, _ = EncodeOption(dst[OptionsOffset+numOpts:], OptServerID, c.serverDUID...)
|
|
numOpts += n
|
|
auxN, _ := EncodeOptionIAAddr(c.auxbuf[:], c.assignedAddr, 0, 0)
|
|
n, _ = EncodeOptionIANA(dst[OptionsOffset+numOpts:], c.iaid, 0, 0, c.auxbuf[:auxN])
|
|
numOpts += n
|
|
n, _ = EncodeOption16(dst[OptionsOffset+numOpts:], OptElapsedTime, 0)
|
|
numOpts += n
|
|
nextState = StateRenewing
|
|
|
|
case StateRebinding:
|
|
frm.SetMsgType(MsgRebind)
|
|
frm.SetTransactionID(c.xid)
|
|
n, _ := EncodeOption(dst[OptionsOffset+numOpts:], OptClientID, c.duid...)
|
|
numOpts += n
|
|
// No OptServerID in Rebind (RFC 8415 §18.2.5).
|
|
auxN, _ := EncodeOptionIAAddr(c.auxbuf[:], c.assignedAddr, 0, 0)
|
|
n, _ = EncodeOptionIANA(dst[OptionsOffset+numOpts:], c.iaid, 0, 0, c.auxbuf[:auxN])
|
|
numOpts += n
|
|
n, _ = EncodeOption16(dst[OptionsOffset+numOpts:], OptElapsedTime, 0)
|
|
numOpts += n
|
|
nextState = StateRebinding
|
|
|
|
default:
|
|
return 0, nil // StateSoliciting, StateBound, or uninitialised.
|
|
}
|
|
|
|
c.state = nextState
|
|
return OptionsOffset + numOpts, nil
|
|
}
|
|
|
|
// Demux processes an incoming DHCPv6 message at carrierData[frameOffset:].
|
|
// It validates the transaction ID and advances the client state machine on success.
|
|
// Implements [lneto.StackNode].
|
|
func (c *Client) Demux(carrierData []byte, frameOffset int) error {
|
|
if c.isClosed() {
|
|
return net.ErrClosed
|
|
}
|
|
frm, err := NewFrame(carrierData[frameOffset:])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if frm.TransactionID() != c.xid {
|
|
return lneto.ErrMismatch
|
|
}
|
|
|
|
msgType := frm.MsgType()
|
|
var nextState ClientState
|
|
switch c.state {
|
|
case StateSoliciting:
|
|
if msgType != MsgAdvertise {
|
|
return lneto.ErrPacketDrop
|
|
}
|
|
nextState = StateRequesting
|
|
case StateRequesting, StateRenewing, StateRebinding:
|
|
if msgType != MsgReply {
|
|
return lneto.ErrPacketDrop
|
|
}
|
|
nextState = StateBound
|
|
default:
|
|
return lneto.ErrPacketDrop
|
|
}
|
|
|
|
if err := c.setOptions(frm); err != nil {
|
|
return err
|
|
}
|
|
c.state = nextState
|
|
return nil
|
|
}
|
|
|
|
// setOptions parses all DHCPv6 options in frm and stores relevant values.
|
|
func (c *Client) setOptions(frm Frame) error {
|
|
return frm.ForEachOption(func(_ int, code OptCode, data []byte) error {
|
|
switch code {
|
|
case OptServerID:
|
|
c.serverDUID = append(c.serverDUID[:0], data...)
|
|
case OptIANA:
|
|
c.parseIANA(data)
|
|
case OptDNSServers:
|
|
if len(c.dns) > 0 || len(data)%16 != 0 {
|
|
break // skip if already populated or malformed
|
|
}
|
|
for i := 0; i+16 <= len(data); i += 16 {
|
|
c.dns = append(c.dns, netip.AddrFrom16([16]byte(data[i:i+16])))
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// parseIANA processes the payload of an OptIANA option, extracting the
|
|
// assigned address and lease timers from any embedded OptIAAddr sub-option.
|
|
func (c *Client) parseIANA(data []byte) {
|
|
if len(data) < 12 {
|
|
return
|
|
}
|
|
if [4]byte(data[:4]) != c.iaid {
|
|
return // not our Identity Association
|
|
}
|
|
t1 := binary.BigEndian.Uint32(data[4:8])
|
|
t2 := binary.BigEndian.Uint32(data[8:12])
|
|
|
|
// Iterate sub-options manually (same 4-byte TLV format).
|
|
ptr := 12
|
|
for ptr+4 <= len(data) {
|
|
subCode := OptCode(binary.BigEndian.Uint16(data[ptr:]))
|
|
subLen := int(binary.BigEndian.Uint16(data[ptr+2:]))
|
|
if ptr+4+subLen > len(data) {
|
|
break // malformed sub-option; stop safely
|
|
}
|
|
if subCode == OptIAAddr && subLen >= 24 {
|
|
sub := data[ptr+4 : ptr+4+subLen]
|
|
c.assignedAddr = [16]byte(sub[:16])
|
|
c.assignedAddrValid = true
|
|
c.preferredLifetime = binary.BigEndian.Uint32(sub[16:20])
|
|
c.validLifetime = binary.BigEndian.Uint32(sub[20:24])
|
|
}
|
|
ptr += 4 + subLen
|
|
}
|
|
if c.assignedAddrValid {
|
|
c.t1 = t1
|
|
c.t2 = t2
|
|
}
|
|
}
|
|
|
|
func (c *Client) isClosed() bool { return c.state == 0 || c.xid == 0 }
|
|
|
|
// State returns the current client state.
|
|
func (c *Client) State() ClientState { return c.state }
|
|
|
|
// AssignedAddr returns the IPv6 address assigned by the server and whether it is valid.
|
|
func (c *Client) AssignedAddr() ([16]byte, bool) { return c.assignedAddr, c.assignedAddrValid }
|
|
|
|
// AppendDNSServers appends the DNS server addresses received from the server to dst.
|
|
func (c *Client) AppendDNSServers(dst []netip.Addr) []netip.Addr { return append(dst, c.dns...) }
|
|
|
|
// NumDNSServers returns the number of DNS server addresses received.
|
|
func (c *Client) NumDNSServers() int { return len(c.dns) }
|
|
|
|
// ConnectionID returns a pointer to the client's connection ID.
|
|
// The value increments on each reset; callers should discard registrations when it changes.
|
|
// Implements [lneto.StackNode].
|
|
func (c *Client) ConnectionID() *uint64 { return &c.connID }
|
|
|
|
// LocalPort returns the DHCPv6 client port (546).
|
|
// Implements [lneto.StackNode].
|
|
func (c *Client) LocalPort() uint16 { return ClientPort }
|
|
|
|
// Protocol returns the IP protocol number for UDP.
|
|
// Implements [lneto.StackNode].
|
|
func (c *Client) Protocol() uint64 { return uint64(lneto.IPProtoUDP) }
|
|
|
|
// defaultOptRequestList is the ORO payload (RFC 8415 §21.7) listing the options
|
|
// the client wants the server to include in its reply.
|
|
var defaultOptRequestList = []byte{
|
|
byte(OptDNSServers >> 8), byte(OptDNSServers), // 23
|
|
byte(OptDomainList >> 8), byte(OptDomainList), // 24
|
|
byte(OptNTPServer >> 8), byte(OptNTPServer), // 56
|
|
}
|