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
This commit is contained in:
Pat Whittingslow
2026-05-13 15:31:18 -03:00
committed by GitHub
parent 7d5830d7ab
commit a2970b923d
44 changed files with 1938 additions and 395 deletions
+618
View File
@@ -0,0 +1,618 @@
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)
}
}