mirror of
https://github.com/soypat/lneto.git
synced 2026-09-12 01:29: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,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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user