Files
lneto/x/xnet/xnet_udppktconn_test.go
T
Pat Whittingslow 6ba06acdcb lneto rework: Require explicit BackoffStrategy in all APIs (#116)
* make backoff explicit API

* fix tests to use explicit backoff

* fix examples with explicit tcp
2026-06-09 10:20:40 -03:00

318 lines
9.5 KiB
Go

package xnet
import (
"bytes"
"net/netip"
"testing"
"time"
"github.com/soypat/lneto/ethernet"
"github.com/soypat/lneto/udp"
)
const (
testUDPBufSize = 2048
testUDPQueueSize = 4
)
// newUDPTestPair creates a server/client StackAsync pair for UDP tests with
// static addresses and each stack's gateway pointing at the peer.
func newUDPTestPair(t testing.TB, seed int64) (s1, s2 *StackAsync) {
t.Helper()
s1, s2 = new(StackAsync), new(StackAsync)
if err := s1.Reset(StackConfig{
Hostname: "UDP-1",
RandSeed: seed,
StaticAddress4: [4]byte{10, 1, 0, 1},
HardwareAddress: [6]byte{0xaa, 0xbb, 0, 0, 0, 1},
MTU: ethernet.MaxMTU,
MaxActiveUDPPorts: 4,
}); err != nil {
t.Fatal("sv Reset:", err)
}
if err := s2.Reset(StackConfig{
Hostname: "UDP-2",
RandSeed: ^seed,
StaticAddress4: [4]byte{10, 1, 0, 2},
HardwareAddress: [6]byte{0xaa, 0xbb, 0, 0, 0, 2},
MTU: ethernet.MaxMTU,
MaxActiveUDPPorts: 4,
}); err != nil {
t.Fatal("cl Reset:", err)
}
s1.SetGatewayHardwareAddr(s2.HardwareAddr())
s2.SetGatewayHardwareAddr(s1.HardwareAddr())
return s1, s2
}
// TestStackAsyncRegisterListenerUDP_ReceiveData registers a udp.PacketConn as
// a listener and verifies that a datagram from a dialed client is delivered
// with the correct payload and sender address.
func TestStackAsyncRegisterListenerUDP_ReceiveData(t *testing.T) {
const (
svPort = 9000
clPort = 9001
)
sv, cl := newUDPTestPair(t, 1234)
buf := make([]byte, ethernet.MaxMTU+ethernet.MaxOverheadSize)
var pc udp.PacketConn
if err := pc.Configure(udp.PacketConnConfig{
RxBuf: make([]byte, testUDPBufSize), TxBuf: make([]byte, testUDPBufSize),
RxQueueSize: testUDPQueueSize, TxQueueSize: testUDPQueueSize,
RWBackoff: backoffYield,
}); err != nil {
t.Fatal("pc Configure:", err)
}
if err := pc.Open(netip.AddrPortFrom(netip.AddrFrom4(sv.Addr4()), svPort)); err != nil {
t.Fatal("pc.Open:", err)
}
if err := sv.RegisterListenerUDP(&pc); err != nil {
t.Fatal("RegisterListenerUDP:", err)
}
var conn udp.Conn
if err := conn.Configure(udp.ConnConfig{
RxBuf: make([]byte, testUDPBufSize), TxBuf: make([]byte, testUDPBufSize),
RxQueueSize: testUDPQueueSize, TxQueueSize: testUDPQueueSize,
RWBackoff: backoffYield,
}); err != nil {
t.Fatal("conn Configure:", err)
}
if err := cl.DialUDP4(&conn, clPort, sv.Addr4(), svPort); err != nil {
t.Fatal("DialUDP4:", err)
}
want := []byte("hello listener")
if _, err := conn.Write(want); err != nil {
t.Fatal("Write:", err)
}
if exchangeEthernetOnce(t, cl, sv, buf) == 0 {
t.Fatal("no packet sent by client")
}
pc.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
var rbuf [testUDPBufSize]byte
n, senderAddr, err := pc.ReadFrom(rbuf[:])
if err != nil {
t.Fatal("ReadFrom:", err)
}
if !bytes.Equal(rbuf[:n], want) {
t.Errorf("data: got %q, want %q", rbuf[:n], want)
}
wantSender := netip.AddrPortFrom(netip.AddrFrom4(cl.Addr4()), clPort)
if senderAddr != wantSender {
t.Errorf("sender addr: got %v, want %v", senderAddr, wantSender)
}
}
// TestStackAsyncRegisterListenerUDP_ReplyToClient registers a PacketConn, receives
// a datagram, then calls WriteTo to send a reply back to the sender and verifies
// the client's udp.Conn reads it.
func TestStackAsyncRegisterListenerUDP_ReplyToClient(t *testing.T) {
const (
svPort = 9002
clPort = 9003
)
sv, cl := newUDPTestPair(t, 5678)
buf := make([]byte, ethernet.MaxMTU+ethernet.MaxOverheadSize)
var pc udp.PacketConn
if err := pc.Configure(udp.PacketConnConfig{
RxBuf: make([]byte, testUDPBufSize), TxBuf: make([]byte, testUDPBufSize),
RxQueueSize: testUDPQueueSize, TxQueueSize: testUDPQueueSize,
RWBackoff: backoffYield,
}); err != nil {
t.Fatal("pc Configure:", err)
}
if err := pc.Open(netip.AddrPortFrom(netip.AddrFrom4(sv.Addr4()), svPort)); err != nil {
t.Fatal("pc.Open:", err)
}
if err := sv.RegisterListenerUDP(&pc); err != nil {
t.Fatal("RegisterListenerUDP:", err)
}
var conn udp.Conn
if err := conn.Configure(udp.ConnConfig{
RxBuf: make([]byte, testUDPBufSize), TxBuf: make([]byte, testUDPBufSize),
RxQueueSize: testUDPQueueSize, TxQueueSize: testUDPQueueSize,
RWBackoff: backoffYield,
}); err != nil {
t.Fatal("conn Configure:", err)
}
if err := cl.DialUDP4(&conn, clPort, sv.Addr4(), svPort); err != nil {
t.Fatal("DialUDP4:", err)
}
// Client → Server
if _, err := conn.Write([]byte("ping")); err != nil {
t.Fatal("Write:", err)
}
exchangeEthernetOnce(t, cl, sv, buf)
pc.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
var rbuf [testUDPBufSize]byte
_, senderAddr, err := pc.ReadFrom(rbuf[:])
if err != nil {
t.Fatal("ReadFrom:", err)
}
// Server → Client
reply := []byte("pong")
pc.SetWriteDeadline(time.Now().Add(100 * time.Millisecond))
if _, err := pc.WriteTo(reply, senderAddr); err != nil {
t.Fatal("WriteTo:", err)
}
if exchangeEthernetOnce(t, sv, cl, buf) == 0 {
t.Fatal("no reply packet from server")
}
var rbuf2 [testUDPBufSize]byte
n, err := conn.Read(rbuf2[:])
if err != nil {
t.Fatal("conn.Read:", err)
}
if !bytes.Equal(rbuf2[:n], reply) {
t.Errorf("reply data: got %q, want %q", rbuf2[:n], reply)
}
}
// TestStackAsyncRegisterListenerUDP_MultiSource verifies that a single PacketConn
// registered via RegisterListenerUDP (no MAC filter) receives datagrams from two
// distinct clients and ReadFrom returns the correct sender address for each.
func TestStackAsyncRegisterListenerUDP_MultiSource(t *testing.T) {
const (
svPort = 9004
clPort = 9005
)
buf := make([]byte, ethernet.MaxMTU+ethernet.MaxOverheadSize)
sv := new(StackAsync)
if err := sv.Reset(StackConfig{
Hostname: "UDPSvMS",
RandSeed: 999,
StaticAddress4: [4]byte{10, 1, 0, 1},
HardwareAddress: [6]byte{0xaa, 0xbb, 0, 0, 0, 1},
MTU: ethernet.MaxMTU,
MaxActiveUDPPorts: 1,
}); err != nil {
t.Fatal("sv Reset:", err)
}
cl1 := new(StackAsync)
cl1Addr := [4]byte{10, 1, 0, 2}
if err := cl1.Reset(StackConfig{
Hostname: "UDPCl1",
RandSeed: 111,
StaticAddress4: cl1Addr,
HardwareAddress: [6]byte{0xaa, 0xbb, 0, 0, 0, 2},
MTU: ethernet.MaxMTU,
MaxActiveUDPPorts: 1,
}); err != nil {
t.Fatal("cl1 Reset:", err)
}
cl2 := new(StackAsync)
cl2Addr := [4]byte{10, 1, 0, 3}
if err := cl2.Reset(StackConfig{
Hostname: "UDPCl2",
RandSeed: 222,
StaticAddress4: cl2Addr,
HardwareAddress: [6]byte{0xaa, 0xbb, 0, 0, 0, 3},
MTU: ethernet.MaxMTU,
MaxActiveUDPPorts: 1,
}); err != nil {
t.Fatal("cl2 Reset:", err)
}
cl1.SetGatewayHardwareAddr(sv.HardwareAddr())
cl2.SetGatewayHardwareAddr(sv.HardwareAddr())
var pc udp.PacketConn
if err := pc.Configure(udp.PacketConnConfig{
RxBuf: make([]byte, testUDPBufSize), TxBuf: make([]byte, testUDPBufSize),
RxQueueSize: testUDPQueueSize, TxQueueSize: testUDPQueueSize,
RWBackoff: backoffYield,
}); err != nil {
t.Fatal("pc Configure:", err)
}
if err := pc.Open(netip.AddrPortFrom(netip.AddrFrom4(sv.Addr4()), svPort)); err != nil {
t.Fatal("pc.Open:", err)
}
if err := sv.RegisterListenerUDP(&pc); err != nil {
t.Fatal("RegisterListenerUDP:", err)
}
// Client 1 dials and sends.
var conn1 udp.Conn
if err := conn1.Configure(udp.ConnConfig{
RxBuf: make([]byte, testUDPBufSize), TxBuf: make([]byte, testUDPBufSize),
RxQueueSize: testUDPQueueSize, TxQueueSize: testUDPQueueSize,
RWBackoff: backoffYield,
}); err != nil {
t.Fatal("conn1 Configure:", err)
}
if err := cl1.DialUDP4(&conn1, clPort, sv.Addr4(), svPort); err != nil {
t.Fatal("cl1 DialUDP4:", err)
}
msg1 := []byte("from-client-1")
if _, err := conn1.Write(msg1); err != nil {
t.Fatal("conn1 Write:", err)
}
exchangeEthernetOnce(t, cl1, sv, buf)
// Client 2 dials and sends.
var conn2 udp.Conn
if err := conn2.Configure(udp.ConnConfig{
RxBuf: make([]byte, testUDPBufSize), TxBuf: make([]byte, testUDPBufSize),
RxQueueSize: testUDPQueueSize, TxQueueSize: testUDPQueueSize,
RWBackoff: backoffYield,
}); err != nil {
t.Fatal("conn2 Configure:", err)
}
if err := cl2.DialUDP4(&conn2, clPort, sv.Addr4(), svPort); err != nil {
t.Fatal("cl2 DialUDP4:", err)
}
msg2 := []byte("from-client-2")
if _, err := conn2.Write(msg2); err != nil {
t.Fatal("conn2 Write:", err)
}
exchangeEthernetOnce(t, cl2, sv, buf)
// Server reads both datagrams.
pc.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
type recv struct {
data []byte
from netip.AddrPort
}
var got [2]recv
var rbuf [testUDPBufSize]byte
for i := range 2 {
n, addr, err := pc.ReadFrom(rbuf[:])
if err != nil {
t.Fatalf("ReadFrom[%d]: %v", i, err)
}
got[i] = recv{data: bytes.Clone(rbuf[:n]), from: addr}
}
wantFrom1 := netip.AddrPortFrom(netip.AddrFrom4(cl1Addr), clPort)
wantFrom2 := netip.AddrPortFrom(netip.AddrFrom4(cl2Addr), clPort)
// Datagrams may arrive in either order.
if got[0].from == wantFrom1 {
if !bytes.Equal(got[0].data, msg1) {
t.Errorf("[0] data: got %q, want %q", got[0].data, msg1)
}
if got[1].from != wantFrom2 || !bytes.Equal(got[1].data, msg2) {
t.Errorf("[1]: got addr=%v data=%q, want addr=%v data=%q", got[1].from, got[1].data, wantFrom2, msg2)
}
} else {
if got[0].from != wantFrom2 || !bytes.Equal(got[0].data, msg2) {
t.Errorf("[0]: got addr=%v data=%q, want addr=%v data=%q", got[0].from, got[0].data, wantFrom2, msg2)
}
if got[1].from != wantFrom1 || !bytes.Equal(got[1].data, msg1) {
t.Errorf("[1]: got addr=%v data=%q, want addr=%v data=%q", got[1].from, got[1].data, wantFrom1, msg1)
}
}
}