Files
lneto/x/xnet/retransmit_test.go
T

213 lines
6.7 KiB
Go

package xnet
import (
"context"
"fmt"
"net"
"net/netip"
"syscall"
"testing"
"time"
"github.com/soypat/lneto"
"github.com/soypat/lneto/ethernet"
"github.com/soypat/lneto/internal/ltesto"
"github.com/soypat/lneto/tcp"
"github.com/soypat/lneto/tcp/rto"
)
// TestTCPRetransmitsLostSegment drops exactly one data segment and requires the
// bytes to arrive anyway. It covers [TCPPoolConfig.NewPolicy] reaching the pooled
// and dialed connections alike: with no [tcp.Policy] installed nothing notices the
// loss, no retransmission is ever sent and the read below never completes.
//
// The server and the client each get an [ltesto.Sched] goroutine and the test
// thread drives them as a barrier: it only moves frames or advances the clock
// once both are parked, so the stacks are never touched concurrently. Time is
// simulated, so waiting out the one-second initial RTO (RFC 6298 §2.1) costs
// nothing and the outcome does not depend on how fast the machine is.
func TestTCPRetransmitsLostSegment(t *testing.T) {
const (
MTU = ethernet.MaxMTU
svPort = 80
bufSize = 2 << 10
want = "this segment is lost in transit"
// A quiet round means both sides are waiting on the network, which is
// what a lost segment looks like: only then does the clock move, so the
// RTO expires in a bounded number of rounds instead of in real time.
quietStep = 100 * time.Millisecond
maxRounds = 600
// Headers total 54 bytes, so a larger frame carries payload. Dropping a
// bare ACK would exercise the other direction's recovery instead.
minDataFrame = 14 + 20 + 20 + 8
)
client, sv := new(StackAsync), new(StackAsync)
if err := client.Reset(StackConfig{
Hostname: "rtx-client",
RandSeed: 11,
StaticAddress4: [4]byte{10, 0, 0, 90},
MaxActiveTCPPorts: 2,
HardwareAddress: [6]byte{0xbe, 0xef, 0, 0, 0, 90},
MTU: MTU,
ICMPQueueLimit: 2,
}); err != nil {
t.Fatal(err)
}
if err := sv.Reset(StackConfig{
Hostname: "rtx-server",
RandSeed: ^int64(11),
StaticAddress4: [4]byte{10, 0, 0, 91},
MaxActiveTCPPorts: 2,
HardwareAddress: [6]byte{0xbe, 0xef, 0, 0, 0, 91},
MTU: MTU,
ICMPQueueLimit: 2,
}); err != nil {
t.Fatal(err)
}
client.SetGatewayHardwareAddr(sv.HardwareAddr())
sv.SetGatewayHardwareAddr(client.HardwareAddr())
tsched := ltesto.NewSched(t)
svGoro, clGoro := tsched.Goro(), tsched.Goro()
// Simulated monotonic clock. Only the driver writes it, and only while every
// scheduled goroutine is parked, so it needs no synchronization of its own.
var now int64
nanotime := func() int64 { return now }
// Each side backs off into its own scheduler handle, so the driver can park
// and resume the two independently.
newPool := func(yield lneto.BackoffStrategy) TCPPoolConfig {
return TCPPoolConfig{
PoolSize: 2, QueueSize: 4,
TxBufSize: bufSize, RxBufSize: bufSize,
// Well past the simulated time this test spends, so the pool never
// reaps a connection out from under the retransmission.
EstablishedTimeout: 120 * time.Second,
ClosingTimeout: 120 * time.Second,
NanoTime: nanotime,
NewBackoff: func() lneto.BackoffStrategy { return yield },
NewPolicy: func() tcp.Policy {
timer := new(rto.Timer)
if err := timer.Configure(nanotime); err != nil {
t.Error(err)
}
return timer
},
}
}
svGo := sv.StackBlocking(svGoro.Yield).StackGo(StackGoConfig{
ListenerPoolConfig: newPool(svGoro.Yield),
})
clGo := client.StackBlocking(clGoro.Yield).StackGo(StackGoConfig{
ListenerPoolConfig: newPool(clGoro.Yield),
TCPDialTimeout: 60 * time.Second,
TCPDialRetries: 1,
})
svGo.blk._nanotime = nanotime
clGo.blk._nanotime = nanotime
lsAny, err := svGo.SocketNetip(context.Background(), "tcp", syscall.AF_INET, sockSTREAM,
netip.AddrPortFrom(netip.AddrFrom4(sv.Addr4()), svPort), netip.AddrPort{})
if err != nil {
t.Fatal(err)
}
listener := lsAny.(net.Listener)
defer listener.Close()
// dropNext arms the driver to swallow the next server→client data frame. It
// is handed between the server goroutine and the driver by the scheduler
// handoff, which orders every access to it.
var dropNext, dropped bool
go func() {
c, err := listener.Accept()
if err != nil {
svGoro.FinishWithErr(err)
return
}
dropNext = true // The very next data frame is lost in transit.
_, err = c.Write([]byte(want))
c.Close() // Closing here is what makes #182's FIN-WAIT-1 retransmit matter.
svGoro.FinishWithErr(err)
}()
raddr := netip.AddrPortFrom(netip.AddrFrom4(sv.Addr4()), svPort)
go func() {
cAny, err := clGo.SocketNetip(context.Background(), "tcp", syscall.AF_INET, sockSTREAM,
netip.AddrPort{}, raddr)
if err != nil {
clGoro.FinishWithErr(err)
return
}
conn := cAny.(net.Conn)
got := make([]byte, 0, len(want))
rb := make([]byte, 64)
for len(got) < len(want) {
n, err := conn.Read(rb)
got = append(got, rb[:n]...)
if err != nil {
clGoro.FinishWithErr(fmt.Errorf("read %d/%d bytes: %w", len(got), len(want), err))
return
}
}
if string(got) != want {
clGoro.FinishWithErr(fmt.Errorf("read %q, want %q", got, want))
return
}
// Closed before finishing: a Yield after FinishWithErr would never be
// serviced, since the driver stops resuming a goroutine it has reaped.
conn.Close()
clGoro.Finish()
}()
var buf [MTU + ethernet.MaxOverheadSize]byte
// pump moves one frame each way, dropping the armed one. Only ever called
// with both goroutines parked.
// Ingress errors are not fatal here: once a segment is dropped the frames
// behind it arrive past rcv.nxt and are rejected, which is precisely the
// stall the retransmission has to break. Egress errors are real faults.
pump := func() (moved bool) {
n, err := client.EgressEthernet(buf[:])
if err != nil {
t.Fatal("client egress:", err)
} else if n > 0 {
sv.IngressEthernet(buf[:n])
moved = true
}
n, err = sv.EgressEthernet(buf[:])
if err != nil {
t.Fatal("server egress:", err)
} else if n > 0 {
if dropNext && n > minDataFrame {
dropNext, dropped = false, true
} else {
client.IngressEthernet(buf[:n])
}
moved = true
}
return moved
}
for round := 0; ; round++ {
if round == maxRounds {
t.Fatalf("no retransmission after %d rounds and %v of simulated time (dropped=%v): is a Policy installed?",
maxRounds, time.Duration(now), dropped)
}
allFinished, err := tsched.AwaitAllParked()
if err != nil {
t.Fatalf("after losing one segment (dropped=%v): %v", dropped, err)
}
if allFinished {
break
}
if !pump() {
now += int64(quietStep) // Both sides idle: let the RTO age.
}
tsched.YieldToAllParked()
}
if !dropped {
t.Fatal("no frame was dropped, so the test did not exercise retransmission")
}
}