Files
lneto/tcp/rtointegration_test.go
T
Derek den Haas 21f477b86e fix(tcp): retransmit unacknowledged data after a local close (#182)
ControlBlock.Send refused any outgoing segment carrying data in
FIN-WAIT-1, citing RFC 9293's "no further SENDs from the user will be
accepted by the TCP implementation". That rule bounds what the
application may queue, which Handler.Write already enforces, and not the
retransmission of data the connection has already accepted from it.

The consequence is that write-then-close, which is what nearly every
server does with a response, cannot recover from losing its last data
segment. The FIN occupies a sequence number above that data, so the peer
cannot cross the gap to process the close: it waits for bytes that are
never resent while the sender waits for an ACK that cannot arrive.
Neither side times out at the TCP layer.

FIN-WAIT-2 keeps the restriction and gains the reasoning: it is reached
by our FIN being acknowledged, which acknowledges everything below it, so
no unacknowledged data can remain there.

PendingSegment needs the same distinction, since it decides whether to
offer send-buffer data at all; it gets an unexported State predicate
rather than a new exported one.

Two tests, the second red before the change:

  - retransmission after RTO expiry, covering the Handler/LossRecovery
    seam that the RTO unit tests do not reach (they exercise the state
    machine in isolation, where it behaves correctly).
  - retransmission after a close with unacknowledged data.

Co-authored-by: Derek den Haas <i.pestano@easyflor.nl>
2026-08-24 19:36:48 -03:00

121 lines
3.7 KiB
Go

package tcp
import (
"math/rand"
"testing"
"time"
"github.com/soypat/lneto/ethernet"
)
// TestHandlerRetransmitsAfterRTO covers the seam between a Handler and its
// LossRecovery, which the RTO unit tests do not: a lost data segment must be
// resent once the timer expires, with nothing arriving to prompt it.
func TestHandlerRetransmitsAfterRTO(t *testing.T) {
const mtu = ethernet.MaxMTU
const maxpackets = 4
rng := rand.New(rand.NewSource(5))
client, server := newHandler(t, mtu, maxpackets), newHandler(t, mtu, maxpackets)
var now int64 // injected monotonic clock, in nanoseconds
client.SetLossRecovery(new(RTO), func() int64 { return now })
setupClientServer(t, rng, client, server)
var rawbuf [mtu]byte
establish(t, client, server, rawbuf[:])
data := []byte("hello")
if n, err := client.Write(data); err != nil || n != len(data) {
t.Fatal("client write:", n, err)
}
clear(rawbuf[:])
n, err := client.Send(rawbuf[:])
if err != nil || n == 0 {
t.Fatal("client send:", n, err)
}
// That frame is lost: it is never handed to the server.
// Nothing may come back before the timer expires.
var probe [mtu]byte
if n, err := client.Send(probe[:]); err != nil || n != 0 {
t.Fatalf("client sent %d bytes before the RTO expired (err %v)", n, err)
}
now += int64(3 * time.Second) // past the initial RTO and one backoff
clear(probe[:])
n, err = client.Send(probe[:])
if err != nil {
t.Fatal("client send after RTO:", err)
}
if n == 0 {
t.Fatal("no retransmission after the RTO expired: the loss-recovery directive is never applied")
}
if err := server.Recv(probe[:n]); err != nil {
t.Fatal("server refused the retransmission:", err)
}
got := make([]byte, 16)
nr, err := server.Read(got)
if err != nil || string(got[:nr]) != string(data) {
t.Fatalf("server read %q (%v), want %q", got[:nr], err, data)
}
}
// TestHandlerRetransmitsAfterCloseWithUnackedData is the write-then-close case
// every server performs. With the last data segment lost, the FIN behind it sits
// above a gap the peer cannot cross, so FIN-WAIT-1 must still retransmit that
// data or both sides wait forever.
func TestHandlerRetransmitsAfterCloseWithUnackedData(t *testing.T) {
const mtu = ethernet.MaxMTU
const maxpackets = 4
rng := rand.New(rand.NewSource(9))
client, server := newHandler(t, mtu, maxpackets), newHandler(t, mtu, maxpackets)
var now int64
client.SetLossRecovery(new(RTO), func() int64 { return now })
setupClientServer(t, rng, client, server)
var rawbuf [mtu]byte
establish(t, client, server, rawbuf[:])
data := []byte("last response bytes")
if n, err := client.Write(data); err != nil || n != len(data) {
t.Fatal("client write:", n, err)
}
clear(rawbuf[:])
n, err := client.Send(rawbuf[:]) // this frame is lost in transit
if err != nil || n == 0 {
t.Fatal("client send:", n, err)
}
// The application closes right after writing.
if err := client.Close(); err != nil {
t.Fatal("client close:", err)
}
var finbuf [mtu]byte
nfin, err := client.Send(finbuf[:]) // FIN (also lost, or simply unacked)
if err != nil {
t.Fatal("client send FIN:", err)
}
t.Logf("state after close: %s (FIN frame %d bytes)", client.State(), nfin)
now += int64(3 * time.Second) // past the RTO
var probe [mtu]byte
n, err = client.Send(probe[:])
if err != nil {
t.Fatal("client send after RTO:", err)
}
if n == 0 {
t.Fatalf("no retransmission in %s: unacknowledged data is stranded by the close", client.State())
}
if err := server.Recv(probe[:n]); err != nil {
t.Fatal("server refused the retransmission:", err)
}
got := make([]byte, 32)
nr, err := server.Read(got)
if err != nil || string(got[:nr]) != string(data) {
t.Fatalf("server read %q (%v), want %q", got[:nr], err, data)
}
}