mirror of
https://github.com/soypat/lneto.git
synced 2026-09-10 08:39:30 +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.
|
||||||
+10
-20
@@ -15,7 +15,7 @@ type Client struct {
|
|||||||
lport uint16
|
lport uint16
|
||||||
msg Message
|
msg Message
|
||||||
respFlags HeaderFlags
|
respFlags HeaderFlags
|
||||||
state clientState
|
state StateClientQuery
|
||||||
enableRecursion bool
|
enableRecursion bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ func (c *Client) StartResolve(localPort, txid uint16, cfg ResolveConfig) error {
|
|||||||
if nd > math.MaxUint16 {
|
if nd > math.MaxUint16 {
|
||||||
return lneto.ErrInvalidConfig
|
return lneto.ErrInvalidConfig
|
||||||
}
|
}
|
||||||
c.reset(localPort, txid, dnsSendQuery, cfg.EnableRecursion)
|
c.reset(localPort, txid, CQueryPending, cfg.EnableRecursion)
|
||||||
c.msg.LimitResourceDecoding(uint16(nd), uint16(nd), 0, 0)
|
c.msg.LimitResourceDecoding(uint16(nd), uint16(nd), 0, 0)
|
||||||
c.msg.AddQuestions(cfg.Questions)
|
c.msg.AddQuestions(cfg.Questions)
|
||||||
c.msg.AddAdditionals(cfg.Additional)
|
c.msg.AddAdditionals(cfg.Additional)
|
||||||
@@ -46,7 +46,7 @@ func (c *Client) StartResolve(localPort, txid uint16, cfg ResolveConfig) error {
|
|||||||
func (c *Client) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int) (int, error) {
|
func (c *Client) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int) (int, error) {
|
||||||
if c.isClosed() {
|
if c.isClosed() {
|
||||||
return 0, net.ErrClosed
|
return 0, net.ErrClosed
|
||||||
} else if c.state != dnsSendQuery {
|
} else if c.state != CQueryPending {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ func (c *Client) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int)
|
|||||||
internal.LogAttrs(nil, slog.LevelError, "dns:unexpected-write", slog.Int("got", len(data)), slog.Int("want", int(msglen)))
|
internal.LogAttrs(nil, slog.LevelError, "dns:unexpected-write", slog.Int("got", len(data)), slog.Int("want", int(msglen)))
|
||||||
return 0, lneto.ErrBug
|
return 0, lneto.ErrBug
|
||||||
}
|
}
|
||||||
c.state = dnsAwaitResponse
|
c.state = CQueryOutstanding
|
||||||
// Unset don't frag since DNS requests go through LOTS of nodes.
|
// Unset don't frag since DNS requests go through LOTS of nodes.
|
||||||
// if frameOffset >= 28 {
|
// if frameOffset >= 28 {
|
||||||
// version := carrierData[0] >> 4
|
// version := carrierData[0] >> 4
|
||||||
@@ -78,7 +78,7 @@ func (c *Client) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int)
|
|||||||
func (c *Client) Demux(carrierData []byte, frameOffset int) error {
|
func (c *Client) Demux(carrierData []byte, frameOffset int) error {
|
||||||
if c.isClosed() {
|
if c.isClosed() {
|
||||||
return net.ErrClosed
|
return net.ErrClosed
|
||||||
} else if c.state != dnsAwaitResponse {
|
} else if c.state != CQueryOutstanding {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
frame := carrierData[frameOffset:]
|
frame := carrierData[frameOffset:]
|
||||||
@@ -91,7 +91,7 @@ func (c *Client) Demux(carrierData []byte, frameOffset int) error {
|
|||||||
return nil // Not meant for our client.
|
return nil // Not meant for our client.
|
||||||
}
|
}
|
||||||
c.respFlags = flags
|
c.respFlags = flags
|
||||||
c.state = dnsDone
|
c.state = CQueryDone
|
||||||
msg := &c.msg
|
msg := &c.msg
|
||||||
_, incompleteButOK, err := msg.Decode(frame)
|
_, incompleteButOK, err := msg.Decode(frame)
|
||||||
if err != nil && !incompleteButOK {
|
if err != nil && !incompleteButOK {
|
||||||
@@ -101,7 +101,7 @@ func (c *Client) Demux(carrierData []byte, frameOffset int) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) isClosed() bool {
|
func (c *Client) isClosed() bool {
|
||||||
return c.state == dnsClosed || c.state == dnsAborted
|
return c.state == CQueryIdle || c.state == CQueryAborted
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) MessageCopyTo(dst *Message) (done bool, err error) {
|
func (c *Client) MessageCopyTo(dst *Message) (done bool, err error) {
|
||||||
@@ -117,17 +117,17 @@ func (c *Client) MessageCopyTo(dst *Message) (done bool, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Answers() []Resource {
|
func (c *Client) Answers() []Resource {
|
||||||
if c.state != dnsDone {
|
if c.state != CQueryDone {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return c.msg.Answers
|
return c.msg.Answers
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Abort() {
|
func (c *Client) Abort() {
|
||||||
c.reset(0, 0, 0, false)
|
c.reset(0, 0, CQueryAborted, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) reset(lport, txid uint16, state clientState, enableRecursion bool) {
|
func (c *Client) reset(lport, txid uint16, state StateClientQuery, enableRecursion bool) {
|
||||||
*c = Client{
|
*c = Client{
|
||||||
connID: c.connID + 1,
|
connID: c.connID + 1,
|
||||||
lport: lport,
|
lport: lport,
|
||||||
@@ -138,13 +138,3 @@ func (c *Client) reset(lport, txid uint16, state clientState, enableRecursion bo
|
|||||||
}
|
}
|
||||||
c.msg.Reset()
|
c.msg.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
type clientState uint8
|
|
||||||
|
|
||||||
const (
|
|
||||||
dnsClosed clientState = iota
|
|
||||||
dnsSendQuery
|
|
||||||
dnsAwaitResponse
|
|
||||||
dnsDone
|
|
||||||
dnsAborted
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -238,6 +238,17 @@ const (
|
|||||||
RCodeRefused RCode = 5 // refused
|
RCodeRefused RCode = 5 // refused
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// StateClientQuery is the lifecycle state of a single DNS query.
|
||||||
|
type StateClientQuery uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
CQueryIdle StateClientQuery = iota // no active query (zero value)
|
||||||
|
CQueryPending // query built, not yet transmitted
|
||||||
|
CQueryOutstanding // transmitted; awaiting response (RFC 7766 §9.3)
|
||||||
|
CQueryDone // response received and decoded
|
||||||
|
CQueryAborted // query abandoned (connection error or caller abort)
|
||||||
|
)
|
||||||
|
|
||||||
func b2u8(b bool) uint8 {
|
func b2u8(b bool) uint8 {
|
||||||
if b {
|
if b {
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
+76
-47
@@ -26,6 +26,11 @@ const (
|
|||||||
MaxSizeUDP = 512
|
MaxSizeUDP = 512
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Message is a convenience type for decoding DNS messages and storing results in a single object.
|
||||||
|
// Message is designed for ease of memory reuse. All internal buffers in a Message are reused in methods:
|
||||||
|
// - [Message.Decode]: Limited in decode size by [Message.LimitResourceDecoding] which must be called beforehand.
|
||||||
|
// - [Message.CopyFrom]
|
||||||
|
// - [Message.AddQuestions]
|
||||||
type Message struct {
|
type Message struct {
|
||||||
Questions []Question
|
Questions []Question
|
||||||
Answers []Resource
|
Answers []Resource
|
||||||
@@ -598,8 +603,6 @@ func append32(b []byte, v uint32) []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func visitAllLabels(msg []byte, off uint16, fn func(b []byte), allowCompression bool) (uint16, error) {
|
func visitAllLabels(msg []byte, off uint16, fn func(b []byte), allowCompression bool) (uint16, error) {
|
||||||
// currOff is the current working offset.
|
|
||||||
currOff := off
|
|
||||||
if len(msg) > math.MaxUint16 {
|
if len(msg) > math.MaxUint16 {
|
||||||
return off, errResTooLong
|
return off, errResTooLong
|
||||||
}
|
}
|
||||||
@@ -610,60 +613,86 @@ func visitAllLabels(msg []byte, off uint16, fn func(b []byte), allowCompression
|
|||||||
// the usage of this name.
|
// the usage of this name.
|
||||||
var newOff = off
|
var newOff = off
|
||||||
|
|
||||||
LOOP:
|
|
||||||
for {
|
for {
|
||||||
if currOff >= uint16(len(msg)) {
|
start, end, isPtr, err := NextLabel(msg[off:])
|
||||||
return off, lneto.ErrTruncatedFrame
|
if err != nil {
|
||||||
}
|
return off, err
|
||||||
c := uint16(msg[currOff])
|
} else if start == end {
|
||||||
currOff++
|
|
||||||
switch c & 0xc0 {
|
|
||||||
case 0x00: // String label (segment).
|
|
||||||
if c == 0x00 {
|
|
||||||
break LOOP // Nominal end of name, always ends with null terminator.
|
|
||||||
}
|
|
||||||
endOff := currOff + c
|
|
||||||
if endOff > uint16(len(msg)) {
|
|
||||||
return off, errCalcLen
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reject names containing dots. See issue golang/go#56246
|
|
||||||
if bytes.IndexByte(msg[currOff:endOff], '.') >= 0 {
|
|
||||||
return off, errInvalidName
|
|
||||||
}
|
|
||||||
|
|
||||||
fn(msg[currOff:endOff])
|
|
||||||
currOff = endOff
|
|
||||||
|
|
||||||
case 0xc0: // Pointer.
|
|
||||||
// https://cs.opensource.google/go/x/net/+/refs/tags/v0.19.0:dns/dnsmessage/message.go;l=2078
|
|
||||||
if !allowCompression {
|
|
||||||
return off, errCompressedSRV
|
|
||||||
}
|
|
||||||
if currOff >= uint16(len(msg)) {
|
|
||||||
return off, errInvalidPtr
|
|
||||||
}
|
|
||||||
c1 := msg[currOff]
|
|
||||||
currOff++
|
|
||||||
if ptr == 0 {
|
if ptr == 0 {
|
||||||
newOff = currOff
|
newOff = off + 1 // advance past the null terminator byte
|
||||||
}
|
}
|
||||||
// Don't follow too many pointers, maybe there's a loop.
|
break
|
||||||
if ptr++; ptr > 10 {
|
} else if isPtr {
|
||||||
return off, errTooManyPtr
|
if !allowCompression {
|
||||||
|
return newOff, errCompressedSRV
|
||||||
}
|
}
|
||||||
currOff = (c^0xC0)<<8 | uint16(c1)
|
if ptr == 0 {
|
||||||
default:
|
newOff = off + 2 // next record follows the 2-byte pointer
|
||||||
// Prefixes 0x80 and 0x40 are reserved.
|
}
|
||||||
return off, errReserved
|
off = start
|
||||||
|
if int(off) >= len(msg) {
|
||||||
|
return newOff, errInvalidPtr
|
||||||
|
} else if ptr++; ptr > 10 {
|
||||||
|
return newOff, errTooManyPtr
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Is normal label; start/end are relative to msg[off:].
|
||||||
|
fn(msg[off+start : off+end])
|
||||||
|
off += end
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ptr == 0 {
|
|
||||||
newOff = currOff
|
|
||||||
}
|
|
||||||
return newOff, nil
|
return newOff, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NextLabel parses the first control byte of data and returns the position and extent of next DNS label.
|
||||||
|
//
|
||||||
|
// For a normal string label (RFC 1035 §3.1), isPointer==false and start/end are
|
||||||
|
// byte indices into data: data[start:end] holds the raw label bytes.
|
||||||
|
// A null terminator (c==0) signals the end of the name: start==end==1, err==nil.
|
||||||
|
//
|
||||||
|
// For a compression pointer (RFC 1035 §4.1.4), isPointer==true:
|
||||||
|
// - start is the absolute target offset within the full DNS message to jump to.
|
||||||
|
// - end==0 (sentinel; not a data range).
|
||||||
|
//
|
||||||
|
// Returns [lneto.ErrTruncatedFrame] if data is too short to read the full label or pointer.
|
||||||
|
// Returns errReserved for the 0x40 and 0x80 reserved prefix classes.
|
||||||
|
func NextLabel(data []byte) (start_RelOrAbs, endRel uint16, isAbsPointer bool, err error) {
|
||||||
|
// Default invalid values
|
||||||
|
start_RelOrAbs, endRel = 0, 0
|
||||||
|
if len(data) == 0 {
|
||||||
|
return start_RelOrAbs, endRel, false, lneto.ErrTruncatedFrame
|
||||||
|
}
|
||||||
|
c := uint16(data[0])
|
||||||
|
switch c & 0xc0 {
|
||||||
|
case 0:
|
||||||
|
start_RelOrAbs = 1
|
||||||
|
// String label segment.
|
||||||
|
if c == 0 {
|
||||||
|
return start_RelOrAbs, start_RelOrAbs, false, nil // Null terminator. String ended.
|
||||||
|
}
|
||||||
|
endRel = start_RelOrAbs + c
|
||||||
|
if int(endRel) > len(data) {
|
||||||
|
return start_RelOrAbs, endRel, false, lneto.ErrTruncatedFrame
|
||||||
|
}
|
||||||
|
// Reject names containing dots. See issue golang/go#56246
|
||||||
|
if bytes.IndexByte(data[start_RelOrAbs:endRel], '.') >= 0 {
|
||||||
|
return start_RelOrAbs, endRel, false, errInvalidName
|
||||||
|
}
|
||||||
|
// Correct label!
|
||||||
|
case 0xc0:
|
||||||
|
// Pointer. Start is absolute index in DNS message.
|
||||||
|
isAbsPointer = true
|
||||||
|
if len(data) < 2 {
|
||||||
|
return start_RelOrAbs, endRel, isAbsPointer, lneto.ErrTruncatedFrame // Need more data to fully read pointer.
|
||||||
|
}
|
||||||
|
c1 := uint16(data[1])
|
||||||
|
start_RelOrAbs = (c^0xC0)<<8 | c1
|
||||||
|
default:
|
||||||
|
err = errReserved
|
||||||
|
}
|
||||||
|
return start_RelOrAbs, endRel, isAbsPointer, err
|
||||||
|
}
|
||||||
|
|
||||||
func (dst *Message) CopyFrom(m Message) {
|
func (dst *Message) CopyFrom(m Message) {
|
||||||
internal.SliceReuse(&dst.Questions, len(m.Questions))
|
internal.SliceReuse(&dst.Questions, len(m.Questions))
|
||||||
internal.SliceReuse(&dst.Answers, len(m.Answers))
|
internal.SliceReuse(&dst.Answers, len(m.Answers))
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
package icmpv6
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/soypat/lneto"
|
||||||
|
"github.com/soypat/lneto/internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ lneto.StackNode = (*Client)(nil)
|
||||||
|
|
||||||
|
const (
|
||||||
|
keyHashCompletedBit = 1 << 31
|
||||||
|
keyHashSentBit = 1 << 30
|
||||||
|
keyHashBits = (1 << 30) - 1
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClientConfig struct {
|
||||||
|
// Echo (ping) fields.
|
||||||
|
ResponseQueueBuffer []byte
|
||||||
|
ResponseQueueLimit int
|
||||||
|
HashSeed uint32
|
||||||
|
ID uint16
|
||||||
|
// NDP fields; NDP is disabled when NDPCache == 0.
|
||||||
|
OurAddr [16]byte
|
||||||
|
OurMAC [6]byte
|
||||||
|
NDPCache int
|
||||||
|
}
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
connid uint64
|
||||||
|
magic uint32
|
||||||
|
_seq uint16
|
||||||
|
id uint16
|
||||||
|
|
||||||
|
outgoingEcho []struct {
|
||||||
|
pattern []byte
|
||||||
|
key uint32
|
||||||
|
size uint16
|
||||||
|
raddr [16]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// responseLengths stores the length of responses received.
|
||||||
|
// together they should add up to the written length of responseRing.
|
||||||
|
incomingEcho []struct {
|
||||||
|
length uint16
|
||||||
|
id uint16
|
||||||
|
seq uint16
|
||||||
|
raddr [16]byte
|
||||||
|
}
|
||||||
|
responseRing internal.Ring
|
||||||
|
|
||||||
|
// NDP address resolution fields.
|
||||||
|
ndpCache ndpCache
|
||||||
|
onresolve func(mac [6]byte, addr [16]byte)
|
||||||
|
ourMAC [6]byte
|
||||||
|
ourIP [16]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) Configure(cfg ClientConfig) error {
|
||||||
|
echoOK := cfg.HashSeed != 0 && len(cfg.ResponseQueueBuffer) >= 16 && cfg.ResponseQueueLimit > 0
|
||||||
|
ndpOK := cfg.NDPCache > 0
|
||||||
|
if !echoOK && !ndpOK {
|
||||||
|
return lneto.ErrInvalidConfig
|
||||||
|
}
|
||||||
|
client.connid++
|
||||||
|
if echoOK {
|
||||||
|
internal.SliceReuse(&client.outgoingEcho, cfg.ResponseQueueLimit)
|
||||||
|
internal.SliceReuse(&client.incomingEcho, cfg.ResponseQueueLimit)
|
||||||
|
client.responseRing = internal.Ring{Buf: cfg.ResponseQueueBuffer}
|
||||||
|
client.magic = cfg.HashSeed
|
||||||
|
client.id = cfg.ID
|
||||||
|
}
|
||||||
|
if ndpOK {
|
||||||
|
client.ourIP = cfg.OurAddr
|
||||||
|
client.ourMAC = cfg.OurMAC
|
||||||
|
client.ndpCache.reset(cfg.NDPCache)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) Protocol() uint64 { return uint64(lneto.IPProtoIPv6ICMP) }
|
||||||
|
func (client *Client) LocalPort() uint16 { return 0 }
|
||||||
|
func (client *Client) ConnectionID() *uint64 { return &client.connid }
|
||||||
|
|
||||||
|
func (client *Client) Abort() {
|
||||||
|
client.Reset()
|
||||||
|
client.connid++
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) Reset() {
|
||||||
|
client.incomingEcho = client.incomingEcho[:0]
|
||||||
|
client.outgoingEcho = client.outgoingEcho[:0]
|
||||||
|
client.responseRing.Reset()
|
||||||
|
client.ndpCache.reset(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) Demux(carrierData []byte, frameOffset int) error {
|
||||||
|
rawdata := carrierData[frameOffset:]
|
||||||
|
ifrm, err := NewFrame(rawdata)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tp := ifrm.Type()
|
||||||
|
ipEnabled := frameOffset >= 40
|
||||||
|
var crc lneto.CRC791
|
||||||
|
if ipEnabled {
|
||||||
|
crc.WriteEven(carrierData[8:40]) // IPv6 src(16B)+dst(16B) pseudo-header
|
||||||
|
crc.AddUint32(uint32(len(rawdata)))
|
||||||
|
crc.AddUint32(uint32(lneto.IPProtoIPv6ICMP))
|
||||||
|
}
|
||||||
|
if crc.PayloadSum16(rawdata) != 0 {
|
||||||
|
return lneto.ErrBadCRC
|
||||||
|
}
|
||||||
|
switch tp {
|
||||||
|
case TypeEchoRequest, TypeEchoReply:
|
||||||
|
return client.demuxEcho(carrierData, frameOffset)
|
||||||
|
case TypeNeighborSolicitation, TypeNeighborAdvertisement:
|
||||||
|
return client.demuxNDP(carrierData, frameOffset)
|
||||||
|
default:
|
||||||
|
return lneto.ErrPacketDrop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) Encapsulate(carrierData []byte, ipOffset, frameOffset int) (int, error) {
|
||||||
|
n, dst, err := client.encapsEcho(carrierData, frameOffset)
|
||||||
|
if n == 0 && err == nil {
|
||||||
|
n, dst, err = client.encapsNDP(carrierData, frameOffset)
|
||||||
|
}
|
||||||
|
if n == 0 || err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
ifrm, _ := NewFrame(carrierData[frameOffset : frameOffset+n])
|
||||||
|
ifrm.SetCRC(0)
|
||||||
|
if ipOffset >= 0 {
|
||||||
|
if err = internal.SetIPAddrs(carrierData[ipOffset:], 0, nil, dst[:]); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
var crc lneto.CRC791
|
||||||
|
crc.WriteEven(carrierData[ipOffset+8 : ipOffset+40])
|
||||||
|
crc.AddUint32(uint32(n))
|
||||||
|
crc.AddUint32(uint32(lneto.IPProtoIPv6ICMP))
|
||||||
|
ifrm.SetCRC(crc.PayloadSum16(carrierData[frameOffset : frameOffset+n]))
|
||||||
|
} else {
|
||||||
|
var crc lneto.CRC791
|
||||||
|
ifrm.SetCRC(crc.PayloadSum16(carrierData[frameOffset : frameOffset+n]))
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NDP public API — mirrors NDPHandler but on the unified Client.
|
||||||
|
|
||||||
|
func (client *Client) SetNDPResolveCallback(cb func(mac [6]byte, addr [16]byte)) {
|
||||||
|
client.onresolve = cb
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) NDPStartQuery(addr [16]byte, triggerCallback bool) error {
|
||||||
|
if addr == ([16]byte{}) {
|
||||||
|
return lneto.ErrZeroDestination
|
||||||
|
}
|
||||||
|
e := client.ndpCache.acquireNext()
|
||||||
|
e.use([6]byte{}, addr, ndpFlagIncomplete|ndpFlagIncompletePendingQuery|ndpFlagPriority)
|
||||||
|
if triggerCallback {
|
||||||
|
e.flags |= ndpFlagResolveTriggersCallback
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) NDPCacheLookup(addr [16]byte) ([6]byte, error) {
|
||||||
|
e := client.ndpCache.Lookup(addr)
|
||||||
|
if e == nil {
|
||||||
|
return [6]byte{}, errNDPQueryNotFound
|
||||||
|
} else if e.flags.hasAny(ndpFlagIncomplete) {
|
||||||
|
return [6]byte{}, errNDPQueryPending
|
||||||
|
}
|
||||||
|
return e.mac, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) NDPCacheSeed(addr [16]byte, mac [6]byte) error {
|
||||||
|
if addr == ([16]byte{}) {
|
||||||
|
return lneto.ErrZeroDestination
|
||||||
|
}
|
||||||
|
e := client.ndpCache.acquireNext()
|
||||||
|
e.use(mac, addr, 0)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) NDPCacheRemove(addr [16]byte) error {
|
||||||
|
e := client.ndpCache.Lookup(addr)
|
||||||
|
if e == nil {
|
||||||
|
return errNDPQueryNotFound
|
||||||
|
}
|
||||||
|
e.destroy()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package icmpv6
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/soypat/lneto"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ndpOptSourceLinkAddr = 1 // RFC 4861 §4.6.1
|
||||||
|
ndpOptTargetLinkAddr = 2 // RFC 4861 §4.6.2
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errNDPQueryPending = errors.New("icmpv6: NDP query pending")
|
||||||
|
errNDPQueryNotFound = errors.New("icmpv6: NDP query not found")
|
||||||
|
)
|
||||||
|
|
||||||
|
func (client *Client) demuxNDP(carrierData []byte, frameOffset int) error {
|
||||||
|
rawdata := carrierData[frameOffset:]
|
||||||
|
if len(rawdata) < sizeNDPBase {
|
||||||
|
return lneto.ErrTruncatedFrame
|
||||||
|
}
|
||||||
|
ifrm, _ := NewFrame(rawdata) // already validated by Demux
|
||||||
|
tp := ifrm.Type()
|
||||||
|
targetAddr := (*[16]byte)(rawdata[8:24])
|
||||||
|
options := rawdata[24:]
|
||||||
|
ipEnabled := frameOffset >= 40
|
||||||
|
switch tp {
|
||||||
|
case TypeNeighborSolicitation:
|
||||||
|
if *targetAddr != client.ourIP {
|
||||||
|
return nil // Not for us.
|
||||||
|
}
|
||||||
|
mac, ok := parseLinkLayerOption(options, ndpOptSourceLinkAddr)
|
||||||
|
if !ok {
|
||||||
|
return lneto.ErrPacketDrop
|
||||||
|
}
|
||||||
|
var senderAddr [16]byte
|
||||||
|
if ipEnabled {
|
||||||
|
copy(senderAddr[:], carrierData[8:24]) // IPv6 source address
|
||||||
|
}
|
||||||
|
e := client.ndpCache.acquireNext()
|
||||||
|
e.use(mac, senderAddr, ndpFlagPendingResponse)
|
||||||
|
|
||||||
|
case TypeNeighborAdvertisement:
|
||||||
|
mac, ok := parseLinkLayerOption(options, ndpOptTargetLinkAddr)
|
||||||
|
if !ok {
|
||||||
|
return lneto.ErrPacketDrop
|
||||||
|
}
|
||||||
|
e := client.ndpCache.Lookup(*targetAddr)
|
||||||
|
if e == nil {
|
||||||
|
return nil // Unsolicited or already evicted.
|
||||||
|
}
|
||||||
|
e.mac = mac
|
||||||
|
e.flags &^= ndpFlagIncomplete | ndpFlagIncompletePendingQuery
|
||||||
|
if e.flags.hasAny(ndpFlagResolveTriggersCallback) && client.onresolve != nil {
|
||||||
|
client.onresolve(mac, *targetAddr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) encapsNDP(carrierData []byte, frameOffset int) (n int, dst [16]byte, err error) {
|
||||||
|
buf := carrierData[frameOffset:]
|
||||||
|
if len(buf) < sizeNDP {
|
||||||
|
return 0, [16]byte{}, lneto.ErrShortBuffer
|
||||||
|
}
|
||||||
|
tp := TypeNeighborAdvertisement
|
||||||
|
e := client.ndpCache.getNextFlagged(ndpFlagPendingResponse) // Prioritize responses.
|
||||||
|
if e == nil {
|
||||||
|
e = client.ndpCache.getNextFlagged(ndpFlagIncompletePendingQuery)
|
||||||
|
if e == nil {
|
||||||
|
return 0, [16]byte{}, nil
|
||||||
|
}
|
||||||
|
e.flags &^= ndpFlagIncompletePendingQuery
|
||||||
|
tp = TypeNeighborSolicitation
|
||||||
|
} else {
|
||||||
|
e.flags &^= ndpFlagPendingResponse
|
||||||
|
}
|
||||||
|
n, err = e.put(buf, client.ourIP, client.ourMAC, tp)
|
||||||
|
if err != nil {
|
||||||
|
return 0, [16]byte{}, err
|
||||||
|
}
|
||||||
|
switch tp {
|
||||||
|
case TypeNeighborSolicitation:
|
||||||
|
dst = solicitedNodeMulticast(e.addr)
|
||||||
|
case TypeNeighborAdvertisement:
|
||||||
|
dst = e.addr
|
||||||
|
}
|
||||||
|
return n, dst, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// solicitedNodeMulticast returns the solicited-node multicast address for addr (RFC 4291 §2.7.1).
|
||||||
|
func solicitedNodeMulticast(addr [16]byte) [16]byte {
|
||||||
|
return [16]byte{
|
||||||
|
0xff, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0xff,
|
||||||
|
addr[13], addr[14], addr[15],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseLinkLayerOption scans NDP options for the first option of optType
|
||||||
|
// and returns the embedded 6-byte Ethernet address.
|
||||||
|
func parseLinkLayerOption(options []byte, optType byte) ([6]byte, bool) {
|
||||||
|
for len(options) >= sizeNDPOption {
|
||||||
|
t := options[0]
|
||||||
|
l := int(options[1]) * 8
|
||||||
|
if l == 0 || l > len(options) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if t == optType && l >= sizeNDPOption {
|
||||||
|
return [6]byte(options[2:8]), true
|
||||||
|
}
|
||||||
|
options = options[l:]
|
||||||
|
}
|
||||||
|
return [6]byte{}, false
|
||||||
|
}
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
package icmpv6
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/soypat/lneto"
|
||||||
|
"github.com/soypat/lneto/internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (client *Client) PingIncomingCapacity() int {
|
||||||
|
return cap(client.incomingEcho)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) PingStart(remoteAddr [16]byte, pattern []byte, size uint16) (key uint32, err error) {
|
||||||
|
if int(size) < len(pattern) || len(pattern) == 0 {
|
||||||
|
return 0, lneto.ErrInvalidConfig
|
||||||
|
} else if remoteAddr == [16]byte{} {
|
||||||
|
return 0, lneto.ErrZeroDestination
|
||||||
|
}
|
||||||
|
free := cap(client.outgoingEcho) - len(client.outgoingEcho)
|
||||||
|
if free == 0 {
|
||||||
|
return 0, lneto.ErrExhausted
|
||||||
|
}
|
||||||
|
key = client.magichash(pattern, int(size)) & keyHashBits
|
||||||
|
v := internal.SliceReclaim(&client.outgoingEcho)
|
||||||
|
v.key = key
|
||||||
|
v.size = size
|
||||||
|
v.pattern = append(v.pattern[:0], pattern...)
|
||||||
|
v.raddr = remoteAddr
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) PingPeek(key uint32) (completed, ok bool) {
|
||||||
|
idx := client.pingidx(key)
|
||||||
|
if idx >= 0 {
|
||||||
|
return client.outgoingEcho[idx].key&keyHashCompletedBit != 0, true
|
||||||
|
}
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) PingPop(key uint32) (completed, ok bool) {
|
||||||
|
idx := client.pingidx(key)
|
||||||
|
if idx >= 0 {
|
||||||
|
completed := client.outgoingEcho[idx].key&keyHashCompletedBit != 0
|
||||||
|
client.outgoingEcho = slices.Delete(client.outgoingEcho, idx, idx+1)
|
||||||
|
return completed, true
|
||||||
|
}
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) pingidx(key uint32) int {
|
||||||
|
for i := range client.outgoingEcho {
|
||||||
|
if client.outgoingEcho[i].key&keyHashBits == key {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) seq() uint16 {
|
||||||
|
client._seq++
|
||||||
|
return client._seq
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) magichash(pattern []byte, size int) (hash uint32) {
|
||||||
|
hash = client.magic
|
||||||
|
i := 0
|
||||||
|
n := size / len(pattern)
|
||||||
|
for i < n {
|
||||||
|
for _, b := range pattern {
|
||||||
|
hash = hash*31 + uint32(b)
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
n = size % len(pattern)
|
||||||
|
for i = 0; i < n; i++ {
|
||||||
|
hash = hash*31 + uint32(pattern[i])
|
||||||
|
}
|
||||||
|
return hash
|
||||||
|
}
|
||||||
|
|
||||||
|
// demuxEcho handles TypeEchoRequest and TypeEchoReply frames.
|
||||||
|
// CRC has already been verified by the caller (Client.Demux).
|
||||||
|
func (client *Client) demuxEcho(carrierData []byte, frameOffset int) error {
|
||||||
|
rawdata := carrierData[frameOffset:]
|
||||||
|
ifrm, _ := NewFrame(rawdata) // already validated by Demux
|
||||||
|
tp := ifrm.Type()
|
||||||
|
ipEnabled := frameOffset >= 40
|
||||||
|
var raddr [16]byte
|
||||||
|
if ipEnabled {
|
||||||
|
src, _, _, _, _ := internal.GetIPAddr(carrierData)
|
||||||
|
if len(src) == 16 {
|
||||||
|
raddr = [16]byte(src)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
switch tp {
|
||||||
|
case TypeEchoRequest:
|
||||||
|
free := cap(client.incomingEcho) - len(client.incomingEcho)
|
||||||
|
if free == 0 {
|
||||||
|
return lneto.ErrExhausted
|
||||||
|
}
|
||||||
|
efrm := FrameEcho{Frame: ifrm}
|
||||||
|
data := efrm.Data()
|
||||||
|
if len(data) == 0 {
|
||||||
|
return lneto.ErrPacketDrop
|
||||||
|
}
|
||||||
|
n, werr := client.responseRing.Write(data)
|
||||||
|
if werr != nil {
|
||||||
|
err = werr
|
||||||
|
break
|
||||||
|
}
|
||||||
|
v := internal.SliceReclaim(&client.incomingEcho)
|
||||||
|
v.length = uint16(n)
|
||||||
|
v.id = efrm.Identifier()
|
||||||
|
v.seq = efrm.SequenceNumber()
|
||||||
|
v.raddr = raddr
|
||||||
|
|
||||||
|
case TypeEchoReply:
|
||||||
|
efrm := FrameEcho{Frame: ifrm}
|
||||||
|
data := efrm.Data()
|
||||||
|
if len(data) == 0 {
|
||||||
|
return lneto.ErrPacketDrop
|
||||||
|
}
|
||||||
|
hash := client.magichash(data, len(data)) & keyHashBits
|
||||||
|
idx := client.pingidx(hash)
|
||||||
|
if idx < 0 || (ipEnabled && client.outgoingEcho[idx].raddr != raddr) {
|
||||||
|
err = lneto.ErrPacketDrop
|
||||||
|
break
|
||||||
|
}
|
||||||
|
client.outgoingEcho[idx].key |= keyHashCompletedBit
|
||||||
|
|
||||||
|
default:
|
||||||
|
err = lneto.ErrPacketDrop
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// encapsEcho writes an echo reply or request frame into carrierData[frameOffset:].
|
||||||
|
// CRC and SetIPAddrs are handled by the caller (Client.Encapsulate).
|
||||||
|
func (client *Client) encapsEcho(carrierData []byte, frameOffset int) (n int, dst [16]byte, err error) {
|
||||||
|
if len(client.incomingEcho) == 0 && len(client.outgoingEcho) == 0 {
|
||||||
|
return 0, [16]byte{}, nil
|
||||||
|
}
|
||||||
|
ifrm, err := NewFrame(carrierData[frameOffset:])
|
||||||
|
if err != nil {
|
||||||
|
return 0, [16]byte{}, err
|
||||||
|
}
|
||||||
|
if len(client.incomingEcho) > 0 {
|
||||||
|
// Priority: send echo reply.
|
||||||
|
inc := client.incomingEcho[0]
|
||||||
|
efrm := FrameEcho{Frame: ifrm}
|
||||||
|
efrm.SetType(TypeEchoReply)
|
||||||
|
efrm.SetIdentifier(inc.id)
|
||||||
|
efrm.SetSequenceNumber(inc.seq)
|
||||||
|
dataLen := int(inc.length)
|
||||||
|
_, rerr := client.responseRing.Read(efrm.Data()[:dataLen])
|
||||||
|
if rerr != nil {
|
||||||
|
return 0, [16]byte{}, rerr
|
||||||
|
}
|
||||||
|
client.incomingEcho = slices.Delete(client.incomingEcho, 0, 1)
|
||||||
|
n = sizeHeader + dataLen
|
||||||
|
dst = inc.raddr
|
||||||
|
} else {
|
||||||
|
idx := 0
|
||||||
|
for idx < len(client.outgoingEcho) && client.outgoingEcho[idx].key&keyHashSentBit != 0 {
|
||||||
|
idx++
|
||||||
|
}
|
||||||
|
if idx >= len(client.outgoingEcho) {
|
||||||
|
return 0, [16]byte{}, nil // No pending packet to send.
|
||||||
|
}
|
||||||
|
out := &client.outgoingEcho[idx]
|
||||||
|
efrm := FrameEcho{Frame: ifrm}
|
||||||
|
efrm.SetType(TypeEchoRequest)
|
||||||
|
efrm.SetIdentifier(client.id)
|
||||||
|
efrm.SetSequenceNumber(client.seq())
|
||||||
|
pattern := out.pattern
|
||||||
|
data := efrm.Data()
|
||||||
|
size := int(out.size)
|
||||||
|
written := 0
|
||||||
|
for written+len(pattern) <= size && written+len(pattern) <= len(data) {
|
||||||
|
copy(data[written:], pattern)
|
||||||
|
written += len(pattern)
|
||||||
|
}
|
||||||
|
copy(data[written:written+size%len(pattern)], pattern)
|
||||||
|
n = sizeHeader + size
|
||||||
|
out.key |= keyHashSentBit
|
||||||
|
dst = out.raddr
|
||||||
|
}
|
||||||
|
ifrm.buf = carrierData[frameOffset : frameOffset+n]
|
||||||
|
ifrm.SetCode(0)
|
||||||
|
return n, dst, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
package icmpv6
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/soypat/lneto/internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
testHashSeed = 0xdeadbeef
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClients(t *testing.T) {
|
||||||
|
const sizebuffer = 64
|
||||||
|
const queuesize = 2
|
||||||
|
var sender, responder Client
|
||||||
|
err := sender.Configure(ClientConfig{
|
||||||
|
ResponseQueueBuffer: make([]byte, sizebuffer),
|
||||||
|
ResponseQueueLimit: queuesize,
|
||||||
|
HashSeed: testHashSeed,
|
||||||
|
ID: 1,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
err = responder.Configure(ClientConfig{
|
||||||
|
ResponseQueueBuffer: make([]byte, sizebuffer),
|
||||||
|
ResponseQueueLimit: queuesize,
|
||||||
|
HashSeed: testHashSeed,
|
||||||
|
ID: 2,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
pattern := []byte("ab12")
|
||||||
|
size := 8
|
||||||
|
var buf [64]byte
|
||||||
|
key1 := testSingleExchange(t, &sender, &responder, buf[:], pattern, uint16(size))
|
||||||
|
completed, ok := sender.PingPop(key1)
|
||||||
|
if !completed || !ok {
|
||||||
|
t.Fatal("ping did not complete or not exist")
|
||||||
|
}
|
||||||
|
n, err := sender.Encapsulate(buf[:], -1, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
} else if n > 0 {
|
||||||
|
t.Error("sender: expected no more data to be sent")
|
||||||
|
}
|
||||||
|
n, err = responder.Encapsulate(buf[:], -1, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
} else if n > 0 {
|
||||||
|
t.Error("responder: expected no more data to be sent")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSingleExchange(t *testing.T, sender, responder *Client, buf []byte, pattern []byte, size uint16) (senderKey uint32) {
|
||||||
|
const frameOff = 0
|
||||||
|
const ipOff = -1
|
||||||
|
n, err := responder.Encapsulate(buf, ipOff, frameOff)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if n > 0 {
|
||||||
|
t.Fatal("expected no data pending to be sent from responder")
|
||||||
|
}
|
||||||
|
senderKey, n = testSendEcho(t, sender, buf, pattern, size)
|
||||||
|
completed, ok := sender.PingPeek(senderKey)
|
||||||
|
if !ok {
|
||||||
|
t.Error("ping key not exist")
|
||||||
|
} else if completed {
|
||||||
|
t.Error("ping completed before response")
|
||||||
|
}
|
||||||
|
ifrm, _ := NewFrame(buf[frameOff : frameOff+n])
|
||||||
|
efrm := FrameEcho{Frame: ifrm}
|
||||||
|
id, seq := efrm.Identifier(), efrm.SequenceNumber()
|
||||||
|
err1 := responder.Demux(buf[:frameOff+n], frameOff)
|
||||||
|
if err1 != nil {
|
||||||
|
t.Error("responder demux during single", err1)
|
||||||
|
}
|
||||||
|
n, err = responder.Encapsulate(buf, ipOff, frameOff)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("responder encaps during single", err)
|
||||||
|
return
|
||||||
|
} else if n == 0 && err1 == nil {
|
||||||
|
t.Error("responder wrote no data")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ifrm, err = NewFrame(buf[frameOff : frameOff+n])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if ifrm.Type() != TypeEchoReply {
|
||||||
|
t.Fatalf("expected echo reply %d", ifrm.Type())
|
||||||
|
}
|
||||||
|
efrm = FrameEcho{Frame: ifrm}
|
||||||
|
if efrm.Identifier() != id {
|
||||||
|
t.Error("mismatched identifier want/got:", id, efrm.Identifier())
|
||||||
|
}
|
||||||
|
if efrm.SequenceNumber() != seq {
|
||||||
|
t.Error("mismatched sequence number want/got:", seq, efrm.SequenceNumber())
|
||||||
|
}
|
||||||
|
data := efrm.Data()
|
||||||
|
testPatternMatch(t, data, pattern, int(size))
|
||||||
|
err = sender.Demux(buf[:frameOff+n], frameOff)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("sender demuxed response", err)
|
||||||
|
}
|
||||||
|
completed, ok = sender.PingPeek(senderKey)
|
||||||
|
if !completed {
|
||||||
|
t.Error("expected ping to have completed")
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
t.Error("ping key not exist after completion")
|
||||||
|
}
|
||||||
|
if completed2, ok2 := sender.PingPeek(senderKey); completed != completed2 || ok != ok2 {
|
||||||
|
t.Error("change in status after peek")
|
||||||
|
}
|
||||||
|
n, err = sender.Encapsulate(buf, ipOff, frameOff)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("error after done")
|
||||||
|
}
|
||||||
|
if n > 0 {
|
||||||
|
t.Error("expected no data to be sent after ping completion", n)
|
||||||
|
}
|
||||||
|
return senderKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSendEcho(t *testing.T, sender *Client, buf []byte, pattern []byte, size uint16) (key uint32, n int) {
|
||||||
|
t.Helper()
|
||||||
|
const frameOff = 0
|
||||||
|
const ipOff = -1
|
||||||
|
n, err := sender.Encapsulate(buf, ipOff, frameOff)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if n > 0 {
|
||||||
|
t.Fatal("expected no data pending to send on testSendEcho start")
|
||||||
|
}
|
||||||
|
key, err = sender.PingStart([16]byte{1}, pattern, size)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err = sender.Encapsulate(buf[:], ipOff, frameOff)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("sender encapsulate: %v", err)
|
||||||
|
}
|
||||||
|
ifrm, err := NewFrame(buf[:n])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err) // only fails in short frame case.
|
||||||
|
}
|
||||||
|
if ifrm.Type() != TypeEchoRequest {
|
||||||
|
t.Errorf("not echo request type on send: %d", ifrm.Type())
|
||||||
|
}
|
||||||
|
efrm := FrameEcho{Frame: ifrm}
|
||||||
|
data := efrm.Data()
|
||||||
|
testPatternMatch(t, data, pattern, int(size))
|
||||||
|
return key, n
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPatternMatch(t *testing.T, data []byte, pattern []byte, size int) {
|
||||||
|
t.Helper()
|
||||||
|
if len(data) != size {
|
||||||
|
t.Errorf("pattern size mismatch, want %d, got %d", size, len(data))
|
||||||
|
}
|
||||||
|
for i := 0; i < size; i += len(pattern) {
|
||||||
|
got := data[i:min(len(data), i+len(pattern))]
|
||||||
|
want := pattern[:len(got)]
|
||||||
|
if !internal.BytesEqual(got, want) {
|
||||||
|
t.Errorf("pattern data mismatch at %d, got %s, want %s", i, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
package icmpv6
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
|
||||||
|
"github.com/soypat/lneto"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:generate stringer -type=Type,CodeDestinationUnreachable,CodeParameterProblem -linecomment -output stringers.go
|
||||||
|
|
||||||
|
const (
|
||||||
|
sizeHeader = 8
|
||||||
|
sizeNDPBase = sizeHeader + 16 // 24: ICMPv6 header + 16-byte target address
|
||||||
|
sizeNDPOption = 8 // 1 type + 1 len + 6 MAC (Ethernet link-layer option, RFC 4861 §4.6.1)
|
||||||
|
sizeNDP = sizeNDPBase + sizeNDPOption
|
||||||
|
)
|
||||||
|
|
||||||
|
type Type uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
TypeDestinationUnreachable Type = 1 // destination unreachable
|
||||||
|
TypePacketTooBig Type = 2 // packet too big
|
||||||
|
TypeTimeExceeded Type = 3 // time exceeded
|
||||||
|
TypeParameterProblem Type = 4 // parameter problem
|
||||||
|
|
||||||
|
TypeEchoRequest Type = 128 // echo request
|
||||||
|
TypeEchoReply Type = 129 // echo reply
|
||||||
|
|
||||||
|
TypeRouterSolicitation Type = 133 // router solicitation
|
||||||
|
TypeRouterAdvertisement Type = 134 // router advertisement
|
||||||
|
TypeNeighborSolicitation Type = 135 // neighbor solicitation
|
||||||
|
TypeNeighborAdvertisement Type = 136 // neighbor advertisement
|
||||||
|
TypeRedirectMessage Type = 137 // redirect message
|
||||||
|
)
|
||||||
|
|
||||||
|
type CodeTimeExceeded uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
CodeHopLimitExceeded CodeTimeExceeded = iota // hop limit exceeded in transit
|
||||||
|
CodeFragmentReassembly // fragment reassembly time exceeded
|
||||||
|
)
|
||||||
|
|
||||||
|
type CodeDestinationUnreachable uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
CodeNoRoute CodeDestinationUnreachable = iota // no route to destination
|
||||||
|
CodeAdminProhibited // communication administratively prohibited
|
||||||
|
CodeBeyondScope // beyond scope of source address
|
||||||
|
CodeAddressUnreachable // address unreachable
|
||||||
|
CodePortUnreachable // port unreachable
|
||||||
|
CodeIngressEgressPolicy // source address failed ingress/egress policy
|
||||||
|
CodeRejectRoute // reject route to destination
|
||||||
|
)
|
||||||
|
|
||||||
|
type CodeParameterProblem uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
CodeErroneousHeaderField CodeParameterProblem = iota // erroneous header field encountered
|
||||||
|
CodeUnrecognizedNextHeader // unrecognized next header type encountered
|
||||||
|
CodeUnrecognizedIPv6Option // unrecognized IPv6 option encountered
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewFrame(buf []byte) (Frame, error) {
|
||||||
|
if len(buf) < sizeHeader {
|
||||||
|
return Frame{}, lneto.ErrTruncatedFrame
|
||||||
|
}
|
||||||
|
return Frame{buf: buf}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Frame struct {
|
||||||
|
buf []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (frm Frame) Type() Type { return Type(frm.buf[0]) }
|
||||||
|
|
||||||
|
func (frm Frame) SetType(t Type) { frm.buf[0] = uint8(t) }
|
||||||
|
|
||||||
|
func (frm Frame) Code() uint8 { return frm.buf[1] }
|
||||||
|
|
||||||
|
func (frm Frame) SetCode(code uint8) { frm.buf[1] = code }
|
||||||
|
|
||||||
|
// CRC returns the checksum field of the frame.
|
||||||
|
func (frm Frame) CRC() uint16 {
|
||||||
|
return binary.BigEndian.Uint16(frm.buf[2:4])
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCRC sets the checksum field of the frame.
|
||||||
|
func (frm Frame) SetCRC(crc uint16) {
|
||||||
|
binary.BigEndian.PutUint16(frm.buf[2:4], crc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (frm Frame) payload() []byte {
|
||||||
|
return frm.buf[4:]
|
||||||
|
}
|
||||||
|
|
||||||
|
type FrameDestinationUnreachable struct {
|
||||||
|
Frame
|
||||||
|
}
|
||||||
|
|
||||||
|
func (frm FrameDestinationUnreachable) Code() CodeDestinationUnreachable {
|
||||||
|
return CodeDestinationUnreachable(frm.Frame.Code())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (frm FrameDestinationUnreachable) SetCode(code CodeDestinationUnreachable) {
|
||||||
|
frm.Frame.SetCode(uint8(code))
|
||||||
|
}
|
||||||
|
|
||||||
|
type FramePacketTooBig struct {
|
||||||
|
Frame
|
||||||
|
}
|
||||||
|
|
||||||
|
func (frm FramePacketTooBig) MTU() uint32 {
|
||||||
|
return binary.BigEndian.Uint32(frm.buf[4:8])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (frm FramePacketTooBig) SetMTU(mtu uint32) {
|
||||||
|
binary.BigEndian.PutUint32(frm.buf[4:8], mtu)
|
||||||
|
}
|
||||||
|
|
||||||
|
type FrameParameterProblem struct {
|
||||||
|
Frame
|
||||||
|
}
|
||||||
|
|
||||||
|
func (frm FrameParameterProblem) Code() CodeParameterProblem {
|
||||||
|
return CodeParameterProblem(frm.Frame.Code())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (frm FrameParameterProblem) SetCode(code CodeParameterProblem) {
|
||||||
|
frm.Frame.SetCode(uint8(code))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pointer identifies the octet offset within the invoking packet where the error was detected.
|
||||||
|
func (frm FrameParameterProblem) Pointer() uint32 {
|
||||||
|
return binary.BigEndian.Uint32(frm.buf[4:8])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (frm FrameParameterProblem) SetPointer(ptr uint32) {
|
||||||
|
binary.BigEndian.PutUint32(frm.buf[4:8], ptr)
|
||||||
|
}
|
||||||
|
|
||||||
|
type FrameEcho struct {
|
||||||
|
Frame
|
||||||
|
}
|
||||||
|
|
||||||
|
func (frm FrameEcho) Identifier() uint16 {
|
||||||
|
return binary.BigEndian.Uint16(frm.buf[4:6])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (frm FrameEcho) SetIdentifier(id uint16) {
|
||||||
|
binary.BigEndian.PutUint16(frm.buf[4:6], id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (frm FrameEcho) SequenceNumber() uint16 {
|
||||||
|
return binary.BigEndian.Uint16(frm.buf[6:8])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (frm FrameEcho) SetSequenceNumber(seq uint16) {
|
||||||
|
binary.BigEndian.PutUint16(frm.buf[6:8], seq)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (frm FrameEcho) Data() []byte {
|
||||||
|
return frm.buf[8:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (frm FrameEcho) RawData() []byte {
|
||||||
|
return frm.buf
|
||||||
|
}
|
||||||
|
|
||||||
|
// FrameNeighborSolicitation accesses a Neighbor Solicitation message (RFC 4861 §4.3).
|
||||||
|
// Layout after ICMPv6 base header: Reserved(4B) | TargetAddr(16B) | Options.
|
||||||
|
type FrameNeighborSolicitation struct {
|
||||||
|
Frame
|
||||||
|
}
|
||||||
|
|
||||||
|
// TargetAddr returns the IPv6 address being queried.
|
||||||
|
func (frm FrameNeighborSolicitation) TargetAddr() *[16]byte {
|
||||||
|
return (*[16]byte)(frm.buf[8:24])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options returns the bytes following the fixed header for parsing NDP options.
|
||||||
|
func (frm FrameNeighborSolicitation) Options() []byte {
|
||||||
|
return frm.buf[24:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// FrameNeighborAdvertisement accesses a Neighbor Advertisement message (RFC 4861 §4.4).
|
||||||
|
// Layout after ICMPv6 base header: R|S|O|Reserved(4B) | TargetAddr(16B) | Options.
|
||||||
|
type FrameNeighborAdvertisement struct {
|
||||||
|
Frame
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flags returns the R (router), S (solicited), O (override) flag bits.
|
||||||
|
func (frm FrameNeighborAdvertisement) Flags() (router, solicited, override bool) {
|
||||||
|
b := frm.buf[4]
|
||||||
|
return b&0x80 != 0, b&0x40 != 0, b&0x20 != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFlags sets the R, S, O flags and zeroes the reserved bits.
|
||||||
|
func (frm FrameNeighborAdvertisement) SetFlags(router, solicited, override bool) {
|
||||||
|
var b byte
|
||||||
|
if router {
|
||||||
|
b |= 0x80
|
||||||
|
}
|
||||||
|
if solicited {
|
||||||
|
b |= 0x40
|
||||||
|
}
|
||||||
|
if override {
|
||||||
|
b |= 0x20
|
||||||
|
}
|
||||||
|
frm.buf[4] = b
|
||||||
|
frm.buf[5] = 0
|
||||||
|
frm.buf[6] = 0
|
||||||
|
frm.buf[7] = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// TargetAddr returns the IPv6 address being announced.
|
||||||
|
func (frm FrameNeighborAdvertisement) TargetAddr() *[16]byte {
|
||||||
|
return (*[16]byte)(frm.buf[8:24])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options returns the bytes following the fixed header for parsing NDP options.
|
||||||
|
func (frm FrameNeighborAdvertisement) Options() []byte {
|
||||||
|
return frm.buf[24:]
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package icmpv6
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/soypat/lneto"
|
||||||
|
"github.com/soypat/lneto/internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ndpCache struct {
|
||||||
|
entries []ndpEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
// ndpEntry maps an IPv6 address to a MAC. Designed for compactness: 24 bytes.
|
||||||
|
type ndpEntry struct {
|
||||||
|
addr [16]byte
|
||||||
|
mac [6]byte
|
||||||
|
age uint8
|
||||||
|
flags ndpFlags
|
||||||
|
}
|
||||||
|
|
||||||
|
type ndpFlags uint8
|
||||||
|
|
||||||
|
// unset ndpFlagInUse to signal the entry can be acquired for a new query.
|
||||||
|
const (
|
||||||
|
ndpFlagInUse ndpFlags = 1 << iota
|
||||||
|
ndpFlagPendingResponse // received a NS for our addr; must send NA
|
||||||
|
ndpFlagIncomplete // query in flight; MAC not yet resolved
|
||||||
|
ndpFlagIncompletePendingQuery // NS not yet transmitted
|
||||||
|
ndpFlagPriority // user query; evicted last
|
||||||
|
ndpFlagResolveTriggersCallback // call onresolve when MAC is learned
|
||||||
|
)
|
||||||
|
|
||||||
|
func (f ndpFlags) hasAny(bits ndpFlags) bool { return f&bits != 0 }
|
||||||
|
|
||||||
|
func (e *ndpEntry) use(mac [6]byte, addr [16]byte, flags ndpFlags) {
|
||||||
|
e.flags = ndpFlagInUse | flags
|
||||||
|
e.addr = addr
|
||||||
|
e.age = 0
|
||||||
|
e.mac = mac
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ndpEntry) destroy() { *e = ndpEntry{} }
|
||||||
|
|
||||||
|
func (e *ndpEntry) put(buf []byte, ourAddr [16]byte, ourMAC [6]byte, tp Type) (int, error) {
|
||||||
|
if len(buf) < sizeNDP {
|
||||||
|
return 0, lneto.ErrShortBuffer
|
||||||
|
}
|
||||||
|
buf[0] = uint8(tp)
|
||||||
|
buf[1] = 0
|
||||||
|
buf[2], buf[3] = 0, 0 // checksum zeroed; caller computes it
|
||||||
|
switch tp {
|
||||||
|
case TypeNeighborSolicitation:
|
||||||
|
buf[4], buf[5], buf[6], buf[7] = 0, 0, 0, 0
|
||||||
|
copy(buf[8:24], e.addr[:]) // target = address being queried
|
||||||
|
buf[24] = ndpOptSourceLinkAddr
|
||||||
|
buf[25] = 1 // length in units of 8 bytes
|
||||||
|
copy(buf[26:32], ourMAC[:])
|
||||||
|
case TypeNeighborAdvertisement:
|
||||||
|
buf[4] = 0x60 // S=1 (solicited), O=1 (override)
|
||||||
|
buf[5], buf[6], buf[7] = 0, 0, 0
|
||||||
|
copy(buf[8:24], ourAddr[:]) // target = our address being announced
|
||||||
|
buf[24] = ndpOptTargetLinkAddr
|
||||||
|
buf[25] = 1
|
||||||
|
copy(buf[26:32], ourMAC[:])
|
||||||
|
default:
|
||||||
|
return 0, lneto.ErrUnsupported
|
||||||
|
}
|
||||||
|
return sizeNDP, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ndpCache) ageEntries() {
|
||||||
|
for i := range c.entries {
|
||||||
|
if c.entries[i].flags&ndpFlagInUse != 0 && c.entries[i].age < 255 {
|
||||||
|
c.entries[i].age++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ndpCache) reset(maxLimit int) {
|
||||||
|
internal.SliceReuse(&c.entries, maxLimit)
|
||||||
|
c.entries = c.entries[:cap(c.entries)]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ndpCache) getNextFlagged(flags ndpFlags) *ndpEntry {
|
||||||
|
for i := range c.entries {
|
||||||
|
f := c.entries[i].flags
|
||||||
|
if f&ndpFlagInUse != 0 && f.hasAny(flags) {
|
||||||
|
return &c.entries[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ndpCache) clearFlags(entryHasFlags, clrTheseFlagsIfMatch ndpFlags) {
|
||||||
|
for i := range c.entries {
|
||||||
|
if c.entries[i].flags&entryHasFlags != 0 {
|
||||||
|
c.entries[i].flags &^= clrTheseFlagsIfMatch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ndpCache) Lookup(addr [16]byte) *ndpEntry {
|
||||||
|
for i := range c.entries {
|
||||||
|
if c.entries[i].flags&ndpFlagInUse != 0 && c.entries[i].addr == addr {
|
||||||
|
return &c.entries[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// acquireNext returns the next available entry, evicting the oldest passive
|
||||||
|
// (passively-learned) entry before touching active user queries or pending responses.
|
||||||
|
func (c *ndpCache) acquireNext() *ndpEntry {
|
||||||
|
const priorityFlags = ndpFlagPendingResponse | ndpFlagIncomplete | ndpFlagPriority
|
||||||
|
oldest, oldestPassive := 0, -1
|
||||||
|
for i := range c.entries {
|
||||||
|
if c.entries[i].flags&ndpFlagInUse == 0 {
|
||||||
|
oldest = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if !c.entries[i].flags.hasAny(priorityFlags) {
|
||||||
|
if oldestPassive < 0 || c.entries[i].age > c.entries[oldestPassive].age {
|
||||||
|
oldestPassive = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.entries[i].age > c.entries[oldest].age {
|
||||||
|
oldest = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if oldestPassive >= 0 && c.entries[oldest].flags&ndpFlagInUse != 0 {
|
||||||
|
oldest = oldestPassive
|
||||||
|
}
|
||||||
|
c.ageEntries()
|
||||||
|
e := &c.entries[oldest]
|
||||||
|
e.destroy()
|
||||||
|
return e
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package icmpv6
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClientNDP(t *testing.T) {
|
||||||
|
addr1 := [16]byte{0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
|
||||||
|
addr2 := [16]byte{0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2}
|
||||||
|
mac1 := [6]byte{0xde, 0xad, 0xbe, 0xef, 0x00, 0x01}
|
||||||
|
mac2 := [6]byte{0xc0, 0xff, 0xee, 0xc0, 0xff, 0x02}
|
||||||
|
|
||||||
|
var h1, h2 Client
|
||||||
|
if err := h1.Configure(ClientConfig{OurAddr: addr1, OurMAC: mac1, NDPCache: 2}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := h2.Configure(ClientConfig{OurAddr: addr2, OurMAC: mac2, NDPCache: 2}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf [64]byte
|
||||||
|
|
||||||
|
// No pending work: both clients should be silent.
|
||||||
|
n, err := h1.Encapsulate(buf[:], -1, 0)
|
||||||
|
if err != nil || n > 0 {
|
||||||
|
t.Fatal("expected no data before query:", err, n)
|
||||||
|
}
|
||||||
|
n, err = h2.Encapsulate(buf[:], -1, 0)
|
||||||
|
if err != nil || n > 0 {
|
||||||
|
t.Fatal("expected no data before query:", err, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// h1 queries h2's MAC.
|
||||||
|
if err = h1.NDPStartQuery(addr2, false); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// h1 sends a Neighbor Solicitation.
|
||||||
|
n, err = h1.Encapsulate(buf[:], -1, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("h1 encapsulate NS:", err)
|
||||||
|
} else if n == 0 {
|
||||||
|
t.Fatal("expected NS to be written")
|
||||||
|
}
|
||||||
|
validateNDP(t, buf[:n], TypeNeighborSolicitation)
|
||||||
|
|
||||||
|
// Verify NS target address is addr2.
|
||||||
|
ifrm, _ := NewFrame(buf[:n])
|
||||||
|
nsfrm := FrameNeighborSolicitation{Frame: ifrm}
|
||||||
|
if *nsfrm.TargetAddr() != addr2 {
|
||||||
|
t.Errorf("NS target addr mismatch: want %x, got %x", addr2, *nsfrm.TargetAddr())
|
||||||
|
}
|
||||||
|
|
||||||
|
// h2 receives the NS.
|
||||||
|
if err = h2.Demux(buf[:n], 0); err != nil {
|
||||||
|
t.Fatal("h2 demux NS:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// h2 sends a Neighbor Advertisement.
|
||||||
|
n, err = h2.Encapsulate(buf[:], -1, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("h2 encapsulate NA:", err)
|
||||||
|
} else if n == 0 {
|
||||||
|
t.Fatal("expected NA to be written")
|
||||||
|
}
|
||||||
|
validateNDP(t, buf[:n], TypeNeighborAdvertisement)
|
||||||
|
|
||||||
|
// Verify NA target address is addr2 (h2's own address).
|
||||||
|
ifrm, _ = NewFrame(buf[:n])
|
||||||
|
nafrm := FrameNeighborAdvertisement{Frame: ifrm}
|
||||||
|
if *nafrm.TargetAddr() != addr2 {
|
||||||
|
t.Errorf("NA target addr mismatch: want %x, got %x", addr2, *nafrm.TargetAddr())
|
||||||
|
}
|
||||||
|
_, solicited, _ := nafrm.Flags()
|
||||||
|
if !solicited {
|
||||||
|
t.Error("expected solicited flag set in NA")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Double-tap: h2 should have nothing left to send.
|
||||||
|
n2, err := h2.Encapsulate(buf[:], -1, 0)
|
||||||
|
if err != nil || n2 > 0 {
|
||||||
|
t.Fatal("double tap: expected no more data from h2:", err, n2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// h1 receives the NA and resolves h2's MAC.
|
||||||
|
if err = h1.Demux(buf[:n], 0); err != nil {
|
||||||
|
t.Fatal("h1 demux NA:", err)
|
||||||
|
}
|
||||||
|
mac, err := h1.NDPCacheLookup(addr2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("cache lookup after resolution:", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(mac[:], mac2[:]) {
|
||||||
|
t.Errorf("resolved MAC mismatch: want %x, got %x", mac2, mac)
|
||||||
|
}
|
||||||
|
|
||||||
|
// h1 should have nothing left to send.
|
||||||
|
n, err = h1.Encapsulate(buf[:], -1, 0)
|
||||||
|
if err != nil || n > 0 {
|
||||||
|
t.Fatal("expected no data after completion:", err, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateNDP(t *testing.T, buf []byte, wantType Type) {
|
||||||
|
t.Helper()
|
||||||
|
if len(buf) < sizeNDP {
|
||||||
|
t.Errorf("NDP frame too short: %d < %d", len(buf), sizeNDP)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ifrm, err := NewFrame(buf)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("NewFrame:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ifrm.Type() != wantType {
|
||||||
|
t.Errorf("type mismatch: want %s, got %s", wantType, ifrm.Type())
|
||||||
|
}
|
||||||
|
if ifrm.Code() != 0 {
|
||||||
|
t.Errorf("code must be zero, got %d", ifrm.Code())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
// Code generated by "stringer -type=Type,CodeDestinationUnreachable,CodeParameterProblem -linecomment -output stringers.go"; DO NOT EDIT.
|
||||||
|
|
||||||
|
package icmpv6
|
||||||
|
|
||||||
|
import "strconv"
|
||||||
|
|
||||||
|
func _() {
|
||||||
|
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||||
|
// Re-run the stringer command to generate them again.
|
||||||
|
var x [1]struct{}
|
||||||
|
_ = x[TypeDestinationUnreachable-1]
|
||||||
|
_ = x[TypePacketTooBig-2]
|
||||||
|
_ = x[TypeTimeExceeded-3]
|
||||||
|
_ = x[TypeParameterProblem-4]
|
||||||
|
_ = x[TypeEchoRequest-128]
|
||||||
|
_ = x[TypeEchoReply-129]
|
||||||
|
_ = x[TypeRouterSolicitation-133]
|
||||||
|
_ = x[TypeRouterAdvertisement-134]
|
||||||
|
_ = x[TypeNeighborSolicitation-135]
|
||||||
|
_ = x[TypeNeighborAdvertisement-136]
|
||||||
|
_ = x[TypeRedirectMessage-137]
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
_Type_name_0 = "destination unreachablepacket too bigtime exceededparameter problem"
|
||||||
|
_Type_name_1 = "echo requestecho reply"
|
||||||
|
_Type_name_2 = "router solicitationrouter advertisementneighbor solicitationneighbor advertisementredirect message"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_Type_index_0 = [...]uint8{0, 23, 37, 50, 67}
|
||||||
|
_Type_index_1 = [...]uint8{0, 12, 22}
|
||||||
|
_Type_index_2 = [...]uint8{0, 19, 39, 60, 82, 98}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (i Type) String() string {
|
||||||
|
switch {
|
||||||
|
case 1 <= i && i <= 4:
|
||||||
|
i -= 1
|
||||||
|
return _Type_name_0[_Type_index_0[i]:_Type_index_0[i+1]]
|
||||||
|
case 128 <= i && i <= 129:
|
||||||
|
i -= 128
|
||||||
|
return _Type_name_1[_Type_index_1[i]:_Type_index_1[i+1]]
|
||||||
|
case 133 <= i && i <= 137:
|
||||||
|
i -= 133
|
||||||
|
return _Type_name_2[_Type_index_2[i]:_Type_index_2[i+1]]
|
||||||
|
default:
|
||||||
|
return "Type(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func _() {
|
||||||
|
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||||
|
// Re-run the stringer command to generate them again.
|
||||||
|
var x [1]struct{}
|
||||||
|
_ = x[CodeNoRoute-0]
|
||||||
|
_ = x[CodeAdminProhibited-1]
|
||||||
|
_ = x[CodeBeyondScope-2]
|
||||||
|
_ = x[CodeAddressUnreachable-3]
|
||||||
|
_ = x[CodePortUnreachable-4]
|
||||||
|
_ = x[CodeIngressEgressPolicy-5]
|
||||||
|
_ = x[CodeRejectRoute-6]
|
||||||
|
}
|
||||||
|
|
||||||
|
const _CodeDestinationUnreachable_name = "no route to destinationcommunication administratively prohibitedbeyond scope of source addressaddress unreachableport unreachablesource address failed ingress/egress policyreject route to destination"
|
||||||
|
|
||||||
|
var _CodeDestinationUnreachable_index = [...]uint8{0, 23, 64, 94, 113, 129, 172, 199}
|
||||||
|
|
||||||
|
func (i CodeDestinationUnreachable) String() string {
|
||||||
|
if i >= CodeDestinationUnreachable(len(_CodeDestinationUnreachable_index)-1) {
|
||||||
|
return "CodeDestinationUnreachable(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||||
|
}
|
||||||
|
return _CodeDestinationUnreachable_name[_CodeDestinationUnreachable_index[i]:_CodeDestinationUnreachable_index[i+1]]
|
||||||
|
}
|
||||||
|
|
||||||
|
func _() {
|
||||||
|
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||||
|
// Re-run the stringer command to generate them again.
|
||||||
|
var x [1]struct{}
|
||||||
|
_ = x[CodeErroneousHeaderField-0]
|
||||||
|
_ = x[CodeUnrecognizedNextHeader-1]
|
||||||
|
_ = x[CodeUnrecognizedIPv6Option-2]
|
||||||
|
}
|
||||||
|
|
||||||
|
const _CodeParameterProblem_name = "erroneous header field encounteredunrecognized next header type encounteredunrecognized IPv6 option encountered"
|
||||||
|
|
||||||
|
var _CodeParameterProblem_index = [...]uint8{0, 34, 75, 111}
|
||||||
|
|
||||||
|
func (i CodeParameterProblem) String() string {
|
||||||
|
if i >= CodeParameterProblem(len(_CodeParameterProblem_index)-1) {
|
||||||
|
return "CodeParameterProblem(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||||
|
}
|
||||||
|
return _CodeParameterProblem_name[_CodeParameterProblem_index[i]:_CodeParameterProblem_index[i+1]]
|
||||||
|
}
|
||||||
+16
-1
@@ -173,6 +173,20 @@ func (conn *Conn) OpenListen(localPort uint16, iss Value) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CloseRead activates local discard mode on the connection. Incoming data is
|
||||||
|
// still ACKed normally but payload is dropped; future Read calls return io.EOF.
|
||||||
|
// The write side is unaffected.
|
||||||
|
func (conn *Conn) CloseRead() error {
|
||||||
|
conn.mu.Lock()
|
||||||
|
defer conn.mu.Unlock()
|
||||||
|
err := conn.checkPipeOpen()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
conn.h.ShutdownRead()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (conn *Conn) Close() error {
|
func (conn *Conn) Close() error {
|
||||||
conn.mu.Lock()
|
conn.mu.Lock()
|
||||||
defer conn.mu.Unlock()
|
defer conn.mu.Unlock()
|
||||||
@@ -294,10 +308,11 @@ func (conn *Conn) Read(b []byte) (int, error) {
|
|||||||
break
|
break
|
||||||
} else {
|
} else {
|
||||||
state := conn.h.State()
|
state := conn.h.State()
|
||||||
|
rxRefuse := conn.h.shutdownRx
|
||||||
conn.mu.Unlock()
|
conn.mu.Unlock()
|
||||||
if state.IsClosed() {
|
if state.IsClosed() {
|
||||||
return n, net.ErrClosed
|
return n, net.ErrClosed
|
||||||
} else if !state.RxDataOpen() {
|
} else if !state.RxDataOpen() || rxRefuse {
|
||||||
return n, io.EOF
|
return n, io.EOF
|
||||||
} else if conn.deadlineExceeded(&conn.rdead) {
|
} else if conn.deadlineExceeded(&conn.rdead) {
|
||||||
return n, errDeadlineExceeded
|
return n, errDeadlineExceeded
|
||||||
|
|||||||
+15
-3
@@ -28,8 +28,9 @@ type Handler struct {
|
|||||||
// connection is established via Open calls. This disambiguates whether
|
// connection is established via Open calls. This disambiguates whether
|
||||||
// Read and Write calls belong to the current connection.
|
// Read and Write calls belong to the current connection.
|
||||||
|
|
||||||
optcodec OptionCodec
|
optcodec OptionCodec
|
||||||
closing bool
|
closing bool
|
||||||
|
shutdownRx bool
|
||||||
// nRetransmit stores the number of times the oldest packet was retransmit.
|
// nRetransmit stores the number of times the oldest packet was retransmit.
|
||||||
nRetransmit uint8
|
nRetransmit uint8
|
||||||
}
|
}
|
||||||
@@ -128,6 +129,7 @@ func (h *Handler) reset(localPort, remotePort uint16, iss Value) {
|
|||||||
validator: h.validator,
|
validator: h.validator,
|
||||||
logger: h.logger,
|
logger: h.logger,
|
||||||
closing: false,
|
closing: false,
|
||||||
|
shutdownRx: false,
|
||||||
}
|
}
|
||||||
h.bufTx.ResetOrReuse(nil, 0, iss)
|
h.bufTx.ResetOrReuse(nil, 0, iss)
|
||||||
h.bufRx.Reset()
|
h.bufRx.Reset()
|
||||||
@@ -185,7 +187,7 @@ func (h *Handler) Recv(incomingPacket []byte) error {
|
|||||||
if prevState != h.scb.State() {
|
if prevState != h.scb.State() {
|
||||||
h.info("tcp.Handler:rx-statechange", slog.Uint64("port", uint64(h.localPort)), slog.String("old", prevState.String()), slog.String("new", h.scb.State().String()), slog.String("rxflags", segIncoming.Flags.String()))
|
h.info("tcp.Handler:rx-statechange", slog.Uint64("port", uint64(h.localPort)), slog.String("old", prevState.String()), slog.String("new", h.scb.State().String()), slog.String("rxflags", segIncoming.Flags.String()))
|
||||||
}
|
}
|
||||||
if segIncoming.DATALEN != 0 {
|
if segIncoming.DATALEN != 0 && !h.shutdownRx {
|
||||||
_, err = h.bufRx.Write(payload)
|
_, err = h.bufRx.Write(payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -229,6 +231,13 @@ func (h *Handler) Recv(incomingPacket []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ShutdownRead activates local discard mode: incoming payload bytes are dropped
|
||||||
|
// (ACK/SEQ still advance normally) and Read returns io.EOF immediately.
|
||||||
|
// Not reversible within the lifetime of a connection; reset clears it.
|
||||||
|
func (h *Handler) ShutdownRead() {
|
||||||
|
h.shutdownRx = true
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) Close() error {
|
func (h *Handler) Close() error {
|
||||||
h.trace("tcp.Handler.Close")
|
h.trace("tcp.Handler.Close")
|
||||||
if h.closing {
|
if h.closing {
|
||||||
@@ -338,6 +347,9 @@ func (h *Handler) Write(b []byte) (int, error) {
|
|||||||
|
|
||||||
// Read implements [io.Reader] by reading received data from remote peer in internal buffer.
|
// Read implements [io.Reader] by reading received data from remote peer in internal buffer.
|
||||||
func (h *Handler) Read(b []byte) (n int, err error) {
|
func (h *Handler) Read(b []byte) (n int, err error) {
|
||||||
|
if h.shutdownRx {
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
if h.bufRx.Buffered() > 0 {
|
if h.bufRx.Buffered() > 0 {
|
||||||
n, err = h.bufRx.Read(b)
|
n, err = h.bufRx.Read(b)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -164,7 +164,7 @@ func (conn *Conn) Read(b []byte) (int, error) {
|
|||||||
conn.mu.Unlock()
|
conn.mu.Unlock()
|
||||||
return 0, net.ErrClosed
|
return 0, net.ErrClosed
|
||||||
}
|
}
|
||||||
n, err := conn.h.Read(b)
|
n, err := conn.h.ReadNext(b)
|
||||||
conn.mu.Unlock()
|
conn.mu.Unlock()
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
return n, err
|
return n, err
|
||||||
|
|||||||
+3
-3
@@ -141,10 +141,10 @@ func (h *Handler) Write(b []byte) (int, error) {
|
|||||||
return len(b), nil
|
return len(b), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read dequeues the next received datagram into b. If b is smaller than the
|
// ReadNext dequeues the next received datagram into b. If b is smaller than the
|
||||||
// datagram, the remaining bytes are discarded (SOCK_DGRAM semantics).
|
// datagram, the remaining bytes are discarded (SOCK_DGRAM semantics).
|
||||||
// Returns 0, nil if no datagrams are available.
|
// Returns 0, nil if no datagrams are available.
|
||||||
func (h *Handler) Read(b []byte) (int, error) {
|
func (h *Handler) ReadNext(b []byte) (int, error) {
|
||||||
if len(h.rxDgrams) == 0 {
|
if len(h.rxDgrams) == 0 {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
@@ -190,7 +190,7 @@ func (h *Handler) Abort() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// BufferedInputNext returns the size of the next datagram to read. A call
|
// BufferedInputNext returns the size of the next datagram to read. A call
|
||||||
// to [Handler.Read] will read up to this amount of bytes.
|
// to [Handler.ReadNext] will read up to this amount of bytes.
|
||||||
func (h *Handler) BufferedInputNext() int {
|
func (h *Handler) BufferedInputNext() int {
|
||||||
if len(h.rxDgrams) == 0 {
|
if len(h.rxDgrams) == 0 {
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
+360
@@ -0,0 +1,360 @@
|
|||||||
|
package udp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
|
"github.com/soypat/lneto"
|
||||||
|
"github.com/soypat/lneto/internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ lneto.StackNode = (*muxHandler)(nil)
|
||||||
|
_ lneto.StackNode = (*MuxHandlerMIMO)(nil)
|
||||||
|
_ lneto.StackNode = (*MuxHandlerSIMO)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
// MuxHandlerSIMO is a single-input multiple-output UDP mux handler.
|
||||||
|
// It binds to one local UDP port and multiplexes transmit/receive using that port.
|
||||||
|
type MuxHandlerSIMO struct {
|
||||||
|
muxHandler
|
||||||
|
localPort uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
// MuxHandlerMIMO is a multi-input multi-output UDP mux handler.
|
||||||
|
// It supports sending and receiving on multiple local UDP ports through shared state.
|
||||||
|
type MuxHandlerMIMO struct {
|
||||||
|
muxHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure initializes the SIMO handler for a single local source port.
|
||||||
|
// The provided localPort becomes the only permitted receive port.
|
||||||
|
func (ms *MuxHandlerSIMO) Configure(localPort uint16, cfg MuxConfig) (err error) {
|
||||||
|
if localPort == 0 {
|
||||||
|
return lneto.ErrZeroSource
|
||||||
|
}
|
||||||
|
err = ms.muxHandler.Configure(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ms.localPort = localPort
|
||||||
|
ms.muxHandler.FilterAddLocalPort(localPort, 1)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LocalPort returns the configured local UDP port for this SIMO handler.
|
||||||
|
func (ms *MuxHandlerSIMO) LocalPort() uint16 { return ms.localPort }
|
||||||
|
|
||||||
|
// WriteTo queues a UDP payload for transmission from the handler's local port.
|
||||||
|
func (ms *MuxHandlerSIMO) WriteTo(buf []byte, raddr netip.AddrPort) error {
|
||||||
|
return ms.muxHandler.WriteTo(buf, ms.localPort, raddr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadNext returns the next received datagram for this handler's local port.
|
||||||
|
// If the datagram is for a different port it is discarded.
|
||||||
|
func (ms *MuxHandlerSIMO) ReadNext(buf []byte) (n int, completeRead bool, raddr netip.AddrPort) {
|
||||||
|
var lport uint16
|
||||||
|
n, completeRead, lport, raddr = ms.muxHandler.ReadNext(buf)
|
||||||
|
if lport != ms.localPort { // Can happen if user fiddles with filters.
|
||||||
|
return 0, false, raddr
|
||||||
|
}
|
||||||
|
return n, completeRead, raddr
|
||||||
|
}
|
||||||
|
|
||||||
|
// MuxConfig configures receive/transmit buffers and queue sizes for a UDP mux handler.
|
||||||
|
type MuxConfig struct {
|
||||||
|
// Configure receive buffer. If not set will use previously set buffer in [MuxHandler.Configure].
|
||||||
|
RxBuf []byte
|
||||||
|
// Configure transmit buffer. If not set will use previously set buffer in [MuxHandler.Configure].
|
||||||
|
TxBuf []byte
|
||||||
|
RxQueueSize int
|
||||||
|
TxQueueSize int
|
||||||
|
}
|
||||||
|
|
||||||
|
// muxHandler
|
||||||
|
type muxHandler struct {
|
||||||
|
connid uint64
|
||||||
|
// filterLPorts stores rx port ranges over which Handler can receive data.
|
||||||
|
// If not set will not filter UDP data.
|
||||||
|
filterLPorts []struct {
|
||||||
|
startPort uint16
|
||||||
|
nports uint16 // must be at least 1 to be valid.
|
||||||
|
}
|
||||||
|
// filterRAddrs. If not set will receive any data.
|
||||||
|
// filterRAddrs []struct {
|
||||||
|
// addr netip.Prefix
|
||||||
|
// }
|
||||||
|
rxRing internal.Ring
|
||||||
|
rxDgrams []struct {
|
||||||
|
length uint16
|
||||||
|
lport uint16
|
||||||
|
rport uint16
|
||||||
|
raddr netip.Addr
|
||||||
|
}
|
||||||
|
txRing internal.Ring
|
||||||
|
txDgrams []struct {
|
||||||
|
length uint16
|
||||||
|
lport uint16
|
||||||
|
rport uint16
|
||||||
|
raddr netip.Addr
|
||||||
|
}
|
||||||
|
closeCalled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure initializes the handler with the given buffer and queue configuration.
|
||||||
|
// Increments the connection ID, invalidating any prior stack registration.
|
||||||
|
func (mh *muxHandler) Configure(cfg MuxConfig) error {
|
||||||
|
if cfg.RxBuf == nil {
|
||||||
|
cfg.RxBuf = mh.rxRing.Buf
|
||||||
|
}
|
||||||
|
if cfg.TxBuf == nil {
|
||||||
|
cfg.TxBuf = mh.txRing.Buf
|
||||||
|
}
|
||||||
|
if len(cfg.RxBuf) < sizeHeader || len(cfg.TxBuf) < sizeHeader || cfg.RxQueueSize <= 0 || cfg.TxQueueSize <= 0 {
|
||||||
|
return lneto.ErrInvalidConfig
|
||||||
|
}
|
||||||
|
mh.Abort()
|
||||||
|
mh.rxRing = internal.Ring{Buf: cfg.RxBuf}
|
||||||
|
mh.txRing = internal.Ring{Buf: cfg.TxBuf}
|
||||||
|
internal.SliceReuse(&mh.rxDgrams, cfg.RxQueueSize)
|
||||||
|
internal.SliceReuse(&mh.txDgrams, cfg.TxQueueSize)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Protocol implements [lneto.StackNode].
|
||||||
|
func (mh *muxHandler) Protocol() uint64 { return uint64(lneto.IPProtoUDP) }
|
||||||
|
|
||||||
|
// ConnectionID implements [lneto.StackNode].
|
||||||
|
func (mh *muxHandler) ConnectionID() *uint64 { return &mh.connid }
|
||||||
|
|
||||||
|
// LocalPort implements [lneto.StackNode] but not applicable to mux. Mux is a multi Rx/Tx port abstraction.
|
||||||
|
func (mh *muxHandler) LocalPort() uint16 { return 0 }
|
||||||
|
|
||||||
|
func (mh *muxHandler) FilterResetLocalPorts() {
|
||||||
|
mh.filterLPorts = mh.filterLPorts[:0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mh *muxHandler) FilterAddLocalPort(startPort, nports uint16) {
|
||||||
|
if int(startPort)+int(nports) > math.MaxUint16 {
|
||||||
|
panic("port overflow")
|
||||||
|
}
|
||||||
|
f := internal.SliceReclaim(&mh.filterLPorts)
|
||||||
|
f.startPort = startPort
|
||||||
|
f.nports = nports
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mh *muxHandler) FilterLocalPort(lport uint16) (filtered bool) {
|
||||||
|
filtered = len(mh.filterLPorts) > 0
|
||||||
|
for i := range mh.filterLPorts {
|
||||||
|
maxPort := mh.filterLPorts[i].startPort + mh.filterLPorts[i].nports
|
||||||
|
if lport >= mh.filterLPorts[i].startPort && lport < maxPort {
|
||||||
|
filtered = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recv parses a UDP frame from buf, validates the ports and length fields,
|
||||||
|
// and enqueues the payload into the rx ring buffer. Returns [lneto.ErrMismatch]
|
||||||
|
// if source/destination ports don't match the configured ports.
|
||||||
|
func (mh *muxHandler) Demux(carrierData []byte, frameOffset int) error {
|
||||||
|
if mh.closeCalled {
|
||||||
|
return net.ErrClosed
|
||||||
|
}
|
||||||
|
ufrm, err := NewFrame(carrierData[frameOffset:])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Rx port filter.
|
||||||
|
lport := ufrm.DestinationPort()
|
||||||
|
if mh.FilterLocalPort(lport) {
|
||||||
|
return lneto.ErrMismatch
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header size validation.
|
||||||
|
// No CRC validation at this level.
|
||||||
|
ul := ufrm.Length()
|
||||||
|
if ul < sizeHeader {
|
||||||
|
return lneto.ErrInvalidLengthField
|
||||||
|
} else if int(ul) > len(ufrm.RawData()) {
|
||||||
|
return lneto.ErrTruncatedFrame
|
||||||
|
}
|
||||||
|
|
||||||
|
free := cap(mh.rxDgrams) - len(mh.rxDgrams)
|
||||||
|
if free == 0 {
|
||||||
|
return lneto.ErrExhausted
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := ufrm.Payload()
|
||||||
|
_, err = mh.rxRing.Write(payload)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dgram := internal.SliceReclaim(&mh.rxDgrams)
|
||||||
|
dgram.length = uint16(len(payload))
|
||||||
|
dgram.lport = lport
|
||||||
|
dgram.rport = ufrm.SourcePort()
|
||||||
|
if frameOffset >= 20 {
|
||||||
|
src, _, _, _, _ := internal.GetIPAddr(carrierData)
|
||||||
|
dgram.raddr, _ = netip.AddrFromSlice(src)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mh *muxHandler) Encapsulate(carrierData []byte, ipOffset, frameOffset int) (int, error) {
|
||||||
|
if mh.closeCalled {
|
||||||
|
return 0, net.ErrClosed
|
||||||
|
} else if len(mh.txDgrams) == 0 {
|
||||||
|
return 0, nil // No data to send.
|
||||||
|
}
|
||||||
|
buf := carrierData[frameOffset:]
|
||||||
|
ufrm, err := NewFrame(buf)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dgram := internal.SliceDequeueFront(&mh.txDgrams)
|
||||||
|
avail := len(buf) - 8
|
||||||
|
if avail < int(dgram.length) {
|
||||||
|
// TODO(soypat): If packet is too long we discard it entirely. Maybe we prefer sending incomplete data? How do other stacks deal with this?
|
||||||
|
mh.txRing.ReadDiscard(int(dgram.length))
|
||||||
|
return 0, lneto.ErrShortBuffer
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := mh.txRing.Read(buf[8 : 8+dgram.length])
|
||||||
|
if err != nil || n != int(dgram.length) {
|
||||||
|
panic(fmt.Sprintf("udp send handler failure %d %s", n, err))
|
||||||
|
}
|
||||||
|
ufrm.SetSourcePort(dgram.lport)
|
||||||
|
ufrm.SetDestinationPort(dgram.rport)
|
||||||
|
ufrm.SetLength(8 + dgram.length)
|
||||||
|
if ipOffset >= 0 && dgram.raddr.IsValid() {
|
||||||
|
// Address write. Version check.
|
||||||
|
var addroffset int
|
||||||
|
switch carrierData[ipOffset] >> 4 {
|
||||||
|
case 4:
|
||||||
|
if !dgram.raddr.Is4() {
|
||||||
|
return 0, lneto.ErrUnsupported
|
||||||
|
}
|
||||||
|
addroffset = ipOffset + 16
|
||||||
|
|
||||||
|
case 6:
|
||||||
|
if !dgram.raddr.Is6() {
|
||||||
|
return 0, lneto.ErrUnsupported
|
||||||
|
}
|
||||||
|
addroffset = ipOffset + 24
|
||||||
|
default:
|
||||||
|
return 0, lneto.ErrUnsupported
|
||||||
|
}
|
||||||
|
dgram.raddr.AppendBinary(carrierData[addroffset:addroffset])
|
||||||
|
}
|
||||||
|
return int(8 + dgram.length), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mh *muxHandler) WriteTo(buf []byte, lport uint16, raddr netip.AddrPort) error {
|
||||||
|
if mh.closeCalled {
|
||||||
|
return net.ErrClosed
|
||||||
|
} else if raddr.Port() == 0 || !raddr.IsValid() {
|
||||||
|
return lneto.ErrZeroDestination
|
||||||
|
} else if lport == 0 {
|
||||||
|
return lneto.ErrZeroSource
|
||||||
|
}
|
||||||
|
avail := cap(mh.txDgrams) - len(mh.txDgrams)
|
||||||
|
if avail == 0 {
|
||||||
|
return lneto.ErrExhausted
|
||||||
|
} else if mh.txRing.Free() < len(buf) {
|
||||||
|
return lneto.ErrBufferFull
|
||||||
|
}
|
||||||
|
n, err := mh.txRing.Write(buf)
|
||||||
|
if err != nil || n != len(buf) {
|
||||||
|
return lneto.ErrBug
|
||||||
|
}
|
||||||
|
dgram := internal.SliceReclaim(&mh.txDgrams)
|
||||||
|
dgram.length = uint16(len(buf))
|
||||||
|
dgram.raddr = raddr.Addr()
|
||||||
|
dgram.lport = lport
|
||||||
|
dgram.rport = raddr.Port()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadNext dequeues the next received datagram into b. If b is smaller than the
|
||||||
|
// datagram, the remaining bytes are discarded (SOCK_DGRAM semantics).
|
||||||
|
// The port the datagram was destined to and address it was received from are returned.
|
||||||
|
// If bytes are discarded completeRead=false.
|
||||||
|
func (mh *muxHandler) ReadNext(buf []byte) (n int, completeRead bool, lport uint16, raddr netip.AddrPort) {
|
||||||
|
if len(mh.rxDgrams) == 0 {
|
||||||
|
return 0, false, 0, raddr
|
||||||
|
}
|
||||||
|
dgram := internal.SliceDequeueFront(&mh.rxDgrams)
|
||||||
|
dlen := int(dgram.length)
|
||||||
|
maxRead := min(dlen, len(buf))
|
||||||
|
n, _ = mh.rxRing.Read(buf[:maxRead])
|
||||||
|
if n < dlen {
|
||||||
|
mh.rxRing.ReadDiscard(dlen - n)
|
||||||
|
}
|
||||||
|
return n, n == dlen, dgram.lport, netip.AddrPortFrom(dgram.raddr, dgram.rport)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BufferedInputNext returns the size of the next datagram to read. A call
|
||||||
|
// to [Handler.ReadNext] will read up to this amount of bytes.
|
||||||
|
func (mh *muxHandler) BufferedInputNext() uint16 {
|
||||||
|
if len(mh.rxDgrams) > 0 {
|
||||||
|
return mh.rxDgrams[0].length
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// BufferedInput returns the number of unread bytes in the receive buffer.
|
||||||
|
func (h *muxHandler) BufferedInput() int {
|
||||||
|
return h.rxRing.Buffered()
|
||||||
|
}
|
||||||
|
|
||||||
|
// BufferedUnsent returns the number of written but unsent bytes in the transmit buffer.
|
||||||
|
func (h *muxHandler) BufferedOutput() int {
|
||||||
|
return h.txRing.Buffered()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SizeInput returns the total size of the receive ring buffer.
|
||||||
|
func (h *muxHandler) SizeInput() int {
|
||||||
|
return h.rxRing.Size()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SizeOutput returns the total size of the transmit ring buffer.
|
||||||
|
func (h *muxHandler) SizeOutput() int {
|
||||||
|
return h.txRing.Size()
|
||||||
|
}
|
||||||
|
|
||||||
|
// FreeOutput returns the number of free bytes in the transmit buffer.
|
||||||
|
// This tells the user how many bytes can be written with Write method before write failing.
|
||||||
|
func (h *muxHandler) FreeOutput() int {
|
||||||
|
return h.txRing.Free()
|
||||||
|
}
|
||||||
|
|
||||||
|
// FreeInput returns the number of free bytes in the receive buffer.
|
||||||
|
func (h *muxHandler) FreeInput() int {
|
||||||
|
return h.rxRing.Free()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mh *muxHandler) IsOpen() bool {
|
||||||
|
return cap(mh.rxDgrams) > 0 && !mh.closeCalled
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mh *muxHandler) Close() {
|
||||||
|
mh.closeCalled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mh *muxHandler) Abort() {
|
||||||
|
*mh = muxHandler{
|
||||||
|
connid: mh.connid + 1,
|
||||||
|
filterLPorts: mh.filterLPorts[:0],
|
||||||
|
rxRing: mh.rxRing,
|
||||||
|
rxDgrams: mh.rxDgrams[:0],
|
||||||
|
txRing: mh.txRing,
|
||||||
|
txDgrams: mh.txDgrams[:0],
|
||||||
|
}
|
||||||
|
mh.rxRing.Reset()
|
||||||
|
mh.txRing.Reset()
|
||||||
|
}
|
||||||
+229
@@ -0,0 +1,229 @@
|
|||||||
|
package udp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/netip"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/soypat/lneto"
|
||||||
|
"github.com/soypat/lneto/internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newTestSIMO(t *testing.T, localPort uint16) *MuxHandlerSIMO {
|
||||||
|
t.Helper()
|
||||||
|
var ms MuxHandlerSIMO
|
||||||
|
err := ms.Configure(localPort, MuxConfig{
|
||||||
|
RxBuf: make([]byte, 256),
|
||||||
|
TxBuf: make([]byte, 256),
|
||||||
|
RxQueueSize: 4,
|
||||||
|
TxQueueSize: 4,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return &ms
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMuxSIMO_Configure_ZeroPort(t *testing.T) {
|
||||||
|
var ms MuxHandlerSIMO
|
||||||
|
err := ms.Configure(0, MuxConfig{
|
||||||
|
RxBuf: make([]byte, 256),
|
||||||
|
TxBuf: make([]byte, 256),
|
||||||
|
RxQueueSize: 4,
|
||||||
|
TxQueueSize: 4,
|
||||||
|
})
|
||||||
|
if err != lneto.ErrZeroSource {
|
||||||
|
t.Fatalf("want ErrZeroSource, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMuxSIMO_LocalPort(t *testing.T) {
|
||||||
|
ms := newTestSIMO(t, 1234)
|
||||||
|
if ms.LocalPort() != 1234 {
|
||||||
|
t.Fatalf("want 1234, got %d", ms.LocalPort())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMuxSIMO_WriteToEncapsulateRoundtrip(t *testing.T) {
|
||||||
|
const localPort = 1234
|
||||||
|
raddr := netip.AddrPortFrom(netip.AddrFrom4([4]byte{10, 0, 0, 1}), 8080)
|
||||||
|
ms := newTestSIMO(t, localPort)
|
||||||
|
payload := []byte("hello mux")
|
||||||
|
if err := ms.WriteTo(payload, raddr); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var buf [128]byte
|
||||||
|
n, err := ms.Encapsulate(buf[:], -1, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if want := 8 + len(payload); n != want {
|
||||||
|
t.Fatalf("encapsulated %d bytes, want %d", n, want)
|
||||||
|
}
|
||||||
|
ufrm, err := NewFrame(buf[:n])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if ufrm.SourcePort() != localPort {
|
||||||
|
t.Fatalf("src port %d, want %d", ufrm.SourcePort(), localPort)
|
||||||
|
}
|
||||||
|
if ufrm.DestinationPort() != raddr.Port() {
|
||||||
|
t.Fatalf("dst port %d, want %d", ufrm.DestinationPort(), raddr.Port())
|
||||||
|
}
|
||||||
|
if !internal.BytesEqual(ufrm.Payload(), payload) {
|
||||||
|
t.Fatal("payload mismatch")
|
||||||
|
}
|
||||||
|
// No more pending.
|
||||||
|
n, err = ms.Encapsulate(buf[:], -1, 0)
|
||||||
|
if err != nil || n != 0 {
|
||||||
|
t.Fatalf("expected empty encapsulate, got n=%d err=%v", n, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMuxSIMO_DemuxReadNextRoundtrip(t *testing.T) {
|
||||||
|
const localPort = 1234
|
||||||
|
const remotePort = 8080
|
||||||
|
ms := newTestSIMO(t, localPort)
|
||||||
|
payload := []byte("incoming")
|
||||||
|
frame := makeUDPFrame(remotePort, localPort, payload)
|
||||||
|
if err := ms.Demux(frame, 0); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var buf [64]byte
|
||||||
|
n, completeRead, raddr := ms.ReadNext(buf[:])
|
||||||
|
if n != len(payload) {
|
||||||
|
t.Fatalf("read %d bytes, want %d", n, len(payload))
|
||||||
|
}
|
||||||
|
if !completeRead {
|
||||||
|
t.Fatal("want completeRead=true")
|
||||||
|
}
|
||||||
|
if !internal.BytesEqual(buf[:n], payload) {
|
||||||
|
t.Fatal("payload mismatch")
|
||||||
|
}
|
||||||
|
if raddr.IsValid() {
|
||||||
|
t.Fatal("want zero raddr when no IP carrier (frameOffset=0 < 20)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMuxSIMO_DemuxFiltersMismatch(t *testing.T) {
|
||||||
|
ms := newTestSIMO(t, 1234)
|
||||||
|
frame := makeUDPFrame(8080, 9999, []byte("wrong port"))
|
||||||
|
err := ms.Demux(frame, 0)
|
||||||
|
if err != lneto.ErrMismatch {
|
||||||
|
t.Fatalf("want ErrMismatch, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMuxSIMO_ReadNextTruncates(t *testing.T) {
|
||||||
|
const localPort = 1234
|
||||||
|
ms := newTestSIMO(t, localPort)
|
||||||
|
payload := []byte("toolongpayload")
|
||||||
|
if err := ms.Demux(makeUDPFrame(8080, localPort, payload), 0); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var small [4]byte
|
||||||
|
n, completeRead, _ := ms.ReadNext(small[:])
|
||||||
|
if n != 4 {
|
||||||
|
t.Fatalf("read %d, want 4", n)
|
||||||
|
}
|
||||||
|
if completeRead {
|
||||||
|
t.Fatal("want completeRead=false on truncation")
|
||||||
|
}
|
||||||
|
if !internal.BytesEqual(small[:], payload[:4]) {
|
||||||
|
t.Fatal("truncated data mismatch")
|
||||||
|
}
|
||||||
|
// Next datagram should work cleanly after truncation.
|
||||||
|
payload2 := []byte("ok")
|
||||||
|
if err := ms.Demux(makeUDPFrame(8080, localPort, payload2), 0); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var buf [64]byte
|
||||||
|
n, completeRead, _ = ms.ReadNext(buf[:])
|
||||||
|
if !completeRead || !internal.BytesEqual(buf[:n], payload2) {
|
||||||
|
t.Fatal("post-truncation read mismatch")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMuxSIMO_MultipleDatagrams(t *testing.T) {
|
||||||
|
const localPort = 5000
|
||||||
|
ms := newTestSIMO(t, localPort)
|
||||||
|
type send struct {
|
||||||
|
payload string
|
||||||
|
raddr netip.AddrPort
|
||||||
|
}
|
||||||
|
sends := []send{
|
||||||
|
{"alpha", netip.AddrPortFrom(netip.AddrFrom4([4]byte{1, 0, 0, 1}), 100)},
|
||||||
|
{"beta", netip.AddrPortFrom(netip.AddrFrom4([4]byte{1, 0, 0, 2}), 200)},
|
||||||
|
{"gamma", netip.AddrPortFrom(netip.AddrFrom4([4]byte{1, 0, 0, 3}), 300)},
|
||||||
|
}
|
||||||
|
for _, s := range sends {
|
||||||
|
if err := ms.WriteTo([]byte(s.payload), s.raddr); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var buf [128]byte
|
||||||
|
for _, s := range sends {
|
||||||
|
n, err := ms.Encapsulate(buf[:], -1, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ufrm, err := NewFrame(buf[:n])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := string(ufrm.Payload()); got != s.payload {
|
||||||
|
t.Fatalf("payload %q, want %q", got, s.payload)
|
||||||
|
}
|
||||||
|
if ufrm.DestinationPort() != s.raddr.Port() {
|
||||||
|
t.Fatalf("dst port %d, want %d", ufrm.DestinationPort(), s.raddr.Port())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMuxSIMO_TxQueueExhausted(t *testing.T) {
|
||||||
|
ms := newTestSIMO(t, 1234) // queue size 4
|
||||||
|
raddr := netip.AddrPortFrom(netip.AddrFrom4([4]byte{10, 0, 0, 1}), 9000)
|
||||||
|
for i := range 4 {
|
||||||
|
if err := ms.WriteTo([]byte{byte(i)}, raddr); err != nil {
|
||||||
|
t.Fatalf("WriteTo %d: %v", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := ms.WriteTo([]byte{0xff}, raddr); err != lneto.ErrExhausted {
|
||||||
|
t.Fatalf("want ErrExhausted, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMuxSIMO_RxQueueExhausted(t *testing.T) {
|
||||||
|
const localPort = 1234
|
||||||
|
ms := newTestSIMO(t, localPort) // queue size 4
|
||||||
|
for i := range 4 {
|
||||||
|
frame := makeUDPFrame(8080, localPort, []byte{byte(i)})
|
||||||
|
if err := ms.Demux(frame, 0); err != nil {
|
||||||
|
t.Fatalf("Demux %d: %v", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
frame := makeUDPFrame(8080, localPort, []byte{0xff})
|
||||||
|
if err := ms.Demux(frame, 0); err != lneto.ErrExhausted {
|
||||||
|
t.Fatalf("want ErrExhausted, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMuxSIMO_ClosedBehavior(t *testing.T) {
|
||||||
|
ms := newTestSIMO(t, 1234)
|
||||||
|
ms.Close()
|
||||||
|
raddr := netip.AddrPortFrom(netip.AddrFrom4([4]byte{10, 0, 0, 1}), 9000)
|
||||||
|
if err := ms.WriteTo([]byte("data"), raddr); err == nil {
|
||||||
|
t.Fatal("expected error writing to closed handler")
|
||||||
|
}
|
||||||
|
frame := makeUDPFrame(8080, 1234, []byte("data"))
|
||||||
|
if err := ms.Demux(frame, 0); err == nil {
|
||||||
|
t.Fatal("expected error demuxing to closed handler")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMuxSIMO_WriteToInvalidRaddr(t *testing.T) {
|
||||||
|
ms := newTestSIMO(t, 1234)
|
||||||
|
zeroPort := netip.AddrPortFrom(netip.AddrFrom4([4]byte{10, 0, 0, 1}), 0)
|
||||||
|
if err := ms.WriteTo([]byte("data"), zeroPort); err != lneto.ErrZeroDestination {
|
||||||
|
t.Fatalf("want ErrZeroDestination, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user