Files
lneto/x/xnet/stack6_test.go
T
Pat Whittingslow a2970b923d add ipv6 to xnet.StackAsync (#107)
* add ipv6 to xnet.StackAsync

* dns improvements

* improve DNS workings of StackAsync

* add tentative ICMPv6

* work on prefixes and fix some small bugs, plan UDP/TCP6

* fix bugs in StackAsync and ipv4.Prefix.Contains

* update arpsubtable

* completely remove legacy internet.StackIP for StackIPv4/v6

* ipv4/ipv6 tcp/udp

* add TCP6/UDP6 dialing APIs

* add xnet.Stack6 interface

* more ipv6 integration into StackAsync; various tweaks to lneto and documentation+TODOs

* add stack6 tests

* replace netip.Prefix with ipv4.Prefix where it makes sense
2026-05-13 15:31:18 -03:00

619 lines
17 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package xnet
import (
"bytes"
"testing"
"github.com/soypat/lneto/tcp"
"github.com/soypat/lneto/udp"
)
const (
ipv6HeaderSize = 40
mtu6Test = 1500
maxFrame6 = ipv6HeaderSize + mtu6Test
)
func stack6PairConfigs(seed int64, maxports, icmpQueue uint16) (cfg1, cfg2 StackConfig) {
var (
testAddr6A = [16]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1} // 2001:db8::1
testAddr6B = [16]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2} // 2001:db8::2
testMAC6A = [6]byte{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0x01}
testMAC6B = [6]byte{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0x02}
)
cfg1 = StackConfig{
Hostname: "test-s6-1",
RandSeed: seed,
StaticAddress6: testAddr6A,
HardwareAddress: testMAC6A,
MTU: mtu6Test,
MaxActiveUDPPorts: maxports,
MaxActiveTCPPorts: maxports,
ICMPQueueLimit: int(icmpQueue),
}
cfg2 = StackConfig{
Hostname: "test-s6-2",
RandSeed: ^seed,
StaticAddress6: testAddr6B,
HardwareAddress: testMAC6B,
MTU: mtu6Test,
MaxActiveUDPPorts: maxports,
MaxActiveTCPPorts: maxports,
ICMPQueueLimit: int(icmpQueue),
}
return cfg1, cfg2
}
// newStack6Pair creates two stack6 instances with distinct IPv6 addresses and MACs.
// ICMPQueueLimit is zero so NDP is disabled; DialUDP6/DialTCP6 skip MAC filtering.
func newStack6Pair(t testing.TB, seed int64, maxports, icmpQueue uint16) (s1, s2 Stack6) {
t.Helper()
cfg1, cfg2 := stack6PairConfigs(seed, maxports, icmpQueue)
s1 = DefaultStack6()
s2 = DefaultStack6()
if err := s1.Reset6(&cfg1); err != nil {
t.Fatal("s1 Reset6:", err)
}
if err := s2.Reset6(&cfg2); err != nil {
t.Fatal("s2 Reset6:", err)
}
return s1, s2
}
// newUDPConn6 allocates and configures a udp.Conn for use with stack6.
func newUDPConn6(t testing.TB) *udp.Conn {
t.Helper()
const bufSize = 2048
conn := new(udp.Conn)
if err := conn.Configure(udp.ConnConfig{
RxBuf: make([]byte, bufSize),
TxBuf: make([]byte, bufSize),
RxQueueSize: 4,
TxQueueSize: 4,
}); err != nil {
t.Fatal("UDP Configure:", err)
}
return conn
}
// newTCPConn6 allocates and configures a tcp.Conn for use with stack6.
func newTCPConn6(t testing.TB) *tcp.Conn {
t.Helper()
const bufSize = 2048
conn := new(tcp.Conn)
if err := conn.Configure(tcp.ConnConfig{
RxBuf: make([]byte, bufSize),
TxBuf: make([]byte, bufSize),
TxPacketQueueSize: 4,
}); err != nil {
t.Fatal("TCP Configure:", err)
}
return conn
}
// exchangeIPv6Once encapsulates one IPv6 frame from src and delivers it to dst.
// Returns the number of bytes written (0 if src had nothing to send).
func exchangeIPv6Once(t testing.TB, src, dst Stack6, buf []byte) int {
t.Helper()
n, err := src.EgressIPv6(buf)
if err != nil {
t.Error("EgressIPv6:", err)
return 0
}
if n == 0 {
return 0
}
if err := dst.IngressIPv6(buf[:n]); err != nil {
t.Error("IngressIPv6:", err)
}
return n
}
// listenTCP6 opens a passive TCP connection and registers it with a stack6 directly,
// mirroring what StackAsync.ListenTCP4 does for IPv4.
func listenTCP6(t testing.TB, s Stack6, conn *tcp.Conn, localPort uint16, iss tcp.Value) {
t.Helper()
if err := conn.OpenListen(localPort, iss); err != nil {
t.Fatal("OpenListen:", err)
}
if err := s.(*stack6).tcps6.RegisterMACFiltered(conn, nil); err != nil {
conn.Abort()
t.Fatal("RegisterMACFiltered:", err)
}
}
// ===== Tests =====
func TestStack6Reset(t *testing.T) {
s := DefaultStack6()
testMAC6A := [6]byte{1, 2, 3}
testAddr6A := [16]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
err := s.Reset6(&StackConfig{
Hostname: "reset-test-1",
RandSeed: 42,
StaticAddress6: testAddr6A,
HardwareAddress: testMAC6A,
MTU: mtu6Test,
})
if err != nil {
t.Fatal(err)
}
if got := s.Addr6(); got != testAddr6A {
t.Errorf("Addr6 = %v, want %v", got, testAddr6A)
}
testAddr2 := testAddr6A
testAddr2[15] = 2
// SetAddr6 must update the returned address.
s.SetAddr6(testAddr2)
if got := s.Addr6(); got != testAddr2 {
t.Errorf("after SetAddr6: got %v, want %v", got, testAddr2)
}
}
func TestStack6Reset_ICMPConfigured(t *testing.T) {
s := DefaultStack6()
err := s.Reset6(&StackConfig{
Hostname: "icmp-cfg-1",
RandSeed: 1337,
MTU: mtu6Test,
ICMPQueueLimit: 4,
})
if err != nil {
t.Fatal(err)
}
// EnableICMP6 should succeed since the client was configured.
if err := s.EnableICMP6(true); err != nil {
t.Fatal("EnableICMP6:", err)
}
// Disable should always succeed.
if err := s.EnableICMP6(false); err != nil {
t.Fatal("EnableICMP6(false):", err)
}
}
// TestStack6UDP_DataExchange sends a datagram from stack A to stack B and reads it back.
// Both stacks are configured without ICMP so NDP/MAC-filtering is bypassed.
func TestStack6UDP_DataExchange(t *testing.T) {
const (
rngseed = 100
portA = 5001
portB = 5002
nports = 2
)
s1, s2 := newStack6Pair(t, rngseed, nports, 0)
buf := make([]byte, maxFrame6)
connA := newUDPConn6(t)
connB := newUDPConn6(t)
// Open A -> B direction.
if err := s1.DialUDP6(connA, portA, s2.Addr6(), portB); err != nil {
t.Fatal("DialUDP6 A:", err)
}
// Open B -> A direction so B's stack accepts datagrams from A.
if err := s2.DialUDP6(connB, portB, s1.Addr6(), portA); err != nil {
t.Fatal("DialUDP6 B:", err)
}
want := []byte("hello ipv6 udp")
if _, err := connA.Write(want); err != nil {
t.Fatal("Write:", err)
}
if n := exchangeIPv6Once(t, s1, s2, buf); n == 0 {
t.Fatal("expected packet from A to B")
}
var rbuf [256]byte
n, err := connB.Read(rbuf[:])
if err != nil {
t.Fatal("Read:", err)
}
if !bytes.Equal(rbuf[:n], want) {
t.Errorf("got %q, want %q", rbuf[:n], want)
}
}
// TestStack6UDP_BidirectionalExchange verifies that both sides can send and receive.
func TestStack6UDP_BidirectionalExchange(t *testing.T) {
const (
rngseed = 100
nports = 1
portA = 6001
portB = 6002
)
s1, s2 := newStack6Pair(t, rngseed, nports, 0)
buf := make([]byte, maxFrame6)
connA := newUDPConn6(t)
connB := newUDPConn6(t)
if err := s1.DialUDP6(connA, portA, s2.Addr6(), portB); err != nil {
t.Fatal("DialUDP6 A:", err)
}
if err := s2.DialUDP6(connB, portB, s1.Addr6(), portA); err != nil {
t.Fatal("DialUDP6 B:", err)
}
// A -> B
msgAtoB := []byte("A->B")
if _, err := connA.Write(msgAtoB); err != nil {
t.Fatal("Write A->B:", err)
}
if n := exchangeIPv6Once(t, s1, s2, buf); n == 0 {
t.Fatal("expected packet A->B")
}
var rbuf [256]byte
n, err := connB.Read(rbuf[:])
if err != nil {
t.Fatal("Read B:", err)
}
if !bytes.Equal(rbuf[:n], msgAtoB) {
t.Errorf("B received %q, want %q", rbuf[:n], msgAtoB)
}
// B -> A
msgBtoA := []byte("B->A reply")
if _, err := connB.Write(msgBtoA); err != nil {
t.Fatal("Write B->A:", err)
}
if n := exchangeIPv6Once(t, s2, s1, buf); n == 0 {
t.Fatal("expected packet B->A")
}
n, err = connA.Read(rbuf[:])
if err != nil {
t.Fatal("Read A:", err)
}
if !bytes.Equal(rbuf[:n], msgBtoA) {
t.Errorf("A received %q, want %q", rbuf[:n], msgBtoA)
}
}
// TestStack6ICMPv6_PingEcho verifies a full ICMPv6 echo request/reply exchange.
func TestStack6ICMPv6_PingEcho(t *testing.T) {
const (
rngSeed = 42
nports = 1
icmpQueue = 2
)
s1, s2 := newStack6Pair(t, rngSeed, nports, icmpQueue)
buf := make([]byte, maxFrame6)
if err := s1.EnableICMP6(true); err != nil {
t.Fatal("s1 EnableICMP6:", err)
}
if err := s2.EnableICMP6(true); err != nil {
t.Fatal("s2 EnableICMP6:", err)
}
// Verify that no packets are pending before the ping.
if n, _ := s1.EgressIPv6(buf); n != 0 {
t.Fatal("unexpected egress from s1 before ping")
}
if n, _ := s2.EgressIPv6(buf); n != 0 {
t.Fatal("unexpected egress from s2 before ping")
}
// Start the ping from s1 to s2.
pattern := []byte("ping6test")
key, err := s1.(*stack6).icmp6.PingStart(s2.Addr6(), pattern, 32)
if err != nil {
t.Fatal("PingStart:", err)
}
// s1 sends echo request to s2.
if n := exchangeIPv6Once(t, s1, s2, buf); n == 0 {
t.Fatal("expected ICMPv6 echo request from s1")
}
// s2 sends echo reply back to s1.
if n := exchangeIPv6Once(t, s2, s1, buf); n == 0 {
t.Fatal("expected ICMPv6 echo reply from s2")
}
// No further packets should be needed.
if n, _ := s1.EgressIPv6(buf); n != 0 {
t.Error("unexpected extra egress from s1 after ping")
}
if n, _ := s2.EgressIPv6(buf); n != 0 {
t.Error("unexpected extra egress from s2 after ping")
}
completed, ok := s1.(*stack6).icmp6.PingPop(key)
if !ok {
t.Fatal("ping key not found after exchange")
}
if !completed {
t.Fatal("expected ping to be completed")
}
}
// TestStack6ICMPv6_MultiPing verifies multiple sequential pings work.
func TestStack6ICMPv6_MultiPing(t *testing.T) {
const (
rngseed = 193213
icmpqueue = 2
)
s1, s2 := newStack6Pair(t, rngseed, 0, icmpqueue)
buf := make([]byte, maxFrame6)
if err := s1.EnableICMP6(true); err != nil {
t.Fatal("s1 EnableICMP6:", err)
}
if err := s2.EnableICMP6(true); err != nil {
t.Fatal("s2 EnableICMP6:", err)
}
for i, pattern := range [][]byte{
[]byte("first"),
[]byte("second"),
[]byte("third"),
} {
key, err := s1.(*stack6).icmp6.PingStart(s2.Addr6(), pattern, uint16(len(pattern)+4))
if err != nil {
t.Fatalf("PingStart [%d]: %v", i, err)
}
if n := exchangeIPv6Once(t, s1, s2, buf); n == 0 {
t.Fatalf("[%d] expected echo request from s1", i)
}
if n := exchangeIPv6Once(t, s2, s1, buf); n == 0 {
t.Fatalf("[%d] expected echo reply from s2", i)
}
completed, ok := s1.(*stack6).icmp6.PingPop(key)
if !ok {
t.Fatalf("[%d] ping key not found", i)
}
if !completed {
t.Fatalf("[%d] ping not completed", i)
}
}
}
// TestStack6TCP_Handshake verifies that a TCP connection can be established over stack6.
func TestStack6TCP_Handshake(t *testing.T) {
const (
rngseed = 213213
svPort = 8080
clPort = 12345
nports = 1
icmpqueue = 2
)
s1, s2 := newStack6Pair(t, rngseed, nports, icmpqueue)
buf := make([]byte, maxFrame6)
svConn := newTCPConn6(t)
clConn := newTCPConn6(t)
// Server listens on s2.
listenTCP6(t, s2, svConn, svPort, 200)
// Client dials from s1 to s2.
if err := s1.DialTCP6(clConn, clPort, s2.Addr6(), svPort, 100); err != nil {
t.Fatal("DialTCP6:", err)
}
// SYN: client -> server.
if n := exchangeIPv6Once(t, s1, s2, buf); n == 0 {
t.Fatal("expected SYN from client")
}
if clConn.State() != tcp.StateSynSent {
t.Errorf("client state = %s, want SYN_SENT", clConn.State())
}
if svConn.State() != tcp.StateSynRcvd {
t.Errorf("server state = %s, want SYN_RCVD", svConn.State())
}
// SYNACK: server -> client.
if n := exchangeIPv6Once(t, s2, s1, buf); n == 0 {
t.Fatal("expected SYNACK from server")
}
if clConn.State() != tcp.StateEstablished {
t.Errorf("client state = %s, want ESTABLISHED", clConn.State())
}
// ACK: client -> server.
if n := exchangeIPv6Once(t, s1, s2, buf); n == 0 {
t.Fatal("expected ACK from client")
}
if svConn.State() != tcp.StateEstablished {
t.Errorf("server state = %s, want ESTABLISHED", svConn.State())
}
}
// TestStack6TCP_DataExchange establishes a TCP connection and transfers data.
func TestStack6TCP_DataExchange(t *testing.T) {
const (
rngseed = 400
svPort = 9090
clPort = 11111
nports = 1
icmpqueue = 1
)
s1, s2 := newStack6Pair(t, rngseed, nports, icmpqueue)
buf := make([]byte, maxFrame6)
svConn := newTCPConn6(t)
clConn := newTCPConn6(t)
listenTCP6(t, s2, svConn, svPort, 300)
if err := s1.DialTCP6(clConn, clPort, s2.Addr6(), svPort, 200); err != nil {
t.Fatal("DialTCP6:", err)
}
// Three-way handshake.
tcp6Handshake(t, s1, s2, buf)
// Send data from client to server.
payload := []byte("hello over tcp6")
if _, err := clConn.Write(payload); err != nil {
t.Fatal("Write:", err)
}
// PSH+ACK: client -> server.
if n := exchangeIPv6Once(t, s1, s2, buf); n == 0 {
t.Fatal("expected data packet from client")
}
// ACK: server -> client.
if n := exchangeIPv6Once(t, s2, s1, buf); n == 0 {
t.Fatal("expected ACK from server")
}
// Drain any extra ACKs.
exchangeIPv6Once(t, s1, s2, buf)
exchangeIPv6Once(t, s2, s1, buf)
var rbuf [256]byte
n, err := svConn.Read(rbuf[:])
if err != nil {
t.Fatal("svConn.Read:", err)
}
if !bytes.Equal(rbuf[:n], payload) {
t.Errorf("server received %q, want %q", rbuf[:n], payload)
}
}
// TestStack6_NDP_DialUDP verifies that DialUDP6 with ICMP enabled triggers NDP
// resolution and that seeding the NDP cache allows the connection to proceed.
func TestStack6_NDP_DialUDP(t *testing.T) {
const (
portA = 7001
portB = 7002
rngseed = 132131
nports = 1
icmpqueue = 4
)
cfg1, cfg2 := stack6PairConfigs(rngseed, nports, icmpqueue)
s1, s2 := newStack6Pair(t, rngseed, nports, icmpqueue)
buf := make([]byte, maxFrame6)
if err := s1.EnableICMP6(true); err != nil {
t.Fatal("s1 EnableICMP6:", err)
}
if err := s2.EnableICMP6(true); err != nil {
t.Fatal("s2 EnableICMP6:", err)
}
// Seed s2's NDP cache so it knows s1's MAC (needed for NA reply).
if err := s2.(*stack6).icmp6.NDPCacheSeed(cfg1.StaticAddress6, cfg1.HardwareAddress); err != nil {
t.Fatal("NDPCacheSeed s2:", err)
}
connA := newUDPConn6(t)
connB := newUDPConn6(t)
// DialUDP6 on s1 queues an NS since s2's MAC is not yet in s1's NDP cache.
if err := s1.DialUDP6(connA, portA, cfg2.StaticAddress6, portB); err != nil {
t.Fatal("DialUDP6 A:", err)
}
// s2 already has s1's address seeded so DialUDP6 resolves immediately.
if err := s2.DialUDP6(connB, portB, cfg1.StaticAddress6, portA); err != nil {
t.Fatal("DialUDP6 B:", err)
}
// NDP exchange: s1 sends Neighbor Solicitation, s2 replies with Neighbor Advertisement.
if n := exchangeIPv6Once(t, s1, s2, buf); n == 0 {
t.Fatal("expected NDP Neighbor Solicitation from s1")
}
if n := exchangeIPv6Once(t, s2, s1, buf); n == 0 {
t.Fatal("expected NDP Neighbor Advertisement from s2")
}
// After NDP resolved, connA's macBuf is patched; verify the cache now has s2's MAC.
mac, err := s1.(*stack6).icmp6.NDPCacheLookup(cfg2.StaticAddress6)
if err != nil {
t.Fatal("NDPCacheLookup after NDP exchange:", err)
}
if mac != cfg2.HardwareAddress {
t.Errorf("NDP resolved MAC = %v, want %v", mac, cfg2.HardwareAddress)
}
// Now that NDP is resolved, data should flow.
want := []byte("ndp resolved udp")
if _, err := connA.Write(want); err != nil {
t.Fatal("Write:", err)
}
if n := exchangeIPv6Once(t, s1, s2, buf); n == 0 {
t.Fatal("expected UDP packet after NDP resolution")
}
var rbuf [256]byte
n, err := connB.Read(rbuf[:])
if err != nil {
t.Fatal("Read:", err)
}
if !bytes.Equal(rbuf[:n], want) {
t.Errorf("got %q, want %q", rbuf[:n], want)
}
}
// tcp6Handshake performs the SYN/SYNACK/ACK exchange between two stacks.
func tcp6Handshake(t testing.TB, client, server Stack6, buf []byte) {
t.Helper()
if n := exchangeIPv6Once(t, client, server, buf); n == 0 {
t.Fatal("handshake: expected SYN")
}
if n := exchangeIPv6Once(t, server, client, buf); n == 0 {
t.Fatal("handshake: expected SYNACK")
}
if n := exchangeIPv6Once(t, client, server, buf); n == 0 {
t.Fatal("handshake: expected ACK")
}
}
// TestStack6_EgressNoData checks that EgressIPv6 returns 0 when there is nothing to send.
func TestStack6_EgressNoData(t *testing.T) {
s, _ := newStack6Pair(t, 13213, 2, 2)
buf := make([]byte, maxFrame6)
n, err := s.EgressIPv6(buf)
if err != nil {
t.Errorf("EgressIPv6 with no traffic: %v", err)
}
if n != 0 {
t.Errorf("expected 0 bytes, got %d", n)
}
}
// TestStack6_IngressDropsWrongDst checks that packets destined for a different address are dropped.
func TestStack6_IngressDropsWrongDst(t *testing.T) {
s1, s2 := newStack6Pair(t, 2133213, 1, 1)
buf := make([]byte, maxFrame6)
connA := newUDPConn6(t)
connB := newUDPConn6(t)
const (
portA = 4001
portB = 4002
)
if err := s1.DialUDP6(connA, portA, s2.Addr6(), portB); err != nil {
t.Fatal(err)
}
if err := s2.DialUDP6(connB, portB, s1.Addr6(), portA); err != nil {
t.Fatal(err)
}
// Write and encapsulate from s1 (destination = testAddr6B).
if _, err := connA.Write([]byte("drop me")); err != nil {
t.Fatal(err)
}
n, err := s1.EgressIPv6(buf)
if err != nil || n == 0 {
t.Fatalf("expected encapsulated packet: n=%d err=%v", n, err)
}
// Feed to s1 itself (wrong destination) should be dropped (ErrPacketDrop or similar).
wrongDst := s1
if err := wrongDst.IngressIPv6(buf[:n]); err == nil {
t.Error("expected error when delivering packet to wrong destination stack, got nil")
}
// Deliver correctly to s2 should succeed.
if err := s2.IngressIPv6(buf[:n]); err != nil {
t.Errorf("correct delivery to s2 failed: %v", err)
}
// s2 should have received the datagram.
var rbuf [256]byte
rn, err := connB.Read(rbuf[:])
if err != nil || rn == 0 {
t.Errorf("expected s2 to have received data: n=%d err=%v", rn, err)
}
}