mirror of
https://github.com/soypat/lneto.git
synced 2026-09-08 15:59:10 +00:00
166 lines
4.6 KiB
Go
166 lines
4.6 KiB
Go
package xnet
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"net/netip"
|
|
"runtime"
|
|
"sync/atomic"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/soypat/lneto"
|
|
"github.com/soypat/lneto/ethernet"
|
|
"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: without a [tcp.Policy] installed the
|
|
// loss is terminal, which is what this test asserts against.
|
|
func TestTCPRetransmitsLostSegment(t *testing.T) {
|
|
const (
|
|
MTU = ethernet.MaxMTU
|
|
svPort = 80
|
|
bufSize = 2 << 10
|
|
)
|
|
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())
|
|
|
|
pool := TCPPoolConfig{
|
|
PoolSize: 2, QueueSize: 4,
|
|
TxBufSize: bufSize, RxBufSize: bufSize,
|
|
EstablishedTimeout: 30 * time.Second,
|
|
ClosingTimeout: 30 * time.Second,
|
|
NewBackoff: func() lneto.BackoffStrategy { return backoffYield },
|
|
NewPolicy: func() tcp.Policy {
|
|
timer := new(rto.Timer)
|
|
if err := timer.Configure(func() int64 { return time.Now().UnixNano() }); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return timer
|
|
},
|
|
}
|
|
svGo := sv.StackBlocking(backoffYield).StackGo(StackGoConfig{ListenerPoolConfig: pool})
|
|
clGo := client.StackBlocking(backoffYield).StackGo(StackGoConfig{
|
|
ListenerPoolConfig: pool,
|
|
TCPDialTimeout: 2 * time.Second,
|
|
TCPDialRetries: 2,
|
|
})
|
|
|
|
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 pump to swallow the next server→client data frame.
|
|
var dropNext, dropped atomic.Bool
|
|
stopPump := make(chan struct{})
|
|
defer close(stopPump)
|
|
go func() {
|
|
buf := make([]byte, MTU+ethernet.MaxOverheadSize)
|
|
for {
|
|
select {
|
|
case <-stopPump:
|
|
return
|
|
default:
|
|
}
|
|
moved := false
|
|
if n, err := client.EgressEthernet(buf); err == nil && n > 0 {
|
|
sv.IngressEthernet(buf[:n])
|
|
moved = true
|
|
}
|
|
if n, err := sv.EgressEthernet(buf); err == nil && n > 0 {
|
|
// Drop only a data-carrying frame: headers total 54 bytes, so
|
|
// anything larger has payload. Dropping a bare ACK would test the
|
|
// other direction's recovery instead.
|
|
if dropNext.Load() && n > 14+20+20+8 {
|
|
dropNext.Store(false)
|
|
dropped.Store(true)
|
|
} else {
|
|
client.IngressEthernet(buf[:n])
|
|
}
|
|
moved = true
|
|
}
|
|
if !moved {
|
|
runtime.Gosched()
|
|
}
|
|
}
|
|
}()
|
|
|
|
served := make(chan error, 1)
|
|
go func() {
|
|
c, err := listener.Accept()
|
|
if err != nil {
|
|
served <- err
|
|
return
|
|
}
|
|
defer c.Close()
|
|
c.SetDeadline(time.Now().Add(30 * time.Second))
|
|
dropNext.Store(true) // the very next data frame is lost
|
|
_, err = c.Write([]byte("this segment is lost in transit"))
|
|
served <- err
|
|
}()
|
|
|
|
raddr := netip.AddrPortFrom(netip.AddrFrom4(sv.Addr4()), svPort)
|
|
cAny, err := clGo.SocketNetip(context.Background(), "tcp", syscall.AF_INET, sockSTREAM,
|
|
netip.AddrPort{}, raddr)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
conn := cAny.(net.Conn)
|
|
defer conn.Close()
|
|
// Generous on purpose: the first RTO is one second (RFC 6298 §2.1) and may
|
|
// back off once. What is under test is that recovery happens at all.
|
|
conn.SetDeadline(time.Now().Add(15 * time.Second))
|
|
|
|
want := "this segment is lost in transit"
|
|
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 {
|
|
t.Fatalf("read %d/%d bytes after losing one segment (dropped=%v): %v (no retransmission timer?)",
|
|
len(got), len(want), dropped.Load(), err)
|
|
}
|
|
}
|
|
if string(got) != want {
|
|
t.Fatalf("read %q, want %q", got, want)
|
|
}
|
|
if !dropped.Load() {
|
|
t.Fatal("no frame was dropped, so the test did not exercise retransmission")
|
|
}
|
|
if err := <-served; err != nil {
|
|
t.Fatalf("server side: %v", err)
|
|
}
|
|
}
|