Files
lneto/dhcp/dhcpv6/client.go
Pat Whittingslow 7d5830d7ab V2 Netbird integration - UDP MIMO/SIMO, ICMPv6, DHCPv6 implementations (#106)
* 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
2026-05-10 12:16:30 -03:00

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
}