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:
Pat Whittingslow
2026-05-10 12:16:30 -03:00
committed by GitHub
parent bdbd38ab44
commit 7d5830d7ab
22 changed files with 2785 additions and 75 deletions
+304
View File
@@ -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
}
+244
View File
@@ -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)
}
}
+179
View File
@@ -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],
)
}
+84
View File
@@ -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 13.
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) {
// 13 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) }
+4
View File
@@ -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.