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
+1 -1
View File
@@ -128,7 +128,7 @@ func (ls *StackEthernet) LocalPort() uint16 { return 0 }
func (ls *StackEthernet) Protocol() uint64 { return 1 }
func (ls *StackEthernet) Register(h lneto.StackNode) error {
func (ls *StackEthernet) RegisterEthernet(h lneto.StackNode) error {
proto := h.Protocol()
if proto > math.MaxUint16 || proto <= 1500 {
return lneto.ErrInvalidConfig
-68
View File
@@ -1,68 +0,0 @@
package internet
import (
"log/slog"
"github.com/soypat/lneto"
"github.com/soypat/lneto/ethernet"
)
var _ lneto.StackNode = (*StackIP)(nil)
type StackIP struct {
connID uint64
stackip4
stackip6
}
func (stackip *StackIP) Reset(vld *lneto.Validator, maxNodes4, maxNodes6 int) error {
if maxNodes4 <= 0 && maxNodes6 <= 0 || vld == nil {
return lneto.ErrInvalidConfig
}
stackip.connID++
stackip.reset4(vld, maxNodes4)
stackip.reset6(vld, maxNodes6)
return nil
}
func (stackip *StackIP) ConnectionID() *uint64 {
return &stackip.connID
}
func (stackip *StackIP) Protocol() uint64 {
return uint64(ethernet.TypeIPv4) // Only support ipv4 for now.
}
func (stackip *StackIP) LocalPort() uint16 { return 0 }
func (stackip *StackIP) SetLogger(logger *slog.Logger) {
stackip.stackip4.handlers.log = logger
stackip.stackip6.handlers.log = logger
}
func (stackip *StackIP) Demux(carrierData []byte, offset int) error {
debugLog("ip:demux")
if len(carrierData) < 1 {
return lneto.ErrTruncatedFrame
}
version := carrierData[offset] >> 4
switch version {
case 4:
return stackip.stackip4.demux4(carrierData, offset)
case 6:
return stackip.stackip6.demux6(carrierData, offset)
default:
return lneto.ErrUnsupported
}
}
func (stackip *StackIP) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int) (n int, err error) {
if offsetToFrame != offsetToIP {
return 0, lneto.ErrBug
}
n, err = stackip.stackip4.encapsulate4(carrierData, offsetToIP)
if len(stackip.stackip6.handlers.nodes) > 0 && n == 0 {
n, err = stackip.stackip6.encapsulate6(carrierData, offsetToIP)
}
return n, err
}
+37
View File
@@ -5,6 +5,7 @@ import (
"log/slog"
"github.com/soypat/lneto"
"github.com/soypat/lneto/ethernet"
"github.com/soypat/lneto/internal"
"github.com/soypat/lneto/ipv4"
"github.com/soypat/lneto/tcp"
@@ -15,6 +16,42 @@ import (
// It is meant to be embedded within StackNodes.
// var _ lneto.StackNode = (*stackip4)(nil)
type StackIPv4 struct {
connID uint64
stackip4
}
func (stackip4 *StackIPv4) Reset(vld *lneto.Validator, maxNodes int) error {
stackip4.reset4(vld, maxNodes)
return nil
}
func (stackip *StackIPv4) ConnectionID() *uint64 {
return &stackip.connID
}
func (stackip *StackIPv4) Protocol() uint64 {
return uint64(ethernet.TypeIPv4)
}
func (stackip *StackIPv4) LocalPort() uint16 { return 0 }
func (stackip *StackIPv4) SetLogger(logger *slog.Logger) {
stackip.stackip4.handlers.log = logger
}
func (stackip *StackIPv4) Demux(carrierData []byte, offset int) error {
debugLog("ip:demux")
return stackip.stackip4.demux4(carrierData, offset)
}
func (stackip *StackIPv4) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int) (n int, err error) {
if offsetToFrame != offsetToIP {
return 0, lneto.ErrBug
}
return stackip.stackip4.encapsulate4(carrierData, offsetToIP)
}
type stackip4 struct {
handlers handlers
vld *lneto.Validator
+36
View File
@@ -4,6 +4,7 @@ import (
"log/slog"
"github.com/soypat/lneto"
"github.com/soypat/lneto/ethernet"
"github.com/soypat/lneto/ipv6"
"github.com/soypat/lneto/tcp"
"github.com/soypat/lneto/udp"
@@ -12,6 +13,41 @@ import (
// stackip6 is NOT a StackNode implementation.
// It is meant to be embedded within StackNodes.
// var _ lneto.StackNode = (*stackip6)(nil)
type StackIPv6 struct {
connID uint64
stackip6
}
func (stackip4 *StackIPv6) Reset(vld *lneto.Validator, maxNodes int) error {
stackip4.reset6(vld, maxNodes)
return nil
}
func (stackip *StackIPv6) ConnectionID() *uint64 {
return &stackip.connID
}
func (stackip *StackIPv6) Protocol() uint64 {
return uint64(ethernet.TypeIPv6)
}
func (stackip *StackIPv6) LocalPort() uint16 { return 0 }
func (stackip *StackIPv6) SetLogger(logger *slog.Logger) {
stackip.stackip6.handlers.log = logger
}
func (stackip *StackIPv6) Demux(carrierData []byte, offset int) error {
debugLog("ip:demux")
return stackip.stackip6.demux6(carrierData, offset)
}
func (stackip *StackIPv6) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int) (n int, err error) {
if offsetToFrame != offsetToIP {
return 0, lneto.ErrBug
}
return stackip.stackip6.encapsulate6(carrierData, offsetToIP)
}
type stackip6 struct {
handlers handlers
+11 -9
View File
@@ -33,8 +33,6 @@ func (ps *StackPorts) ResetTCP(maxNodes uint16) error {
func (ps *StackPorts) Reset(protocol uint64, dstPortOffset, maxNodes uint16) error {
if protocol > math.MaxUint16 {
return lneto.ErrInvalidConfig
} else if maxNodes <= 0 {
return lneto.ErrInvalidConfig
}
ps.handlers.reset("StackPorts(proto="+strconv.Itoa(int(protocol))+")", int(maxNodes))
*ps = StackPorts{
@@ -105,25 +103,29 @@ type StackPortsMACFiltered struct {
sp StackPorts
}
func (mfsp *StackPortsMACFiltered) Register(h lneto.StackNode, addr []byte) error {
func (mfsp *StackPortsMACFiltered) RegisterMACFiltered(h lneto.StackNode, macAddr []byte) error {
// TODO(soypat): We can likely constrain memory and the slice lifetime if StackPortsMACFiltered owns it
// or better yet, if the handlers node slice owns the memory. We need to think carefully of who has write access (the ARP and NDP handlers)
// and make sure that they never write after the connection has been terminated. Idea:
// RegisterMACFiltered(h lneto.StackNode, filterMAC bool) (macAddr *[6]byte, connIDthing *uint8, err error)
port := h.LocalPort()
proto := h.Protocol()
if port <= 0 {
return lneto.ErrZeroSource
} else if proto != uint64(mfsp.sp.protocol) {
return lneto.ErrInvalidConfig
} else if addr != nil && len(addr) != 6 {
} else if macAddr != nil && len(macAddr) != 6 {
return lneto.ErrInvalidAddr
}
return mfsp.sp.handlers.registerByPortProto(nodeFromStackNode(h, port, proto, addr))
return mfsp.sp.handlers.registerByPortProto(nodeFromStackNode(h, port, proto, macAddr))
}
func (ps *StackPortsMACFiltered) ResetUDP(maxNodes uint16) error {
return ps.sp.ResetUDP(maxNodes)
func (ps *StackPortsMACFiltered) ResetUDP(maxNodes uint16) {
ps.sp.ResetUDP(maxNodes) // Can't error.
}
func (ps *StackPortsMACFiltered) ResetTCP(maxNodes uint16) error {
return ps.sp.ResetTCP(maxNodes)
func (ps *StackPortsMACFiltered) ResetTCP(maxNodes uint16) {
ps.sp.ResetTCP(maxNodes) // Can't error.
}
func (ps *StackPortsMACFiltered) Reset(protocol uint64, dstPortOffset, maxNodes uint16) error {
+13 -13
View File
@@ -11,7 +11,7 @@ import (
func TestBasicStack(t *testing.T) {
rng := rand.New(rand.NewSource(1))
var sbCl, sbSv StackIP
var sbCl, sbSv StackIPv4
var connCl, connSv tcp.Conn
setupClientServer(t, rng, &sbCl, &sbSv, &connCl, &connSv)
var buf [2048]byte
@@ -37,13 +37,13 @@ func TestBasicStack(t *testing.T) {
func TestBasicStack2(t *testing.T) {
rng := rand.New(rand.NewSource(1))
var sbCl, sbSv StackIP
var sbCl, sbSv StackIPv4
var connCl, connSv tcp.Conn
setupClientServerEstablished(t, rng, &sbCl, &sbSv, &connCl, &connSv)
}
func expectExchange(t *testing.T, from, to *StackIP, buf []byte) {
func expectExchange(t *testing.T, from, to lneto.StackNode, buf []byte) {
t.Helper()
n, err := from.Encapsulate(buf, 0, 0)
if err != nil {
@@ -58,13 +58,13 @@ func expectExchange(t *testing.T, from, to *StackIP, buf []byte) {
}
}
func setupClientServerEstablished(t *testing.T, rng *rand.Rand, client, server *StackIP, connClient, connServer *tcp.Conn) {
func setupClientServerEstablished(t *testing.T, rng *rand.Rand, client, server *StackIPv4, connClient, connServer *tcp.Conn) {
t.Helper()
setupClientServer(t, rng, client, server, connClient, connServer)
testClientServerEstablish(t, client, server, connClient, connServer)
}
func testClientServerEstablish(t *testing.T, client, server *StackIP, connClient, connServer *tcp.Conn) {
func testClientServerEstablish(t *testing.T, client, server lneto.StackNode, connClient, connServer *tcp.Conn) {
t.Helper()
var buf [2048]byte
nextToSend := client
@@ -93,7 +93,7 @@ func testClientServerEstablish(t *testing.T, client, server *StackIP, connClient
func TestBasicStack6(t *testing.T) {
rng := rand.New(rand.NewSource(1))
var sbCl, sbSv StackIP
var sbCl, sbSv StackIPv6
var connCl, connSv tcp.Conn
setupClientServer6(t, rng, &sbCl, &sbSv, &connCl, &connSv)
var buf [2048]byte
@@ -119,13 +119,13 @@ func TestBasicStack6(t *testing.T) {
func TestBasicStack6Established(t *testing.T) {
rng := rand.New(rand.NewSource(1))
var sbCl, sbSv StackIP
var sbCl, sbSv StackIPv6
var connCl, connSv tcp.Conn
setupClientServer6(t, rng, &sbCl, &sbSv, &connCl, &connSv)
testClientServerEstablish(t, &sbCl, &sbSv, &connCl, &connSv)
}
func setupClientServer6(t *testing.T, rng *rand.Rand, client, server *StackIP, connClient, connServer *tcp.Conn) {
func setupClientServer6(t *testing.T, rng *rand.Rand, client, server *StackIPv6, connClient, connServer *tcp.Conn) {
t.Helper()
_ = rng
const maxNodes = 1
@@ -134,10 +134,10 @@ func setupClientServer6(t *testing.T, rng *rand.Rand, client, server *StackIP, c
clip6 := netip.AddrFrom16([16]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2}) // 2001:db8::2
svip := netip.AddrPortFrom(svip6, 80)
clip := netip.AddrPortFrom(clip6, 1337)
if err := server.Reset(new(lneto.Validator), 0, maxNodes); err != nil {
if err := server.Reset(new(lneto.Validator), maxNodes); err != nil {
t.Fatal(err)
}
if err := client.Reset(new(lneto.Validator), 0, maxNodes); err != nil {
if err := client.Reset(new(lneto.Validator), maxNodes); err != nil {
t.Fatal(err)
}
server.SetAddr6(svip6.As16())
@@ -172,14 +172,14 @@ func setupClientServer6(t *testing.T, rng *rand.Rand, client, server *StackIP, c
}
}
func setupClientServer(t *testing.T, rng *rand.Rand, client, server *StackIP, connClient, connServer *tcp.Conn) {
func setupClientServer(t *testing.T, rng *rand.Rand, client, server *StackIPv4, connClient, connServer *tcp.Conn) {
const maxNodes = 1
bufsize := 2048
// Ensure buffer sizes are OK with reused buffers.
svip := netip.AddrPortFrom(netip.AddrFrom4([4]byte{192, 168, 1, 0}), 80)
clip := netip.AddrPortFrom(netip.AddrFrom4([4]byte{192, 168, 1, 1}), 1337)
server.Reset(new(lneto.Validator), maxNodes, 0)
client.Reset(new(lneto.Validator), maxNodes, 0)
server.Reset(new(lneto.Validator), maxNodes)
client.Reset(new(lneto.Validator), maxNodes)
server.SetAddr4(svip.Addr().As4())
client.SetAddr4(clip.Addr().As4())
err := connServer.Configure(tcp.ConnConfig{
+9 -19
View File
@@ -12,7 +12,7 @@ import (
func TestListener_SingleConnection(t *testing.T) {
rng := rand.New(rand.NewSource(1))
var clientStack, serverStack StackIP
var clientStack, serverStack StackIPv4
var clientConn, serverConn tcp.Conn
var listener tcp.Listener
@@ -66,7 +66,7 @@ func TestListener_SingleConnection(t *testing.T) {
func TestListener_AcceptAfterEstablished(t *testing.T) {
rng := rand.New(rand.NewSource(1))
var client1Stack, serverStack StackIP
var client1Stack, serverStack StackIPv4
var client1Conn, serverConn tcp.Conn
var listener tcp.Listener
pool := newMockTCPPool(2, 3, 2048)
@@ -104,7 +104,7 @@ func TestListener_AcceptAfterEstablished(t *testing.T) {
}
// Setup second client and verify we can still accept.
var client2Stack StackIP
var client2Stack StackIPv4
var client2Conn tcp.Conn
setupClient(t, &client2Stack, &client2Conn, netip.AddrFrom4(serverStack.Addr4()), serverPort, 1338)
@@ -131,13 +131,13 @@ func TestListener_AcceptAfterEstablished(t *testing.T) {
func TestListener_MultiConn(t *testing.T) {
const numClients = 5
rng := rand.New(rand.NewSource(1))
var serverStack StackIP
var serverStack StackIPv4
var serverConn tcp.Conn
var listener tcp.Listener
pool := newMockTCPPool(numClients, 3, 2048)
// Create slices for clients.
clientStacks := make([]StackIP, numClients)
clientStacks := make([]StackIPv4, numClients)
clientConns := make([]tcp.Conn, numClients)
acceptedConns := make([]*tcp.Conn, numClients)
@@ -257,7 +257,7 @@ func TestListener_MultiConn(t *testing.T) {
// Close connections, alternating between client-initiated and server-initiated.
for i := range numClients {
var closer, responder *StackIP
var closer, responder *StackIPv4
var closerConn, responderConn *tcp.Conn
var serverClosed bool
whoCloses := "client"
@@ -313,7 +313,7 @@ func TestListener_MultiConn(t *testing.T) {
func TestListener_RSTOnPoolExhaustion(t *testing.T) {
rng := rand.New(rand.NewSource(1))
var client1Stack, client2Stack, serverStack StackIP
var client1Stack, client2Stack, serverStack StackIPv4
var client1Conn, client2Conn, serverConn tcp.Conn
var listener tcp.Listener
@@ -606,21 +606,11 @@ func TestStackPorts_ECN_SYN_RST(t *testing.T) {
}
}
// tryExchange attempts an exchange but doesn't fail if no data to send.
func tryExchange(t *testing.T, from, to *StackIP, buf []byte) {
t.Helper()
n, err := from.Encapsulate(buf, -1, 0)
if err != nil || n == 0 {
return // No data to send.
}
_ = to.Demux(buf[:n], 0) // Ignore errors during close.
}
func setupClient(t *testing.T, client *StackIP, conn *tcp.Conn, serverAddr netip.Addr, serverPort, clientPort uint16) {
func setupClient(t *testing.T, client *StackIPv4, conn *tcp.Conn, serverAddr netip.Addr, serverPort, clientPort uint16) {
t.Helper()
bufsize := 2048
clientIP := netip.AddrFrom4([4]byte{192, 168, 1, byte(clientPort % 256)})
client.Reset(new(lneto.Validator), 1, 0)
client.Reset(new(lneto.Validator), 1)
client.SetAddr4(clientIP.As4())
err := conn.Configure(tcp.ConnConfig{
RxBuf: make([]byte, bufsize),