Add tcp.Listener (#15)

* begin reviewing TCP listener

* begin adding listener tests

* adding TestExchange implementation

* split out legacy and Step tests

* move listener to tcp package

* fix up tcp tests taking very long

* add xnet.TCPPool and work on TCPPool semantics

* xnet: TCPPool only accepts established connections

* xnet: getting to bottom of panic in xnet.Listener

* xnet: improve listener tests

* fix several data races in testing fixtures

* fix more synchronization things in listener test

* finalize tcplistener test
This commit is contained in:
Pat Whittingslow
2026-01-03 12:41:57 -03:00
committed by GitHub
parent 5731a378dd
commit 018f9258ac
19 changed files with 2288 additions and 849 deletions
-233
View File
@@ -1,233 +0,0 @@
package internet
import (
"bytes"
"errors"
"log/slog"
"net"
"github.com/soypat/lneto"
"github.com/soypat/lneto/internal"
"github.com/soypat/lneto/tcp"
)
var _ StackNode = (*NodeTCPListener)(nil)
type tcpPool interface {
GetTCP() (*tcp.Conn, tcp.Value)
PutTCP(*tcp.Conn)
}
type NodeTCPListener struct {
connID uint64
// ready have received a
ready []*tcp.Conn
accepted []*tcp.Conn
port uint16
poolGet func() (*tcp.Conn, tcp.Value)
poolReturn func(*tcp.Conn)
}
// LocalPort implements [StackNode].
func (listener *NodeTCPListener) LocalPort() uint16 { return listener.port }
// ConnectionID implements [StackNode].
func (listener *NodeTCPListener) ConnectionID() *uint64 { return &listener.connID }
// Protocol implements [StackNode].
func (listener *NodeTCPListener) Protocol() uint64 { return uint64(lneto.IPProtoTCP) }
func (listener *NodeTCPListener) Close() error {
if listener.isClosed() {
return errors.New("already closed")
}
listener.connID++
listener.port = 0
return nil
}
func (listener *NodeTCPListener) Reset(port uint16, pool tcpPool) error {
if port == 0 {
return errZeroPort
} else if pool == nil {
return errors.New("nil TCP pool")
}
*listener = NodeTCPListener{
connID: listener.connID + 1,
port: port,
poolGet: pool.GetTCP,
poolReturn: pool.PutTCP,
ready: listener.ready[:0],
accepted: listener.accepted[:0],
}
return nil
}
func (listener *NodeTCPListener) NumberOfReadyToAccept() (nready int) {
if listener.isClosed() {
return 0
}
for _, conn := range listener.ready {
if conn == nil {
continue
}
nready++
}
return nready
}
func (listener *NodeTCPListener) TryAccept() (*tcp.Conn, error) {
if listener.isClosed() {
return nil, net.ErrClosed
}
listener.maintainConns()
for i, conn := range listener.ready {
if conn == nil {
continue
}
listener.accepted = append(listener.accepted, conn)
listener.ready[i] = nil // discard from ready.
return conn, nil
}
return nil, errors.New("no conns available")
}
// Encapsulate implements [StackNode].
func (listener *NodeTCPListener) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int) (int, error) {
if listener.isClosed() {
return 0, net.ErrClosed
}
for i, conn := range listener.accepted {
if conn == nil {
continue
}
n, err := conn.Encapsulate(carrierData, offsetToIP, offsetToFrame)
if err != nil {
err = listener.maintainConn(listener.accepted, i, err)
}
if n == 0 {
continue
}
return n, err
}
return 0, nil
}
// Demux implements [StackNode].
func (listener *NodeTCPListener) Demux(carrierData []byte, tcpFrameOffset int) error {
if listener.isClosed() {
return net.ErrClosed
}
tfrm, err := tcp.NewFrame(carrierData[tcpFrameOffset:])
if err != nil {
return err
}
srcaddr, _, _, _, err := internal.GetIPAddr(carrierData)
if err != nil {
return err
}
dst := tfrm.DestinationPort()
if dst != listener.port {
return errors.New("not our port")
}
src := tfrm.SourcePort()
// Try to demux in accepted:
demuxed, err := listener.tryDemux(listener.accepted, src, srcaddr, carrierData, tcpFrameOffset)
if demuxed {
return err
}
demuxed, err = listener.tryDemux(listener.ready, src, srcaddr, carrierData, tcpFrameOffset)
if demuxed {
return err
}
// Connection not in ready nor accepted.
_, flags := tfrm.OffsetAndFlags()
if flags != tcp.FlagSYN {
return nil // Not a synchronizing packet, drop it.
}
conn, iss := listener.poolGet()
if conn == nil {
slog.Error("tcpListener:no-free-conn")
return nil
}
err = conn.OpenListen(dst, iss)
if err != nil {
slog.Error("NodeTCPListener:open", slog.String("err", err.Error()))
return err // This should not happend
}
err = conn.Demux(carrierData, tcpFrameOffset)
if err != nil {
conn.Abort()
slog.Error("NodeTCPListener:demux", slog.String("err", err.Error()))
return nil
}
listener.ready = append(listener.ready, conn)
return nil
}
func (listener *NodeTCPListener) tryDemux(conns []*tcp.Conn, remotePort uint16, remoteAddr, carrierData []byte, tcpFrameOffset int) (demuxed bool, err error) {
idx := getConn(conns, remotePort, remoteAddr)
if idx >= 0 {
err := conns[idx].Demux(carrierData, tcpFrameOffset)
if err != nil {
err = listener.maintainConn(conns, idx, err)
}
return true, err
}
return false, nil
}
func (listener *NodeTCPListener) maintainAccepted(connIdx int, err error) {
if err == net.ErrClosed {
conn := listener.accepted[connIdx]
listener.poolReturn(conn)
listener.accepted[connIdx] = nil
}
}
func (listener *NodeTCPListener) isClosed() bool {
return listener.port == 0
}
func (listener *NodeTCPListener) maintainConns() {
listener.accepted = removeZeros(listener.accepted)
listener.ready = removeZeros(listener.ready)
}
func removeZeros[S ~[]E, E comparable](s S) S {
var z E
putIdx := 0
for i := range s {
if s[i] != z {
s[putIdx] = s[i]
putIdx++
}
}
return s[:putIdx]
}
func getConn(conns []*tcp.Conn, remotePort uint16, remoteAddr []byte) int {
for i, conn := range conns {
if conn == nil {
continue
}
gotPort := conn.RemotePort()
gotaddr := conn.RemoteAddr()
if remotePort == gotPort && bytes.Equal(remoteAddr, gotaddr) {
return i
}
}
return -1
}
func (listener *NodeTCPListener) maintainConn(conns []*tcp.Conn, idx int, err error) error {
if err == net.ErrClosed {
println("CLOSING CONN")
conn := conns[idx]
listener.poolReturn(conn)
conns[idx] = nil
return nil // avoid closing listener entirely.
}
return err
}
+5
View File
@@ -60,6 +60,11 @@ 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) {
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) {
t.Helper()
var buf [2048]byte
nextToSend := client
nextToRecv := server
+398
View File
@@ -0,0 +1,398 @@
package internet
import (
"math/rand"
"net/netip"
"testing"
"github.com/soypat/lneto/tcp"
)
func TestListener_SingleConnection(t *testing.T) {
rng := rand.New(rand.NewSource(1))
var clientStack, serverStack StackIP
var clientConn, serverConn tcp.Conn
var listener tcp.Listener
pool := newMockTCPPool(1, 3, 2048)
// Use existing setup but replace server's conn registration with listener.
setupClientServer(t, rng, &clientStack, &serverStack, &clientConn, &serverConn)
serverConn.Abort()
serverPort := uint16(80)
if err := listener.Reset(serverPort, pool); err != nil {
t.Fatal(err)
}
if err := serverStack.Register(&listener); err != nil {
t.Fatal(err)
}
var buf [2048]byte
// Complete full handshake before TryAccept (TryAccept only works for ESTABLISHED).
// Client sends SYN.
expectExchange(t, &clientStack, &serverStack, buf[:])
if listener.NumberOfReadyToAccept() != 0 {
t.Fatalf("after SYN: expected 0 ready (not established yet), got %d", listener.NumberOfReadyToAccept())
}
// Server sends SYN-ACK.
expectExchange(t, &serverStack, &clientStack, buf[:])
if listener.NumberOfReadyToAccept() != 0 {
t.Fatalf("after SYN: expected 0 ready (not established yet), got %d", listener.NumberOfReadyToAccept())
}
// Client sends ACK.
expectExchange(t, &clientStack, &serverStack, buf[:])
// Now connection is ESTABLISHED, TryAccept should work.
if listener.NumberOfReadyToAccept() != 1 {
t.Fatalf("after handshake: expected 1 ready, got %d", listener.NumberOfReadyToAccept())
}
acceptedConn, err := listener.TryAccept()
if err != nil {
t.Fatalf("TryAccept: %v", err)
}
if listener.NumberOfReadyToAccept() != 0 {
t.Fatalf("after accept: expected 0 ready, got %d", listener.NumberOfReadyToAccept())
}
if acceptedConn.State() != tcp.StateEstablished {
t.Fatalf("accepted conn: expected StateEstablished, got %s", acceptedConn.State())
}
if clientConn.State() != tcp.StateEstablished {
t.Fatalf("client conn: expected StateEstablished, got %s", clientConn.State())
}
}
func TestListener_AcceptAfterEstablished(t *testing.T) {
rng := rand.New(rand.NewSource(1))
var client1Stack, serverStack StackIP
var client1Conn, serverConn tcp.Conn
var listener tcp.Listener
pool := newMockTCPPool(2, 3, 2048)
// Setup server with listener.
setupClientServer(t, rng, &client1Stack, &serverStack, &client1Conn, &serverConn)
serverConn.Abort()
serverPort := uint16(80)
if err := listener.Reset(serverPort, pool); err != nil {
t.Fatal(err)
}
if err := serverStack.Register(&listener); err != nil {
t.Fatal(err)
}
var buf [2048]byte
// Complete full handshake for client1.
expectExchange(t, &client1Stack, &serverStack, buf[:]) // SYN
expectExchange(t, &serverStack, &client1Stack, buf[:]) // SYN-ACK
expectExchange(t, &client1Stack, &serverStack, buf[:]) // ACK
// Now TryAccept client1.
if listener.NumberOfReadyToAccept() != 1 {
t.Fatalf("after client1 handshake: expected 1 ready, got %d", listener.NumberOfReadyToAccept())
}
accepted1, err := listener.TryAccept()
if err != nil {
t.Fatalf("TryAccept client1: %v", err)
} else if listener.NumberOfReadyToAccept() != 0 {
t.Fatalf("after accepting conn: expected 0 ready, got %d", listener.NumberOfReadyToAccept())
}
if accepted1.State() != tcp.StateEstablished {
t.Fatalf("accepted1: expected StateEstablished, got %s", accepted1.State())
}
// Setup second client and verify we can still accept.
var client2Stack StackIP
var client2Conn tcp.Conn
setupClient(t, &client2Stack, &client2Conn, serverStack.Addr(), serverPort, 1338)
// Complete full handshake for client2.
expectExchange(t, &client2Stack, &serverStack, buf[:]) // SYN
expectExchange(t, &serverStack, &client2Stack, buf[:]) // SYN-ACK
expectExchange(t, &client2Stack, &serverStack, buf[:]) // ACK
// Now TryAccept client2.
if listener.NumberOfReadyToAccept() != 1 {
t.Fatalf("after client2 handshake: expected 1 ready, got %d", listener.NumberOfReadyToAccept())
}
accepted2, err := listener.TryAccept()
if err != nil {
t.Fatalf("TryAccept client2: %v", err)
} else if listener.NumberOfReadyToAccept() != 0 {
t.Fatalf("after client2 accept: expected 0 ready, got %d", listener.NumberOfReadyToAccept())
}
if accepted2.State() != tcp.StateEstablished {
t.Fatalf("accepted2: expected StateEstablished, got %s", accepted2.State())
}
}
func TestListener_MultiConn(t *testing.T) {
const numClients = 5
rng := rand.New(rand.NewSource(1))
var serverStack StackIP
var serverConn tcp.Conn
var listener tcp.Listener
pool := newMockTCPPool(numClients, 3, 2048)
// Create slices for clients.
clientStacks := make([]StackIP, numClients)
clientConns := make([]tcp.Conn, numClients)
acceptedConns := make([]*tcp.Conn, numClients)
// Setup server with listener using setupClientServer for first client to get server configured.
setupClientServer(t, rng, &clientStacks[0], &serverStack, &clientConns[0], &serverConn)
serverConn.Abort()
serverPort := uint16(80)
if err := listener.Reset(serverPort, pool); err != nil {
t.Fatal(err)
}
if err := serverStack.Register(&listener); err != nil {
t.Fatal(err)
}
// Setup remaining clients.
for i := 1; i < numClients; i++ {
clientPort := uint16(1337 + i)
setupClient(t, &clientStacks[i], &clientConns[i], serverStack.Addr(), serverPort, clientPort)
}
var buf [2048]byte
// Complete full handshakes for all clients.
for i := 0; i < numClients; i++ {
expectExchange(t, &clientStacks[i], &serverStack, buf[:]) // SYN
expectExchange(t, &serverStack, &clientStacks[i], buf[:]) // SYN-ACK
expectExchange(t, &clientStacks[i], &serverStack, buf[:]) // ACK
}
if listener.NumberOfReadyToAccept() != numClients {
t.Fatalf("after all handshakes: expected %d ready, got %d", numClients, listener.NumberOfReadyToAccept())
}
if pool.NumberOfAcquired() != numClients {
t.Fatalf("pool should have %d acquired, got %d", numClients, pool.NumberOfAcquired())
}
// Accept all connections.
for i := 0; i < numClients; i++ {
var err error
acceptedConns[i], err = listener.TryAccept()
if err != nil {
t.Fatalf("TryAccept client %d: %v", i, err)
}
}
if listener.NumberOfReadyToAccept() != 0 {
t.Fatalf("after all accepts: expected 0 ready, got %d", listener.NumberOfReadyToAccept())
}
// Verify all connections established.
for i := 0; i < numClients; i++ {
if clientConns[i].State() != tcp.StateEstablished {
t.Errorf("client %d: expected StateEstablished, got %s", i, clientConns[i].State())
}
if acceptedConns[i].State() != tcp.StateEstablished {
t.Errorf("accepted %d: expected StateEstablished, got %s", i, acceptedConns[i].State())
}
}
// Test data exchange: client -> server.
for i := 0; i < numClients; i++ {
msg := []byte("hello from client " + string('0'+byte(i)))
n, err := clientConns[i].Write(msg)
if err != nil {
t.Fatalf("client %d write: %v", i, err)
}
if n != len(msg) {
t.Fatalf("client %d write: wrote %d, expected %d", i, n, len(msg))
}
}
// Exchange data packets from all clients to server.
for i := 0; i < numClients; i++ {
expectExchange(t, &clientStacks[i], &serverStack, buf[:])
}
// Read data on server side and verify.
for i := 0; i < numClients; i++ {
expected := "hello from client " + string('0'+byte(i))
var readBuf [64]byte
n, err := acceptedConns[i].Read(readBuf[:])
if err != nil {
t.Fatalf("server read %d: %v", i, err)
}
if string(readBuf[:n]) != expected {
t.Errorf("server read %d: got %q, expected %q", i, string(readBuf[:n]), expected)
}
}
// Test data exchange: server -> client.
for i := 0; i < numClients; i++ {
msg := []byte("reply to client " + string('0'+byte(i)))
n, err := acceptedConns[i].Write(msg)
if err != nil {
t.Fatalf("server %d write: %v", i, err)
}
if n != len(msg) {
t.Fatalf("server %d write: wrote %d, expected %d", i, n, len(msg))
}
}
// Exchange data packets from server to all clients.
for i := 0; i < numClients; i++ {
expectExchange(t, &serverStack, &clientStacks[i], buf[:])
}
// Read responses on client side and verify.
for i := 0; i < numClients; i++ {
expected := "reply to client " + string('0'+byte(i))
var readBuf [64]byte
n, err := clientConns[i].Read(readBuf[:])
if err != nil {
t.Fatalf("client read %d: %v", i, err)
}
if string(readBuf[:n]) != expected {
t.Errorf("client read %d: got %q, expected %q", i, string(readBuf[:n]), expected)
}
}
// Close connections, alternating between client-initiated and server-initiated.
for i := 0; i < numClients; i++ {
var closer, responder *StackIP
var closerConn, responderConn *tcp.Conn
var serverClosed bool
whoCloses := "client"
whoResponds := "server"
expectStates := func(ctx string, wantCloserState, wantResponderState tcp.State) {
t.Helper()
if closerConn.State() != wantCloserState {
t.Errorf("%s: %s closer want %s, got %s", ctx, whoCloses, wantCloserState, closerConn.State())
}
if responderConn.State() != wantResponderState {
t.Errorf("%s: %s respon want %s, got %s", ctx, whoResponds, wantResponderState, responderConn.State())
}
}
if i%2 == 0 {
// Client initiates close.
closer, responder = &clientStacks[i], &serverStack
closerConn, responderConn = &clientConns[i], acceptedConns[i]
} else {
// Server initiates close.
serverClosed = true
whoCloses, whoResponds = whoResponds, whoCloses
closer, responder = &serverStack, &clientStacks[i]
closerConn, responderConn = acceptedConns[i], &clientConns[i]
}
_ = serverClosed // Used for context in debugging.
// Closer calls Close(), FIN not sent yet.
if err := closerConn.Close(); err != nil {
t.Fatalf("conn %d close: %v", i, err)
}
expectStates("after-close()", tcp.StateEstablished, tcp.StateEstablished)
// Closer sends FIN -> responder receives, goes to CLOSE-WAIT.
expectExchange(t, closer, responder, buf[:])
expectStates("after-FIN", tcp.StateFinWait1, tcp.StateCloseWait)
// Responder sends ACK -> closer goes to FIN-WAIT-2.
expectExchange(t, responder, closer, buf[:])
expectStates("after-ACK", tcp.StateFinWait2, tcp.StateCloseWait)
// Responder closes and sends FIN -> closer goes to TIME-WAIT.
if err := responderConn.Close(); err != nil {
t.Fatalf("conn %d responder close: %v", i, err)
}
expectExchange(t, responder, closer, buf[:])
expectStates("after-resp-FIN", tcp.StateTimeWait, tcp.StateLastAck)
// Closer sends final ACK -> responder goes to CLOSED.
expectExchange(t, closer, responder, buf[:])
expectStates("after-final-ACK", tcp.StateTimeWait, tcp.StateClosed)
}
}
// 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) {
t.Helper()
bufsize := 2048
clientIP := netip.AddrFrom4([4]byte{192, 168, 1, byte(clientPort % 256)})
client.Reset(clientIP, 1)
err := conn.Configure(tcp.ConnConfig{
RxBuf: make([]byte, bufsize),
TxBuf: make([]byte, bufsize),
TxPacketQueueSize: 3,
})
if err != nil {
t.Fatal(err)
}
serverAddrPort := netip.AddrPortFrom(serverAddr, serverPort)
err = conn.OpenActive(clientPort, serverAddrPort, 100)
if err != nil {
t.Fatal(err)
}
err = client.Register(conn)
if err != nil {
t.Fatal(err)
}
}
// mockTCPPool implements tcpPool for testing.
type mockTCPPool struct {
naqcuired int
conns []tcp.Conn
acquired []bool
nextISS tcp.Value
}
func newMockTCPPool(n, queuesize, bufsize int) *mockTCPPool {
pool := &mockTCPPool{
acquired: make([]bool, n),
conns: make([]tcp.Conn, n),
}
for i := range pool.conns {
err := pool.conns[i].Configure(tcp.ConnConfig{
RxBuf: make([]byte, bufsize),
TxBuf: make([]byte, bufsize),
TxPacketQueueSize: queuesize,
})
if err != nil {
panic(err)
}
}
return pool
}
func (p *mockTCPPool) GetTCP() (*tcp.Conn, tcp.Value) {
for i := range p.conns {
if !p.acquired[i] {
p.acquired[i] = true
p.nextISS += 1000
p.naqcuired++
return &p.conns[i], p.nextISS
}
}
return nil, 0
}
func (p *mockTCPPool) PutTCP(conn *tcp.Conn) {
for i := range p.conns {
if &p.conns[i] == conn {
p.conns[i].Abort()
p.acquired[i] = false
p.naqcuired--
return
}
}
panic("conn does not belong to this pool")
}
func (p *mockTCPPool) NumberOfAcquired() int {
return p.naqcuired
}