mirror of
https://github.com/soypat/lneto.git
synced 2026-09-10 00:29:34 +00:00
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
This commit is contained in:
@@ -0,0 +1,304 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
package dhcpv6
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// writeOpt6 encodes a single DHCPv6 option into dst using the 4-byte TLV header
|
||||
// (2-byte code + 2-byte length) and returns the total bytes written.
|
||||
// Used by tests to build server frames without depending on the stub EncodeOption.
|
||||
func writeOpt6(dst []byte, code OptCode, data ...byte) int {
|
||||
binary.BigEndian.PutUint16(dst[0:2], uint16(code))
|
||||
binary.BigEndian.PutUint16(dst[2:4], uint16(len(data)))
|
||||
copy(dst[4:], data)
|
||||
return 4 + len(data)
|
||||
}
|
||||
|
||||
// buildServerFrame constructs a minimal DHCPv6 Advertise or Reply frame
|
||||
// containing OptServerID, and an OptIANA with an embedded OptIAAddr.
|
||||
func buildServerFrame(msgType MsgType, xid uint32, serverDUID []byte, iaid [4]byte, addr [16]byte) []byte {
|
||||
// IAAddr payload: addr(16) + preferred(4) + valid(4)
|
||||
iaAddrPayload := make([]byte, 24)
|
||||
copy(iaAddrPayload[:16], addr[:])
|
||||
binary.BigEndian.PutUint32(iaAddrPayload[16:20], 3600)
|
||||
binary.BigEndian.PutUint32(iaAddrPayload[20:24], 7200)
|
||||
|
||||
// Encode IAAddr as an option.
|
||||
iaAddrOpt := make([]byte, 4+len(iaAddrPayload))
|
||||
writeOpt6(iaAddrOpt, OptIAAddr, iaAddrPayload...)
|
||||
|
||||
// IA_NA payload: IAID(4) + T1(4) + T2(4) + IAAddr option.
|
||||
iaNAPayload := make([]byte, 12+len(iaAddrOpt))
|
||||
copy(iaNAPayload[:4], iaid[:])
|
||||
binary.BigEndian.PutUint32(iaNAPayload[4:8], 1800)
|
||||
binary.BigEndian.PutUint32(iaNAPayload[8:12], 3600)
|
||||
copy(iaNAPayload[12:], iaAddrOpt)
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
buf[0] = byte(msgType)
|
||||
buf[1] = byte(xid >> 16)
|
||||
buf[2] = byte(xid >> 8)
|
||||
buf[3] = byte(xid)
|
||||
n := OptionsOffset
|
||||
n += writeOpt6(buf[n:], OptServerID, serverDUID...)
|
||||
n += writeOpt6(buf[n:], OptIANA, iaNAPayload...)
|
||||
return buf[:n]
|
||||
}
|
||||
|
||||
// TestFrameForEachOption verifies that ForEachOption correctly delivers
|
||||
// the option code and data to the callback for a hand-built frame.
|
||||
func TestFrameForEachOption(t *testing.T) {
|
||||
buf := make([]byte, OptionsOffset+8)
|
||||
buf[0] = byte(MsgSolicit)
|
||||
buf[3] = 42 // XID low byte
|
||||
|
||||
n := writeOpt6(buf[OptionsOffset:], OptClientID, 'A', 'B')
|
||||
frm, err := NewFrame(buf[:OptionsOffset+n])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var gotCode OptCode
|
||||
var gotData []byte
|
||||
err = frm.ForEachOption(func(_ int, code OptCode, data []byte) error {
|
||||
gotCode = code
|
||||
gotData = append(gotData[:0], data...)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal("ForEachOption:", err)
|
||||
}
|
||||
if gotCode != OptClientID {
|
||||
t.Errorf("option code: want %d (OptClientID), got %d", OptClientID, gotCode)
|
||||
}
|
||||
if string(gotData) != "AB" {
|
||||
t.Errorf("option data: want %q, got %q", "AB", gotData)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFrameValidateSize verifies that ValidateSize returns an error when an option's
|
||||
// declared length extends past the end of the buffer.
|
||||
func TestFrameValidateSize(t *testing.T) {
|
||||
buf := make([]byte, OptionsOffset+6)
|
||||
buf[0] = byte(MsgSolicit)
|
||||
// Option code = OptClientID, claimed length = 100, actual data = 2 bytes.
|
||||
binary.BigEndian.PutUint16(buf[OptionsOffset:], uint16(OptClientID))
|
||||
binary.BigEndian.PutUint16(buf[OptionsOffset+2:], 100)
|
||||
buf[OptionsOffset+4] = 'A'
|
||||
buf[OptionsOffset+5] = 'B'
|
||||
|
||||
frm, err := NewFrame(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := frm.ValidateSize(); err == nil {
|
||||
t.Error("ValidateSize: want error for truncated option, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// TestClientSolicitRequest exercises the full four-step DHCPv6 exchange:
|
||||
// Solicit → (fabricated) Advertise → Request → (fabricated) Reply → Bound.
|
||||
func TestClientSolicitRequest(t *testing.T) {
|
||||
const xid = 0x112233
|
||||
clientMAC := [6]byte{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}
|
||||
serverDUID := []byte{0, 3, 0, 1, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66}
|
||||
assignedAddr := [16]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
|
||||
iaid := [4]byte{clientMAC[0], clientMAC[1], clientMAC[2], clientMAC[3]}
|
||||
|
||||
var cl Client
|
||||
if err := cl.BeginRequest(xid, RequestConfig{ClientHardwareAddr: clientMAC}); err != nil {
|
||||
t.Fatal("BeginRequest:", err)
|
||||
}
|
||||
if cl.State() != StateInit {
|
||||
t.Fatalf("initial state: want StateInit, got %v", cl.State())
|
||||
}
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
|
||||
// CLIENT: send Solicit.
|
||||
n, err := cl.Encapsulate(buf, -1, 0)
|
||||
if err != nil {
|
||||
t.Fatal("Encapsulate (Solicit):", err)
|
||||
}
|
||||
if n == 0 {
|
||||
t.Fatal("Encapsulate (Solicit): wrote 0 bytes")
|
||||
}
|
||||
if cl.State() != StateSoliciting {
|
||||
t.Fatalf("after Solicit: want StateSoliciting, got %v", cl.State())
|
||||
}
|
||||
|
||||
// SERVER: fabricated Advertise.
|
||||
advFrame := buildServerFrame(MsgAdvertise, xid, serverDUID, iaid, assignedAddr)
|
||||
if err := cl.Demux(advFrame, 0); err != nil {
|
||||
t.Fatal("Demux (Advertise):", err)
|
||||
}
|
||||
if cl.State() != StateRequesting {
|
||||
t.Fatalf("after Advertise: want StateRequesting, got %v", cl.State())
|
||||
}
|
||||
|
||||
// CLIENT: send Request.
|
||||
n, err = cl.Encapsulate(buf, -1, 0)
|
||||
if err != nil {
|
||||
t.Fatal("Encapsulate (Request):", err)
|
||||
}
|
||||
if n == 0 {
|
||||
t.Fatal("Encapsulate (Request): wrote 0 bytes")
|
||||
}
|
||||
|
||||
// SERVER: fabricated Reply.
|
||||
replyFrame := buildServerFrame(MsgReply, xid, serverDUID, iaid, assignedAddr)
|
||||
if err := cl.Demux(replyFrame, 0); err != nil {
|
||||
t.Fatal("Demux (Reply):", err)
|
||||
}
|
||||
if cl.State() != StateBound {
|
||||
t.Fatalf("after Reply: want StateBound, got %v", cl.State())
|
||||
}
|
||||
|
||||
addr, valid := cl.AssignedAddr()
|
||||
if !valid {
|
||||
t.Fatal("AssignedAddr: not valid after bound")
|
||||
}
|
||||
if addr != assignedAddr {
|
||||
t.Errorf("AssignedAddr: got %v, want %v", addr, assignedAddr)
|
||||
}
|
||||
}
|
||||
|
||||
// TestClientEncapsulateSolicit verifies that the first Encapsulate call
|
||||
// writes a Solicit message with at least OptClientID and OptIANA.
|
||||
func TestClientEncapsulateSolicit(t *testing.T) {
|
||||
var cl Client
|
||||
if err := cl.BeginRequest(0xABCDEF, RequestConfig{
|
||||
ClientHardwareAddr: [6]byte{1, 2, 3, 4, 5, 6},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
buf := make([]byte, 512)
|
||||
n, err := cl.Encapsulate(buf, -1, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n == 0 {
|
||||
t.Fatal("Encapsulate: wrote 0 bytes")
|
||||
}
|
||||
|
||||
frm, err := NewFrame(buf[:n])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if frm.MsgType() != MsgSolicit {
|
||||
t.Errorf("MsgType: want MsgSolicit, got %v", frm.MsgType())
|
||||
}
|
||||
if frm.TransactionID() != 0xABCDEF {
|
||||
t.Errorf("TransactionID: want 0xABCDEF, got 0x%X", frm.TransactionID())
|
||||
}
|
||||
|
||||
var hasClientID, hasIANA bool
|
||||
err = frm.ForEachOption(func(_ int, code OptCode, _ []byte) error {
|
||||
switch code {
|
||||
case OptClientID:
|
||||
hasClientID = true
|
||||
case OptIANA:
|
||||
hasIANA = true
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal("ForEachOption:", err)
|
||||
}
|
||||
if !hasClientID {
|
||||
t.Error("Solicit: missing OptClientID")
|
||||
}
|
||||
if !hasIANA {
|
||||
t.Error("Solicit: missing OptIANA")
|
||||
}
|
||||
}
|
||||
|
||||
// TestClientDoubleTapEncapsulate verifies that calling Encapsulate twice in the
|
||||
// same state returns 0 bytes on the second call (idempotent, no duplicate messages).
|
||||
func TestClientDoubleTapEncapsulate(t *testing.T) {
|
||||
var cl Client
|
||||
if err := cl.BeginRequest(1, RequestConfig{
|
||||
ClientHardwareAddr: [6]byte{1, 2, 3, 4, 5, 6},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
buf := make([]byte, 512)
|
||||
n, err := cl.Encapsulate(buf, -1, 0)
|
||||
if err != nil {
|
||||
t.Fatal("first Encapsulate:", err)
|
||||
}
|
||||
if n == 0 {
|
||||
t.Fatal("first Encapsulate: wrote 0 bytes")
|
||||
}
|
||||
|
||||
n2, err := cl.Encapsulate(buf, -1, 0)
|
||||
if err != nil {
|
||||
t.Fatal("second Encapsulate:", err)
|
||||
}
|
||||
if n2 != 0 {
|
||||
t.Errorf("second Encapsulate: want 0 bytes (idempotent), got %d", n2)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package dhcpv6
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/soypat/lneto"
|
||||
)
|
||||
|
||||
// ClientState transition table during request:
|
||||
//
|
||||
// StateInit -> | Send out Solicit | -> StateSoliciting
|
||||
// StateSoliciting -> | Accept Advertise | -> StateRequesting
|
||||
// StateRequesting -> | Receive Reply | -> StateBound
|
||||
|
||||
//go:generate stringer -type=MsgType,ClientState,OptCode,StatusCode,DUIDType -linecomment -output stringers.go
|
||||
|
||||
const (
|
||||
// ClientPort is the UDP port DHCPv6 clients listen on.
|
||||
ClientPort = 546
|
||||
// ServerPort is the UDP port DHCPv6 servers listen on.
|
||||
ServerPort = 547
|
||||
// OptionsOffset is the byte offset where DHCPv6 options begin in a client-server message.
|
||||
// Layout: MsgType(1) + TransactionID(3).
|
||||
OptionsOffset = 4
|
||||
)
|
||||
|
||||
// MsgType is the DHCPv6 message type (RFC 8415 §7.3).
|
||||
type MsgType uint8
|
||||
|
||||
const (
|
||||
MsgSolicit MsgType = 1 // solicit
|
||||
MsgAdvertise MsgType = 2 // advertise
|
||||
MsgRequest MsgType = 3 // request
|
||||
MsgConfirm MsgType = 4 // confirm
|
||||
MsgRenew MsgType = 5 // renew
|
||||
MsgRebind MsgType = 6 // rebind
|
||||
MsgReply MsgType = 7 // reply
|
||||
MsgRelease MsgType = 8 // release
|
||||
MsgDecline MsgType = 9 // decline
|
||||
MsgReconfigure MsgType = 10 // reconfigure
|
||||
MsgInformRequest MsgType = 11 // inform-request
|
||||
MsgRelayForw MsgType = 12 // relay-forw
|
||||
MsgRelayRepl MsgType = 13 // relay-repl
|
||||
)
|
||||
|
||||
// ClientState is the DHCPv6 client DORA state machine state.
|
||||
type ClientState uint8
|
||||
|
||||
const (
|
||||
_ ClientState = iota
|
||||
StateInit // init
|
||||
StateSoliciting // soliciting
|
||||
StateRequesting // requesting
|
||||
StateBound // bound
|
||||
StateRenewing // renewing
|
||||
StateRebinding // rebinding
|
||||
)
|
||||
|
||||
// HasIP returns true if the state indicates the Client has an IPv6 address assigned.
|
||||
func (s ClientState) HasIP() bool {
|
||||
return s == StateBound || s == StateRenewing || s == StateRebinding
|
||||
}
|
||||
|
||||
// OptCode is a DHCPv6 option code (RFC 8415 §21), encoded as a 2-byte big-endian value.
|
||||
type OptCode uint16
|
||||
|
||||
const (
|
||||
OptClientID OptCode = 1 // client-id
|
||||
OptServerID OptCode = 2 // server-id
|
||||
OptIANA OptCode = 3 // ia-na
|
||||
OptIATA OptCode = 4 // ia-ta
|
||||
OptIAAddr OptCode = 5 // iaaddr
|
||||
OptORO OptCode = 6 // oro
|
||||
OptPreference OptCode = 7 // preference
|
||||
OptElapsedTime OptCode = 8 // elapsed-time
|
||||
OptRelayMsg OptCode = 9 // relay-msg
|
||||
OptAuth OptCode = 11 // auth
|
||||
OptUnicast OptCode = 12 // unicast
|
||||
OptStatusCode OptCode = 13 // status-code
|
||||
OptRapidCommit OptCode = 14 // rapid-commit
|
||||
OptUserClass OptCode = 15 // user-class
|
||||
OptVendorClass OptCode = 16 // vendor-class
|
||||
OptVendorOpts OptCode = 17 // vendor-opts
|
||||
OptInterfaceID OptCode = 18 // interface-id
|
||||
OptReconfMsg OptCode = 19 // reconf-msg
|
||||
OptReconfAccept OptCode = 20 // reconf-accept
|
||||
OptDNSServers OptCode = 23 // dns-servers
|
||||
OptDomainList OptCode = 24 // domain-list
|
||||
OptIAPD OptCode = 25 // ia-pd
|
||||
OptIAPrefix OptCode = 26 // iaprefix
|
||||
OptNTPServer OptCode = 56 // ntp-server
|
||||
)
|
||||
|
||||
// DUIDType is the DHCP Unique Identifier type (RFC 8415 §11).
|
||||
type DUIDType uint16
|
||||
|
||||
const (
|
||||
DUIDTypeLLT DUIDType = 1 // duid-llt
|
||||
DUIDTypeEN DUIDType = 2 // duid-en
|
||||
DUIDTypeLL DUIDType = 3 // duid-ll
|
||||
)
|
||||
|
||||
// StatusCode is the DHCPv6 status code value (RFC 8415 §21.13).
|
||||
type StatusCode uint16
|
||||
|
||||
const (
|
||||
StatusSuccess StatusCode = 0 // success
|
||||
StatusUnspecFail StatusCode = 1 // unspec-fail
|
||||
StatusNoAddrsAvail StatusCode = 2 // no-addrs-avail
|
||||
StatusNoBinding StatusCode = 3 // no-binding
|
||||
StatusNotOnLink StatusCode = 4 // not-on-link
|
||||
StatusUseMulticast StatusCode = 5 // use-multicast
|
||||
)
|
||||
|
||||
// EncodeOption writes a DHCPv6 TLV option into dst.
|
||||
// Format: code(2) + length(2) + data.
|
||||
func EncodeOption(dst []byte, code OptCode, data ...byte) (int, error) {
|
||||
if len(data) > 0xffff {
|
||||
return 0, lneto.ErrInvalidLengthField
|
||||
} else if len(dst) < 4+len(data) {
|
||||
return 0, lneto.ErrShortBuffer
|
||||
}
|
||||
binary.BigEndian.PutUint16(dst[0:2], uint16(code))
|
||||
binary.BigEndian.PutUint16(dst[2:4], uint16(len(data)))
|
||||
copy(dst[4:], data)
|
||||
return 4 + len(data), nil
|
||||
}
|
||||
|
||||
// EncodeOption16 encodes a single uint16 value as a DHCPv6 option.
|
||||
func EncodeOption16(dst []byte, code OptCode, v uint16) (int, error) {
|
||||
return EncodeOption(dst, code, byte(v>>8), byte(v))
|
||||
}
|
||||
|
||||
// EncodeOption32 encodes a single uint32 value as a DHCPv6 option.
|
||||
func EncodeOption32(dst []byte, code OptCode, v uint32) (int, error) {
|
||||
return EncodeOption(dst, code, byte(v>>24), byte(v>>16), byte(v>>8), byte(v))
|
||||
}
|
||||
|
||||
// EncodeOptionIANA encodes an IA_NA option (RFC 8415 §21.4).
|
||||
// Layout: code(2) + len(2) + IAID(4) + T1(4) + T2(4) + subOpts.
|
||||
func EncodeOptionIANA(dst []byte, iaid [4]byte, t1, t2 uint32, subOpts []byte) (int, error) {
|
||||
const fixedLen = 12 // IAID(4) + T1(4) + T2(4)
|
||||
dataLen := fixedLen + len(subOpts)
|
||||
if len(dst) < 4+dataLen {
|
||||
return 0, lneto.ErrShortBuffer
|
||||
}
|
||||
binary.BigEndian.PutUint16(dst[0:2], uint16(OptIANA))
|
||||
binary.BigEndian.PutUint16(dst[2:4], uint16(dataLen))
|
||||
copy(dst[4:8], iaid[:])
|
||||
binary.BigEndian.PutUint32(dst[8:12], t1)
|
||||
binary.BigEndian.PutUint32(dst[12:16], t2)
|
||||
copy(dst[16:], subOpts)
|
||||
return 4 + dataLen, nil
|
||||
}
|
||||
|
||||
// EncodeOptionIAAddr encodes an IAADDR option (RFC 8415 §21.6).
|
||||
// Layout: code(2) + len(2) + addr(16) + preferred(4) + valid(4).
|
||||
func EncodeOptionIAAddr(dst []byte, addr [16]byte, preferred, valid uint32) (int, error) {
|
||||
const dataLen = 24 // addr(16) + preferred(4) + valid(4)
|
||||
if len(dst) < 4+dataLen {
|
||||
return 0, lneto.ErrShortBuffer
|
||||
}
|
||||
binary.BigEndian.PutUint16(dst[0:2], uint16(OptIAAddr))
|
||||
binary.BigEndian.PutUint16(dst[2:4], dataLen)
|
||||
copy(dst[4:20], addr[:])
|
||||
binary.BigEndian.PutUint32(dst[20:24], preferred)
|
||||
binary.BigEndian.PutUint32(dst[24:28], valid)
|
||||
return 4 + dataLen, nil
|
||||
}
|
||||
|
||||
// AppendDUIDLL appends a DUID-LL (type 3) for an Ethernet MAC to dst.
|
||||
// Format: DUIDType(2) + hwtype=1(2) + mac(6).
|
||||
func AppendDUIDLL(dst []byte, mac [6]byte) []byte {
|
||||
return append(dst,
|
||||
byte(DUIDTypeLL>>8), byte(DUIDTypeLL), // type 3
|
||||
0, 1, // hardware type 1 = Ethernet
|
||||
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5],
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package dhcpv6
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
|
||||
"github.com/soypat/lneto"
|
||||
)
|
||||
|
||||
// NewFrame returns a Frame backed by buf.
|
||||
// Returns an error if buf is shorter than [OptionsOffset] bytes.
|
||||
func NewFrame(buf []byte) (Frame, error) {
|
||||
if len(buf) < OptionsOffset {
|
||||
return Frame{}, lneto.ErrTruncatedFrame
|
||||
}
|
||||
return Frame{buf: buf}, nil
|
||||
}
|
||||
|
||||
// Frame encapsulates the raw bytes of a DHCPv6 client-server message (RFC 8415 §8)
|
||||
// and provides methods for accessing and modifying its fields.
|
||||
//
|
||||
// Layout:
|
||||
//
|
||||
// Byte 0: msg-type
|
||||
// Bytes 1-3: transaction-id (24-bit big-endian)
|
||||
// Bytes 4+: options (code(2) + length(2) + data)
|
||||
type Frame struct {
|
||||
buf []byte
|
||||
}
|
||||
|
||||
// MsgType returns the message type field.
|
||||
func (frm Frame) MsgType() MsgType { return MsgType(frm.buf[0]) }
|
||||
|
||||
// SetMsgType sets the message type field.
|
||||
func (frm Frame) SetMsgType(t MsgType) { frm.buf[0] = byte(t) }
|
||||
|
||||
// TransactionID returns the 24-bit transaction ID as a uint32 (upper byte is always zero).
|
||||
func (frm Frame) TransactionID() uint32 {
|
||||
return uint32(frm.buf[1])<<16 | uint32(frm.buf[2])<<8 | uint32(frm.buf[3])
|
||||
}
|
||||
|
||||
// SetTransactionID writes the lower 24 bits of id into bytes 1–3.
|
||||
func (frm Frame) SetTransactionID(id uint32) {
|
||||
frm.buf[1] = byte(id >> 16)
|
||||
frm.buf[2] = byte(id >> 8)
|
||||
frm.buf[3] = byte(id)
|
||||
}
|
||||
|
||||
// Options returns the options section of the frame (bytes from [OptionsOffset] onward).
|
||||
func (frm Frame) Options() []byte { return frm.buf[OptionsOffset:] }
|
||||
|
||||
// ForEachOption iterates over all DHCPv6 options in the frame's options section.
|
||||
// Each option is passed to fn as (byteOffset, optionCode, optionData).
|
||||
// Iteration stops early if fn returns [io.EOF]; any other non-nil error is returned directly.
|
||||
// If fn is nil, the function only validates the structure.
|
||||
func (frm Frame) ForEachOption(fn func(off int, code OptCode, data []byte) error) error {
|
||||
buf := frm.buf
|
||||
ptr := OptionsOffset
|
||||
for ptr+4 <= len(buf) {
|
||||
code := OptCode(binary.BigEndian.Uint16(buf[ptr:]))
|
||||
optlen := int(binary.BigEndian.Uint16(buf[ptr+2:]))
|
||||
if ptr+4+optlen > len(buf) {
|
||||
return lneto.ErrInvalidLengthField
|
||||
}
|
||||
if fn != nil {
|
||||
err := fn(ptr, code, buf[ptr+4:ptr+4+optlen])
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
ptr += 4 + optlen
|
||||
}
|
||||
if ptr != len(buf) {
|
||||
// 1–3 trailing bytes that cannot form a valid option header.
|
||||
return lneto.ErrTruncatedFrame
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateSize validates the structure of the options section without invoking a callback.
|
||||
func (frm Frame) ValidateSize() error { return frm.ForEachOption(nil) }
|
||||
@@ -0,0 +1,4 @@
|
||||
package dhcpv6
|
||||
|
||||
// Code generated by "stringer -type=MsgType,ClientState,OptCode,StatusCode,DUIDType -linecomment -output stringers.go"; DO NOT EDIT.
|
||||
// Run go generate ./dhcp/dhcpv6/ to regenerate.
|
||||
Reference in New Issue
Block a user