mirror of
https://github.com/soypat/lneto.git
synced 2026-09-10 00:29:34 +00:00
Tcp buffer bug fix (#79)
* start working on tracking down tcp buffer bug * mtu refactor * modularize test * more precise testing * tests fail, but is it the failure we are looking for? * fix typo in espradio link (#76) * implement a new backoff abstraction (#75) * rewrite backoff api * rewrite tcp.Conn.Write * keep fixing small things * much better Conn.Read implementation * fix critical overflow bug in internal.ConnRWBackoff --------- Co-authored-by: Joel Wetzell <jwetzell@yahoo.com>
This commit is contained in:
@@ -4,10 +4,12 @@ import (
|
||||
"bytes"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/soypat/lneto/ethernet"
|
||||
)
|
||||
|
||||
func TestARPLocal(t *testing.T) {
|
||||
const mtu = 1500
|
||||
const mtu = ethernet.MaxMTU
|
||||
const seed = 1
|
||||
s1, s2, c1, c2 := newTCPStacks(t, seed, mtu)
|
||||
routerHw := [6]byte{1, 2, 3, 4, 5, 6}
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
)
|
||||
|
||||
func BenchmarkARPExchange(b *testing.B) {
|
||||
const MTU = 1500
|
||||
const frameSize = MTU + ethernet.MaxOverheadSize
|
||||
const MTU = ethernet.MaxMTU
|
||||
const frameSize = ethernet.MaxFrameLength
|
||||
c1, c2 := new(StackAsync), new(StackAsync)
|
||||
queryAddr := netip.AddrFrom4([4]byte{192, 168, 1, 2})
|
||||
|
||||
@@ -76,8 +76,8 @@ func BenchmarkARPExchange(b *testing.B) {
|
||||
}
|
||||
|
||||
func BenchmarkTCPHandshake(b *testing.B) {
|
||||
const MTU = 1500
|
||||
const frameSize = MTU + ethernet.MaxOverheadSize
|
||||
const MTU = ethernet.MaxMTU
|
||||
const frameSize = ethernet.MaxFrameLength
|
||||
const svPort = 8080
|
||||
client, sv := new(StackAsync), new(StackAsync)
|
||||
clconn, svconn := new(tcp.Conn), new(tcp.Conn)
|
||||
|
||||
@@ -4,14 +4,19 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math/rand"
|
||||
"net/netip"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/soypat/lneto"
|
||||
"github.com/soypat/lneto/ethernet"
|
||||
"github.com/soypat/lneto/internal"
|
||||
"github.com/soypat/lneto/ipv4"
|
||||
"github.com/soypat/lneto/tcp"
|
||||
)
|
||||
|
||||
@@ -142,8 +147,8 @@ func TestTCPListener_ConcurrentEcho(t *testing.T) {
|
||||
}
|
||||
|
||||
func kernelLoop(ctx context.Context, server *StackAsync, clients []StackAsync) {
|
||||
const MTU = 1500
|
||||
const carrierDataSize = MTU + ethernet.MaxOverheadSize
|
||||
const MTU = ethernet.MaxMTU
|
||||
const carrierDataSize = ethernet.MaxFrameLength
|
||||
buf := make([]byte, carrierDataSize)
|
||||
rng := rand.New(rand.NewSource(1)) // Seed 1 for deterministic but randomized order
|
||||
order := make([]int, len(clients))
|
||||
@@ -285,3 +290,148 @@ func runClient(t *testing.T, id int, stack *StackAsync, conn *tcp.Conn,
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func TestCloseTransmitsPending(t *testing.T) {
|
||||
const mtu = ipv4.MinimumMTU
|
||||
const tcpbufsize = mtu * 2
|
||||
const tcpDataPerPkt = mtu - 14 - 20 - 20 // Ethernet=14, IPv4=20, TCP=20
|
||||
const expectPkts = 2*tcpbufsize/tcpDataPerPkt + 1
|
||||
const queueSize = 5
|
||||
const port1, port2 = 10, 20
|
||||
tst := testerFrom(t, mtu)
|
||||
tst.buf = tst.buf[:mtu+14]
|
||||
s1, s2, c1, c2 := newTCPStacks(t, 0x1337_c0de, mtu)
|
||||
t.Run("sync", func(t *testing.T) {
|
||||
// testCloseTransmitsPending(tst, s1, s2, c1, c2, queueSize, tcpbufsize, tcpbufsize, tcpbufsize)
|
||||
})
|
||||
t.Run("async", func(t *testing.T) {
|
||||
testCloseTransmitsPending(tst, s1, s2, c1, c2, queueSize, tcpbufsize, tcpbufsize, 2*tcpbufsize)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func testCloseTransmitsPending(tst *tester, s1, s2 *StackAsync, c1, c2 *tcp.Conn, queueSize, tx1Buf, rx2Buf, datalen int) {
|
||||
t := tst.t
|
||||
buf := tst.buf
|
||||
defer func() {
|
||||
c1.Abort()
|
||||
c2.Abort()
|
||||
// Ensure they are unregistered.
|
||||
s1.EgressIP(buf)
|
||||
s2.EgressIP(buf)
|
||||
}()
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
||||
Level: slog.LevelDebug - 99,
|
||||
}))
|
||||
err := c1.Configure(tcp.ConnConfig{
|
||||
RxBuf: nil,
|
||||
TxBuf: make([]byte, tx1Buf),
|
||||
TxPacketQueueSize: queueSize,
|
||||
RWBackoff: backoffGosched,
|
||||
Logger: logger,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = c2.InternalHandler().SetBuffers(nil, make([]byte, rx2Buf), queueSize)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
const (
|
||||
port1, port2 = 10, 20
|
||||
)
|
||||
tst.TestTCPSetupAndEstablish(s1, s2, c1, c2, port1, port2)
|
||||
if c1.FreeOutput() != tx1Buf {
|
||||
t.Fatalf("want %d free bytes, got %d", tx1Buf, c1.FreeOutput())
|
||||
}
|
||||
data := make([]byte, datalen)
|
||||
for i := range datalen {
|
||||
data[i] = byte(i)
|
||||
}
|
||||
deadline := time.Now().Add(3600 * time.Second)
|
||||
err = c1.SetDeadline(deadline)
|
||||
err2 := c2.SetDeadline(deadline)
|
||||
if err != nil || err2 != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
async := datalen > tx1Buf
|
||||
if async {
|
||||
// Since data does not fit in TCP Tx buffer the test must be run asynchronously.
|
||||
c1.InternalHandler().SetLoggers(logger, logger)
|
||||
// c1.InternalHandler().SetLoggers(nil, nil)
|
||||
go func() {
|
||||
n, err := c1.Write(data)
|
||||
if err != nil {
|
||||
t.Error("async write", err)
|
||||
} else if n != len(data) {
|
||||
t.Error("io.Writer faulty implementation")
|
||||
}
|
||||
err = c1.Close()
|
||||
if err != nil {
|
||||
t.Fatal("async close", err)
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
n, err := c1.Write(data)
|
||||
if err != nil || n != len(data) {
|
||||
t.Fatal(err, n)
|
||||
}
|
||||
err = c1.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
exchanges := -1
|
||||
exchanging := 1
|
||||
tcpData := 0
|
||||
totalRead := 0
|
||||
for exchanging > 0 || c1.State().TxDataOpen() {
|
||||
exchanges++
|
||||
exchanging = exchangeEthernetOnce(t, s1, s2, buf)
|
||||
frm, ok := getTCPFrame(buf[:exchanging])
|
||||
if ok {
|
||||
n := len(frm.Payload())
|
||||
tcpData += n
|
||||
if async && tcpData > 0 {
|
||||
ngot, err := c2.Read(buf[:n])
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
} else if ngot != n {
|
||||
t.Errorf("want %d data read c1->c1, got %d", n, ngot)
|
||||
} else if !internal.BytesEqual(buf[:n], data[totalRead:totalRead+n]) {
|
||||
t.Errorf("exch%d data rx mismatch, want:\n%q\ngot:\n%q\n", exchanges, data[totalRead:totalRead+n], buf[:n])
|
||||
}
|
||||
totalRead += ngot
|
||||
runtime.Gosched() // Yield to let c1 write via goroutine.
|
||||
acks := exchangeEthernetOnce(t, s2, s1, buf) // Send ACK s1's way.
|
||||
if acks == 0 {
|
||||
t.Error("no data sent back to s1")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if c1.BufferedUnsent() != 0 {
|
||||
t.Errorf("done %s: want no data left unsent got %d/%d", c1.State(), c1.BufferedUnsent(), len(data))
|
||||
}
|
||||
if tcpData != datalen {
|
||||
t.Errorf("done %s: want %d bytes sent, got %d", c1.State(), len(data), tcpData)
|
||||
}
|
||||
if t.Failed() {
|
||||
t.Logf("test params: txsz1=%d rxsz2=%d queuesize=%d data(sent/had)=%d/%d", tx1Buf, rx2Buf, queueSize, tcpData, datalen)
|
||||
}
|
||||
if totalRead < datalen {
|
||||
n, err := c2.Read(buf)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
} else if !internal.BytesEqual(buf[:n], data[totalRead:]) {
|
||||
t.Errorf("expected last bytes equal: want:\n%q\ngot:\n%q\n", data[totalRead:], buf[:n])
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func backoffGosched(consecutiveBackoffs int) (sleep time.Duration) {
|
||||
return lneto.BackoffFlagGosched
|
||||
}
|
||||
|
||||
@@ -5,12 +5,13 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/soypat/lneto/ethernet"
|
||||
"github.com/soypat/lneto/tcp"
|
||||
)
|
||||
|
||||
func TestTCPConn_SetDeadline_Established(t *testing.T) {
|
||||
const seed = 9999
|
||||
const MTU = 1500
|
||||
const MTU = ethernet.MaxMTU
|
||||
const svPort = 8080
|
||||
client, sv, clconn, svconn := newTCPStacks(t, seed, MTU)
|
||||
tst := testerFrom(t, MTU)
|
||||
@@ -37,7 +38,7 @@ func TestTCPConn_SetDeadline_Established(t *testing.T) {
|
||||
|
||||
func TestTCPConn_ReadDeadlineExceeded(t *testing.T) {
|
||||
const seed = 10001
|
||||
const MTU = 1500
|
||||
const MTU = ethernet.MaxMTU
|
||||
const svPort = 8080
|
||||
client, sv, clconn, svconn := newTCPStacks(t, seed, MTU)
|
||||
tst := testerFrom(t, MTU)
|
||||
@@ -63,7 +64,7 @@ func TestTCPConn_ReadDeadlineExceeded(t *testing.T) {
|
||||
|
||||
func TestTCPConn_WriteDeadlineExceeded(t *testing.T) {
|
||||
const seed = 10002
|
||||
const MTU = 1500
|
||||
const MTU = ethernet.MaxMTU
|
||||
const svPort = 8080
|
||||
client, sv, clconn, svconn := newTCPStacks(t, seed, MTU)
|
||||
tst := testerFrom(t, MTU)
|
||||
@@ -88,7 +89,7 @@ func TestTCPConn_WriteDeadlineExceeded(t *testing.T) {
|
||||
|
||||
func TestTCPConn_FlushEmptyNoop(t *testing.T) {
|
||||
const seed = 10003
|
||||
const MTU = 1500
|
||||
const MTU = ethernet.MaxMTU
|
||||
const svPort = 8080
|
||||
client, sv, clconn, svconn := newTCPStacks(t, seed, MTU)
|
||||
tst := testerFrom(t, MTU)
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
|
||||
func TestDNS_QueryReceivesAnswer(t *testing.T) {
|
||||
const seed = 9876
|
||||
const MTU = 1500
|
||||
const MTU = ethernet.MaxMTU
|
||||
|
||||
// Create client stack with DNS server configured.
|
||||
client := new(StackAsync)
|
||||
@@ -47,7 +47,7 @@ func TestDNS_QueryReceivesAnswer(t *testing.T) {
|
||||
}
|
||||
|
||||
// Client sends DNS query.
|
||||
const carrierDataSize = MTU + ethernet.MaxOverheadSize
|
||||
const carrierDataSize = ethernet.MaxFrameLength
|
||||
var buf [carrierDataSize]byte
|
||||
n, err := client.EgressEthernet(buf[:])
|
||||
if err != nil {
|
||||
|
||||
@@ -17,9 +17,9 @@ import (
|
||||
)
|
||||
|
||||
func FuzzStackPacketHTTP(f *testing.F) {
|
||||
const MTU = 1500
|
||||
const MTU = ethernet.MaxMTU
|
||||
const seed = 1
|
||||
var buf [MTU + ethernet.MaxOverheadSize]byte
|
||||
var buf [ethernet.MaxFrameLength]byte
|
||||
s1, s2, c1, c2 := newTCPStacks(f, seed, MTU)
|
||||
var hdr httpraw.Header
|
||||
err := s1.ListenTCP(c1, 80)
|
||||
@@ -78,7 +78,7 @@ func FuzzStackPacketHTTP(f *testing.F) {
|
||||
}
|
||||
|
||||
f.Fuzz(func(t *testing.T, pktnum int, a []byte) {
|
||||
var buf [MTU + ethernet.MaxOverheadSize]byte
|
||||
var buf [ethernet.MaxFrameLength]byte
|
||||
s1, s2, c1, c2 := newTCPStacks(t, seed, MTU)
|
||||
err = s1.EnableICMP(true)
|
||||
if err != nil {
|
||||
@@ -268,8 +268,8 @@ func testStackSeeded(t *testing.T, seed1, seed2 int64) {
|
||||
}
|
||||
}
|
||||
|
||||
const mtu = 1500
|
||||
const mfl = mtu + 14 // frame length includes ethernet header
|
||||
const mtu = ethernet.MaxMTU
|
||||
const mfl = mtu + ethernet.MaxOverheadSize // frame length includes ethernet header
|
||||
var buf [mfl]byte
|
||||
var s1, s2 StackAsync
|
||||
v1, v2 := byte(seed1), byte(seed2)
|
||||
|
||||
@@ -42,11 +42,11 @@ func TestStackAsync_ICMPEcho(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
echoSent := exchangeEthernetOnce(t, sender, receiver, buf)
|
||||
if !echoSent {
|
||||
if echoSent == 0 {
|
||||
t.Error("ECHO not sent")
|
||||
}
|
||||
echoReplySent := exchangeEthernetOnce(t, receiver, sender, buf)
|
||||
if !echoReplySent {
|
||||
if echoReplySent == 0 {
|
||||
t.Error("ECHOREPLY not sent")
|
||||
}
|
||||
n, err = sender.EgressEthernet(buf)
|
||||
@@ -70,19 +70,19 @@ func TestStackAsync_ICMPEcho(t *testing.T) {
|
||||
}
|
||||
|
||||
// exchangeEthernetOnce sends one Ethernet frame from src to dst if available.
|
||||
func exchangeEthernetOnce(t *testing.T, src, dst *StackAsync, buf []byte) bool {
|
||||
func exchangeEthernetOnce(t *testing.T, src, dst *StackAsync, buf []byte) int {
|
||||
t.Helper()
|
||||
n, err := src.EgressEthernet(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
t.Error(err)
|
||||
}
|
||||
if n == 0 {
|
||||
return false
|
||||
return 0
|
||||
}
|
||||
if err := dst.IngressEthernet(buf[:n]); err != nil {
|
||||
t.Fatal(err)
|
||||
t.Error(err)
|
||||
}
|
||||
return true
|
||||
return n
|
||||
}
|
||||
|
||||
// newICMPStacks creates two test stacks with distinct static addresses and hardware addresses.
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
func TestStackAsyncListener_SingleConnection(t *testing.T) {
|
||||
const seed int64 = 1234
|
||||
const MTU = 1500
|
||||
const MTU = ethernet.MaxMTU
|
||||
const carrierSize = MTU + ethernet.MaxOverheadSize
|
||||
const svPort = 80
|
||||
const clPort = 1337
|
||||
@@ -122,7 +122,7 @@ func TestStackAsyncListener_SingleConnection(t *testing.T) {
|
||||
|
||||
func TestStackAsyncListener_MultiSequentialConn(t *testing.T) {
|
||||
const seed int64 = 1234
|
||||
const MTU = 1500
|
||||
const MTU = ethernet.MaxMTU
|
||||
const carrierSize = MTU + ethernet.MaxOverheadSize
|
||||
const svPort = 80
|
||||
const clPort = 1337
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
)
|
||||
|
||||
func TestMDNS_QueryResponse(t *testing.T) {
|
||||
const MTU = 1500
|
||||
const MTU = ethernet.MaxMTU
|
||||
svcName, err := dns.NewName("My Web._http._tcp.local")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -196,7 +196,7 @@ func TestMDNS_QueryResponse(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMDNS_SRVThroughStack(t *testing.T) {
|
||||
const MTU = 1500
|
||||
const MTU = ethernet.MaxMTU
|
||||
svcName, err := dns.NewName("My Web._http._tcp.local")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -275,7 +275,7 @@ func newMDNSStack(t *testing.T, hostname string, seed int64,
|
||||
mdnsCfg mdns.ClientConfig,
|
||||
) (*StackAsync, *mdns.Client) {
|
||||
t.Helper()
|
||||
const MTU = 1500
|
||||
const MTU = ethernet.MaxMTU
|
||||
stack := new(StackAsync)
|
||||
err := stack.Reset(StackConfig{
|
||||
Hostname: hostname,
|
||||
@@ -335,7 +335,7 @@ func mdnsQueryRespond(t *testing.T, querier, responder *StackAsync, buf []byte)
|
||||
}
|
||||
|
||||
func TestMDNS_RealWorldQueries(t *testing.T) {
|
||||
const MTU = 1500
|
||||
const MTU = ethernet.MaxMTU
|
||||
|
||||
responderMAC := [6]byte{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x01}
|
||||
querierMAC := [6]byte{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x02}
|
||||
|
||||
+35
-7
@@ -9,6 +9,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/soypat/lneto"
|
||||
"github.com/soypat/lneto/arp"
|
||||
"github.com/soypat/lneto/ethernet"
|
||||
"github.com/soypat/lneto/internal"
|
||||
@@ -29,7 +30,7 @@ const (
|
||||
|
||||
func TestTCPConn_ReadBlocksUntilDataAvailable(t *testing.T) {
|
||||
const seed = 5678
|
||||
const MTU = 1500
|
||||
const MTU = ethernet.MaxMTU
|
||||
const svPort = 8080
|
||||
client, sv, clconn, svconn := newTCPStacks(t, seed, MTU)
|
||||
tst := testerFrom(t, MTU)
|
||||
@@ -151,7 +152,7 @@ func TestStackAsyncTCP_multipacket(t *testing.T) {
|
||||
|
||||
func TestStackAsyncTCP_singlepacket(t *testing.T) {
|
||||
const seed = 1234
|
||||
const MTU = 1500
|
||||
const MTU = ethernet.MaxMTU
|
||||
const svPort = 80
|
||||
client, sv, clconn, svconn := newTCPStacks(t, seed, MTU)
|
||||
tst := testerFrom(t, MTU)
|
||||
@@ -283,8 +284,19 @@ func (tst *tester) TestTCPHandshake(stack1, stack2 *StackAsync) {
|
||||
noExchange(0),
|
||||
noExchange(1),
|
||||
}
|
||||
for _, wants := range exch {
|
||||
tst.TCPExchange(wants, stack1, stack2)
|
||||
var got [len(exch)]struct {
|
||||
seg tcp.Segment
|
||||
}
|
||||
for i, wants := range exch {
|
||||
haveFailed := tst.t.Failed()
|
||||
got[i].seg = tst.TCPExchange(wants, stack1, stack2)
|
||||
if haveFailed != tst.t.Failed() {
|
||||
tst.t.Logf("print out sent segments (%d):\n", i+1)
|
||||
for k := range i + 1 {
|
||||
str := tcp.StringExchange(got[k].seg, 255, 255, exch[k].SourceIdx == 0) // states unknown.
|
||||
tst.t.Log(str)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -734,7 +746,7 @@ func (tst *tester) getARPOperation() arp.Operation {
|
||||
// The bug was that reset() cleared bufRx when state became CLOSED.
|
||||
func TestTCPConn_BufferNotClearedOnPassiveClose(t *testing.T) {
|
||||
const seed = 9999
|
||||
const MTU = 1500
|
||||
const MTU = ethernet.MaxMTU
|
||||
const svPort = 8080
|
||||
client, sv, clconn, svconn := newTCPStacks(t, seed, MTU)
|
||||
tst := testerFrom(t, MTU)
|
||||
@@ -915,8 +927,8 @@ func TestTCPConn_BufferNotClearedOnPassiveClose(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestStackAsync_ICMPEchoChecksum(t *testing.T) {
|
||||
const MTU = 1500
|
||||
const MaxFrameLength = MTU + 14 + 4 // Ethernet header+FCS.
|
||||
const MTU = ethernet.MaxMTU
|
||||
const MaxFrameLength = MTU + ethernet.MaxOverheadSize // Ethernet header+FCS+VLAN.
|
||||
stackAddr := netip.AddrFrom4([4]byte{192, 168, 1, 99})
|
||||
stackMAC := [6]byte{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}
|
||||
routerAddr := [4]byte{192, 168, 1, 1}
|
||||
@@ -1017,3 +1029,19 @@ const (
|
||||
protoIPv4 = "IPv4"
|
||||
protoTCP = "TCP"
|
||||
)
|
||||
|
||||
func getTCPFrame(etherFrame []byte) (tcp.Frame, bool) {
|
||||
efrm, err := ethernet.NewFrame(etherFrame)
|
||||
if err != nil || efrm.EtherTypeOrSize() != ethernet.TypeIPv4 {
|
||||
return tcp.Frame{}, false
|
||||
}
|
||||
ifrm, err := ipv4.NewFrame(efrm.Payload())
|
||||
if err != nil || ifrm.Protocol() != lneto.IPProtoTCP {
|
||||
return tcp.Frame{}, false
|
||||
}
|
||||
tfrm, err := tcp.NewFrame(ifrm.Payload())
|
||||
if err != nil {
|
||||
return tcp.Frame{}, false
|
||||
}
|
||||
return tfrm, true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user