tcp: retransmit logic rollback (#61)

* tcp: remove retransmit logic entirely; add duplicate ack counting to ControlBlock

* retransmit implemented in nice simple straightforward way

* narrow down retransmission cases

* remove old timing tests

* add ControlBlock retransmit test

* add failing handler test

* reworking payload length semantic meaning in code

* fix establish conn logic

* clean up tests and add TCB dupack generation and test it

* catch pending retransmit satisfy in test

* add fuzz test for control block

* bugfix: be more strict in what is considered dupack

* add IncomingIsDupACK docs

* limit queue of retransmits

* protect retransmit overflow from incorrectly updating nxt
This commit is contained in:
Pat Whittingslow
2026-03-26 11:48:02 -03:00
committed by GitHub
parent abb114fc31
commit 64b2647a5e
12 changed files with 508 additions and 889 deletions
+8
View File
@@ -17,3 +17,11 @@ func Prand32[T ~uint32](seed T) T {
seed ^= seed << 5
return seed
}
// Prand32 generates a pseudo random number from a seed.
func Prand64[T ~uint64](seed T) T {
seed ^= seed << 13
seed ^= seed >> 7
seed ^= seed << 17
return seed
}
-22
View File
@@ -28,7 +28,6 @@ type Conn struct {
mu sync.Mutex
h Handler
remoteAddr []byte
nanoTime func() int64 // monotonic clock source; set by Configure.
rdead time.Time
wdead time.Time
@@ -56,10 +55,6 @@ type ConnConfig struct {
TxBuf []byte
TxPacketQueueSize int
Logger *slog.Logger
// NanoTime returns the current monotonic time in nanoseconds.
// Used for retransmission timing (RFC 6298).
// If nil, defaults to a function that calls time.Now().UnixNano().
NanoTime func() int64
}
func (conn *Conn) Configure(config ConnConfig) (err error) {
@@ -70,19 +65,9 @@ func (conn *Conn) Configure(config ConnConfig) (err error) {
return err
}
conn.logger.log = config.Logger
conn.nanoTime = config.NanoTime // nil is fine; conn.now() falls back to time.Now().
return nil
}
// now returns the current monotonic time in nanoseconds.
// Uses the configured NanoTime function or falls back to time.Now().UnixNano().
func (conn *Conn) now() int64 {
if conn.nanoTime != nil {
return conn.nanoTime()
}
return time.Now().UnixNano()
}
// LocalPort returns the local port on which the socket is listening or connected to.
func (conn *Conn) LocalPort() uint16 {
conn.mu.Lock()
@@ -354,7 +339,6 @@ func (conn *Conn) Demux(buf []byte, off int) (err error) {
return lneto.ErrMismatch
}
conn.trace("tcpconn.Recv", slog.Uint64("lport", uint64(conn.h.LocalPort())), slog.Uint64("rport", uint64(conn.h.remotePort)))
conn.h.SetNow(uint32(conn.now() / 1e6)) // ns → ms for accurate ACK timestamps.
err = conn.h.Recv(buf[off:])
if err != nil {
return err
@@ -382,12 +366,6 @@ func (conn *Conn) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int)
} else if len(raddr) != len(conn.remoteAddr) {
return 0, lneto.ErrMismatchLen
}
conn.h.SetNow(uint32(conn.now() / 1e6)) // ns → ms.
// RFC 6298 §5.1: check RTO before sending new data.
if conn.h.ShouldRetransmit() {
conn.h.triggerRetransmit()
conn.h.dupACKs = 0 // RTO is a new loss event; reset dup-ACK counter.
}
n, err = conn.h.Send(carrierData[offsetToFrame:])
if err != nil || n == 0 {
return 0, err
+73 -27
View File
@@ -9,6 +9,13 @@ import (
"github.com/soypat/lneto/internal"
)
const (
// signals to create a retransmit packet after receiving this number of duplicate acks, not including the ack that set UNA.
retransmitAfterDupacks = 3
// retransmitMaxQueued sets maximum amount of retransmits to queue while receiving dupacks.
retransmitMaxQueued = 2
)
// ControlBlock is a partial Transmission Control Block (TCB) implementation as
// per RFC 9293 in section 3.3.1. In contrast with the description in RFC9293,
// this implementation is limited to receiving only sequential segments.
@@ -59,6 +66,10 @@ type ControlBlock struct {
pending [2]Flags
_state State // leading underscore so field not suggested on top of exported State method when developing.
challengeAck bool
// dupack counts received ACK==snd.UNA && ACK<snd.NXT received. Does not count ack that set UNA.
dupack uint8
// nRetransmit counts number of retransmits sent since last UNA update.
nRetransmit uint8
}
// State returns the current state of the TCP connection. See [State].
@@ -107,6 +118,13 @@ func (tcb *ControlBlock) IncomingIsKeepalive(incomingSegment Segment) bool {
incomingSegment.ACK == tcb.snd.NXT && incomingSegment.DATALEN == 0
}
// IncomingIsDupACK returns true if the ACK value is a duplicate acknowledgement:
// the ACK equals the oldest unacknowledged sequence number (snd.UNA) meaning no
// new data is acknowledged, while snd.UNA < snd.NXT meaning data is in flight.
func (tcb *ControlBlock) IncomingIsDupACK(ack Value) bool {
return ack == tcb.snd.UNA && ack.LessThan(tcb.snd.NXT)
}
// MakeKeepalive creates a TCP keepalive segment. This segment
// should not be passed into Recv or Send methods.
func (tcb *ControlBlock) MakeKeepalive() Segment {
@@ -119,6 +137,21 @@ func (tcb *ControlBlock) MakeKeepalive() Segment {
}
}
// MakeDupACK returns a duplicate ACK segment suitable for fast-retransmit
// recovery signaling, without advancing the sender ACK boundary. Useful for:
// - constructing an explicit duplicate ACK from local state (e.g. test harness),
// - expressing retransmit-request condition (`ACK == snd.UNA`, `SEQ == snd.UNA`)
// - advertising receive window via current `rcv.WND`.
func (tcb *ControlBlock) MakeDupACK() Segment {
return Segment{
SEQ: tcb.snd.UNA,
ACK: tcb.rcv.NXT,
Flags: FlagACK,
WND: tcb.rcv.WND,
DATALEN: 0,
}
}
// sendSpace contains Send Sequence Space data. Its sequence numbers correspond to local data.
type sendSpace struct {
ISS Value // initial send sequence number, defined locally on connection start
@@ -181,17 +214,29 @@ func (tcb *ControlBlock) prepareToHandshake(iss Value, wnd Size, newState State)
}
// HasPending returns true if there is a pending control segment to send. Calls to Send will advance the pending queue.
func (tcb *ControlBlock) HasPending() bool { return tcb.pending[0] != 0 }
func (tcb *ControlBlock) HasPending() bool {
return tcb.pending[0] != 0 || tcb.challengeAck || tcb.HasPendingRetransmit()
}
// HasPending returns true if the control block is pending a retransmit according to simple optmist
// retransmit strategy.
func (tcb *ControlBlock) HasPendingRetransmit() bool {
// Force retransmit after 3 consecutive acks of UNA.
return tcb._state.TxDataOpen() && tcb.dupack >= retransmitAfterDupacks && tcb.nRetransmit <= tcb.dupack-retransmitAfterDupacks
}
// PendingSegment calculates a suitable next segment to send from a payload length.
// It does not modify the ControlBlock state or pending segment queue.
func (tcb *ControlBlock) PendingSegment(payloadLen int) (_ Segment, ok bool) {
pending := tcb.pending[0]
if tcb.challengeAck {
// Do not clear challengeAck here: PendingSegment is documented as read-only.
// The flag is consumed in Send when the ACK segment is actually transmitted.
return Segment{SEQ: tcb.snd.NXT, ACK: tcb.rcv.NXT, Flags: FlagACK, WND: tcb.rcv.WND}, true
} else if !pending.HasAny(flagctl) && tcb.HasPendingRetransmit() {
// Optimist Strategy: retransmit oldest data once.
return Segment{SEQ: tcb.snd.UNA, DATALEN: Size(payloadLen), ACK: tcb.rcv.NXT, WND: tcb.rcv.WND, Flags: FlagACK}, true
}
pending := tcb.pending[0]
established := tcb._state == StateEstablished
canSendData := established || tcb._state == StateCloseWait
if !canSendData {
@@ -217,9 +262,6 @@ func (tcb *ControlBlock) PendingSegment(payloadLen int) (_ Segment, ok bool) {
if tcb.snd.MSS > 0 && payloadLen > int(tcb.snd.MSS) {
payloadLen = int(tcb.snd.MSS)
}
if payloadLen > 0 {
pending |= FlagPSH // By default ensure all data flushed to destination application immediately on receive.
}
if canSendData {
pending |= FlagACK // ACK is always set in established state. Not in RFC9293 but somehow expected?
@@ -321,10 +363,19 @@ func (tcb *ControlBlock) Recv(seg Segment) (err error) {
tcb.snd.WL1 = seg.SEQ
tcb.snd.WL2 = seg.ACK
}
if seg.Flags.HasAny(FlagACK) && tcb.snd.UNA.LessThan(seg.ACK) && seg.ACK.LessThanEq(tcb.snd.NXT) {
// Only update ACK if it advances UNA and is not in the future.
tcb.snd.UNA = seg.ACK
if seg.Flags.HasAny(FlagACK) && seg.ACK.LessThanEq(tcb.snd.NXT) {
if tcb.IncomingIsDupACK(seg.ACK) && tcb.State().TxDataOpen() && !seg.Flags.HasAny(flagctl) && tcb.dupack < tcb.nRetransmit+retransmitMaxQueued+retransmitMaxQueued {
// Duplicate ack. Don't advance dupack counter past scb.nRetransmit+retransmitAfterDupacks
tcb.dupack++
} else if tcb.snd.UNA.LessThan(seg.ACK) {
// Only update ACK if it advances UNA and is not in the future.
tcb.snd.UNA = seg.ACK
tcb.dupack = 0
tcb.nRetransmit = 0
}
}
seglen := seg.LEN()
tcb.rcv.NXT.UpdateForward(seglen)
@@ -385,9 +436,16 @@ func (tcb *ControlBlock) Send(seg Segment) error {
// The segment is valid, we can update TCB state.
seglen := seg.LEN()
tcb.snd.NXT.UpdateForward(seglen)
tcb.rcv.WND = seg.WND
retransmit := seg.SEQ.LessThan(tcb.snd.NXT)
if retransmit {
if tcb.nRetransmit < 255-retransmitMaxQueued-retransmitAfterDupacks {
tcb.nRetransmit++
}
} else {
tcb.snd.NXT.UpdateForward(seglen)
}
tcb.rcv.WND = seg.WND
if tcb.logenabled(internal.LevelTrace) {
tcb.traceSnd("tcb:snd")
tcb.traceSeg("tcb:snd", seg)
@@ -405,6 +463,7 @@ func (tcb *ControlBlock) validateOutgoingSegment(seg Segment) (err error) {
zeroWindowOK := tcb.snd.WND == 0 && seg.DATALEN == 0 && seg.SEQ == tcb.snd.NXT
outOfWindow := checkSeq && !seg.SEQ.InWindow(tcb.snd.NXT, tcb.snd.WND) &&
!zeroWindowOK
isRetransmit := checkSeq && seg.SEQ.InRange(tcb.snd.UNA, tcb.snd.NXT)
switch {
case tcb._state == StateClosed && !isFirst:
err = io.ErrClosedPipe
@@ -413,7 +472,7 @@ func (tcb *ControlBlock) validateOutgoingSegment(seg Segment) (err error) {
case hasAck && seg.ACK != tcb.rcv.NXT:
err = errAckNotNext
case outOfWindow:
case outOfWindow && !isRetransmit:
if tcb.snd.WND == 0 {
err = errZeroWindow
} else {
@@ -426,7 +485,7 @@ func (tcb *ControlBlock) validateOutgoingSegment(seg Segment) (err error) {
case checkSeq && tcb.snd.WND == 0 && seg.DATALEN > 0 && seg.SEQ == tcb.snd.NXT:
err = errZeroWindow
case checkSeq && !seglast.InWindow(tcb.snd.NXT, tcb.snd.WND) && !zeroWindowOK:
case checkSeq && !seglast.InWindow(tcb.snd.NXT, tcb.snd.WND) && !zeroWindowOK && !isRetransmit:
err = errLastNotInWindow
}
return err
@@ -489,7 +548,7 @@ func (tcb *ControlBlock) validateIncomingSegment(seg Segment) (err error) {
case established && acksOld && !ctlOrDataSegment:
// We don't drop packet.
if isDebug {
tcb.debug("rcv:ACK-dup", slog.String("state", tcb._state.String()),
tcb.debug("rcv:ACK-old", slog.String("state", tcb._state.String()),
slog.Uint64("seg.ack", uint64(seg.ACK)), slog.Uint64("snd.una", uint64(tcb.snd.UNA)))
}
@@ -571,20 +630,7 @@ func (tcb *ControlBlock) rstJump() Value {
// and Send calls to retransmit unacknowledged data. Must be paired with
// ringTx.RetransmitFromUNA to rewind the transmit buffer.
// Implements RFC 9293 §3.10.8 (RETRANSMISSION TIMEOUT).
func (tcb *ControlBlock) Retransmit() { tcb.snd.NXT = tcb.snd.UNA }
// RecoveryACK accepts a cumulative ACK that covers data sent before a retransmit
// rewind. After Retransmit() rewinds snd.NXT, the remote may ACK data it received
// pre-rewind — a valid cumulative ACK that exceeds the rewound snd.NXT. This method
// advances snd.UNA, snd.NXT and updates the send window from the segment.
// The caller must verify that seg.ACK is within the pre-rewind NXT range.
func (tcb *ControlBlock) RecoveryACK(seg Segment) {
tcb.snd.UNA = seg.ACK
tcb.snd.NXT = seg.ACK
tcb.snd.WND = seg.WND
// Clear any pending ACK that validateIncomingSegment queued on rejection.
tcb.pending[0] &^= FlagACK
}
// func (tcb *ControlBlock) Retransmit() { tcb.snd.NXT = tcb.snd.UNA }
// Abort sets ControlBlock state to Closed and resets all sequence numbers and pending flag.
// No more data can be sent nor received after the connection is aborted until opened again.
+82
View File
@@ -293,3 +293,85 @@ func TestCloseWait_NoAutoFINBeforeUserClose(t *testing.T) {
"(control.go:353-354 queues finack on any ACK sent in CLOSE-WAIT)", seg.Flags)
}
}
func TestPendingSegment_RetransmitAfter3DupACKs(t *testing.T) {
const (
iss Value = 100
remoteISS Value = 500
inFlight = 10
wnd Size = 1024
)
var tcb ControlBlock
tcb.HelperInitState(StateEstablished, iss, iss+inFlight, wnd)
tcb.HelperInitRcv(remoteISS, remoteISS+1, wnd)
// Three duplicate ACKs against UNA must trigger retransmit state
for i := 0; i < 3; i++ {
dup := Segment{
SEQ: remoteISS + 1,
ACK: iss, // UNA (duplicate, no progress)
Flags: FlagACK,
WND: wnd,
}
if !tcb.IncomingIsDupACK(dup.ACK) {
t.Fatal("supposed duplicate ack segment not considered dupack")
}
if err := tcb.Recv(dup); err != nil {
t.Fatalf("dup ACK %d: unexpected error: %v", i+1, err)
}
}
if tcb.dupack != 3 {
t.Fatalf("dupack = %d; want 3", tcb.dupack)
}
if !tcb.HasPendingRetransmit() {
t.Fatal("expected HasPendingRetransmit() == true after 3 dupacks")
}
seg, ok := tcb.PendingSegment(4)
if !ok {
t.Fatal("PendingSegment(false) returned no segment; expected retransmit segment")
}
if seg.SEQ != tcb.snd.UNA {
t.Fatalf("retransmit SEQ = %d; want UNA(%d)", seg.SEQ, tcb.snd.UNA)
}
if seg.ACK != tcb.rcv.NXT {
t.Fatalf("retransmit ACK = %d; want RCV.NXT(%d)", seg.ACK, tcb.rcv.NXT)
}
if !seg.Flags.HasAny(FlagACK) {
t.Errorf("retransmit segment must include ACK")
}
// Send retransmit, expect nRetransmit to be incremented and NXT not moved
prevNXT := tcb.snd.NXT
if err := tcb.Send(seg); err != nil {
t.Fatalf("Send(retransmit) unexpected error: %v", err)
}
if tcb.nRetransmit != 1 {
t.Fatalf("nRetransmit = %d; want 1", tcb.nRetransmit)
}
if tcb.snd.NXT != prevNXT {
t.Fatalf("snd.NXT advanced on retransmit: got %d, want %d", tcb.snd.NXT, prevNXT)
}
if tcb.HasPendingRetransmit() {
t.Fatal("expected retransmit reservation gone after retransmit send")
}
// Deliver cumulative ACK for all in-flight data => reset dupack + nRetransmit
successACK := Segment{
SEQ: remoteISS + 1,
ACK: iss + inFlight,
Flags: FlagACK,
WND: wnd,
}
if err := tcb.Recv(successACK); err != nil {
t.Fatalf("successful ACK unexpected err: %v", err)
}
if tcb.dupack != 0 {
t.Fatalf("dupack after progress = %d; want 0", tcb.dupack)
}
if tcb.nRetransmit != 0 {
t.Fatalf("nRetransmit after progress = %d; want 0", tcb.nRetransmit)
}
}
+2
View File
@@ -172,6 +172,8 @@ const (
synack = FlagSYN | FlagACK
finack = FlagFIN | FlagACK
pshack = FlagPSH | FlagACK
flagctl = FlagSYN | FlagFIN | FlagRST
)
// HasAll checks if mask bits are all set in the receiver flags.
+24 -112
View File
@@ -30,22 +30,8 @@ type Handler struct {
optcodec OptionCodec
closing bool
// dupACKs counts consecutive duplicate ACKs for fast retransmit (RFC 5681 §3.2).
dupACKs uint8
// nRetx counts consecutive retransmissions for exponential backoff (RFC 6298 §5.5).
nRetx uint8
// Retransmission timer state — all uint32 milliseconds, no time package needed.
// rto is the current retransmission timeout in ms; starts at 1000 per RFC 6298 §2.1.
rto uint32
// now is the current time in ms, set by Conn before Send/Recv via SetNow.
now uint32
// lastACK is the last ACK value seen, for duplicate ACK detection (RFC 5681 §3.2).
lastACK Value
// retransmitNXT is the pre-rewind value of snd.NXT, saved when fast retransmit
// fires. A cumulative ACK with seg.ACK <= retransmitNXT is valid even if it
// exceeds the rewound snd.NXT. Zero means not in recovery.
retransmitNXT Value
// nRetransmit stores the number of times the oldest packet was retransmit.
nRetransmit uint8
}
func (h *Handler) SetLoggers(handler, scb *slog.Logger) {
@@ -142,19 +128,11 @@ func (h *Handler) reset(localPort, remotePort uint16, iss Value) {
validator: h.validator,
logger: h.logger,
closing: false,
rto: rtoInitial, // RFC 6298 §2.1: initial RTO = 1s.
}
h.bufTx.ResetOrReuse(nil, 0, iss)
h.bufRx.Reset()
}
const (
// rtoInitial is the initial RTO per RFC 6298 §2.1: "the sender SHOULD set RTO <- 1 second".
rtoInitial uint32 = 1000
// rtoMax caps exponential backoff per RFC 6298 §2.5.
rtoMax uint32 = 60_000
)
// Recv receives an incoming TCP packet frame with the first byte being the first octet of the TCP frame.
// The [Handler]'s internal state is updated if the packet is admitted successfully.
func (h *Handler) Recv(incomingPacket []byte) error {
@@ -188,34 +166,16 @@ func (h *Handler) Recv(incomingPacket []byte) error {
h.info("tcp.Handler:rx-keepalive", slog.Uint64("port", uint64(h.localPort)))
return nil
}
prevState := h.scb.State()
prevUNA := h.scb.snd.UNA // Capture before Recv updates snd.UNA (RFC 6298 §5.3).
err = h.scb.Recv(segIncoming)
if err != nil {
// Recovery path: after fast retransmit rewinds snd.NXT, a cumulative ACK
// for data sent pre-rewind exceeds the rewound NXT. The ControlBlock rejects
// it, but we know it's valid if ACK <= retransmitNXT (pre-rewind high water mark).
if h.retransmitNXT != 0 && segIncoming.Flags.HasAny(FlagACK) &&
h.scb.snd.NXT.LessThan(segIncoming.ACK) &&
segIncoming.ACK.LessThanEq(h.retransmitNXT) {
// TODO: This is a very hacky workaround. It'd be great
// to detect recover acks in Handler before calling ControlBlock.Recv
// and handle it cleanly instead of with an error.
h.scb.RecoveryACK(segIncoming)
h.bufTx.RecoveryACK(segIncoming.ACK)
h.retransmitNXT = 0
h.rto = rtoInitial
h.nRetx = 0
h.dupACKs = 0
h.lastACK = segIncoming.ACK
err = nil // Accept the segment.
} else {
if h.scb.State() == StateClosed {
// TODO(soypat): Should return EOF/ErrClosed?
err = net.ErrClosed //err // Connection closed by reset.
}
return err
if h.scb.State() == StateClosed {
// TODO(soypat): Should return EOF/ErrClosed?
err = net.ErrClosed //err // Connection closed by reset.
}
return err
}
if h.scb.State() == StateClosed {
// TCB aborted, likely because it received an ACK in LastAck state.
@@ -232,27 +192,12 @@ func (h *Handler) Recv(incomingPacket []byte) error {
}
}
if segIncoming.Flags.HasAny(FlagACK) {
// Update TX ring buffer to free up acked data.
h.bufTx.RecvACK(segIncoming.ACK)
// Dup-ACK tracking per RFC 5681 §3.2 and RTO reset per RFC 6298 §5.3.
if segIncoming.ACK != prevUNA && prevUNA.LessThan(segIncoming.ACK) {
// New data acknowledged — reset RTO and dup-ACK counter.
h.rto = rtoInitial // RFC 6298 §5.3.
h.nRetx = 0
h.dupACKs = 0
h.lastACK = segIncoming.ACK
} else if segIncoming.ACK == h.lastACK && segIncoming.DATALEN == 0 &&
!segIncoming.Flags.HasAny(FlagSYN|FlagFIN) && h.bufTx.BufferedSent() > 0 {
// Duplicate ACK per RFC 5681 §2: same ACK, no data, no SYN/FIN,
// and receiver has outstanding data.
h.dupACKs++
if h.dupACKs == 3 {
// RFC 5681 §3.2: "After receiving 3 duplicate ACKs [...]
// TCP performs a retransmission of what appears to be the
// missing segment, without waiting for the retransmission
// timer to expire."
h.triggerRetransmit()
}
if segIncoming.ACK == prevUNA {
// scb keeping track of duplicate acks.
h.info("tcp.Handler:dupack", slog.Uint64("ndupack", uint64(h.scb.dupack)), slog.Uint64("ack", uint64(segIncoming.ACK)), slog.Uint64("lport", uint64(h.localPort)), slog.Uint64("rport", uint64(h.remotePort)))
} else {
// Update TX ring buffer to free up acked data.
h.bufTx.RecvACK(segIncoming.ACK)
}
}
if segIncoming.Flags.HasAny(FlagSYN) {
@@ -340,23 +285,24 @@ func (h *Handler) Send(b []byte) (int, error) {
offset++
} else {
var ok bool
available := min(buffered, len(b)-sizeHeaderTCP)
segment, ok = h.scb.PendingSegment(available)
maxPayload := len(b) - sizeHeaderTCP
segment, ok = h.scb.PendingSegment(maxPayload)
segment.WND = Size(h.bufRx.Free())
if !ok {
// No pending control segment or data to send. Yield.
return 0, nil
}
if segment.DATALEN > 0 {
n, err := h.bufTx.MakePacket(b[sizeHeaderTCP:sizeHeaderTCP+segment.DATALEN], segment.SEQ, h.now)
if err != nil {
return 0, err
} else if n != int(segment.DATALEN) {
panic("expected n == available")
}
} else if segment.Flags == synack {
h.optcodec.PutOption16(b[sizeHeaderTCP:], OptMaxSegmentSize, mss)
offset++
} else if segment.DATALEN > 0 {
n, err := h.bufTx.MakePacket(b[sizeHeaderTCP:sizeHeaderTCP+segment.DATALEN], segment.SEQ)
if err != nil {
return 0, err
}
segment.DATALEN = Size(n)
if n > 0 {
segment.Flags |= FlagPSH
}
}
}
prevState := h.scb.State()
@@ -492,40 +438,6 @@ func min(a, b int) int {
return b
}
// SetNow sets the current time in milliseconds for retransmission timing.
// Must be called by Conn before Send/Recv operations.
func (h *Handler) SetNow(ms uint32) { h.now = ms }
// ShouldRetransmit returns true if the retransmission timeout has expired
// on the oldest unacknowledged segment. Per RFC 6298 §5.1 and §5.4.
func (h *Handler) ShouldRetransmit() bool {
oldest := h.bufTx.slist.Oldest()
if oldest == nil {
return false
}
return h.now-oldest.sentAt >= h.rto
}
// triggerRetransmit rewinds the transmit queue and control block so the next
// Send call retransmits from snd.UNA. Per RFC 9293 §3.10.8, RFC 6298 §5.45.5.
func (h *Handler) triggerRetransmit() {
// Save the high-water mark of NXT before rewinding so that cumulative ACKs
// for data sent pre-rewind can still be accepted (see Recv recovery path).
if h.retransmitNXT == 0 || h.retransmitNXT.LessThan(h.scb.snd.NXT) {
h.retransmitNXT = h.scb.snd.NXT
}
h.scb.Retransmit()
h.bufTx.RetransmitFromUNA()
// RFC 6298 §5.5: "The host MUST set RTO <- RTO * 2 ('back off the timer')."
h.nRetx++
h.rto *= 2
if h.rto > rtoMax {
h.rto = rtoMax
}
h.debug("tcp.Handler:retransmit", slog.Uint64("port", uint64(h.localPort)),
slog.Uint64("rto", uint64(h.rto)), slog.Uint64("nRetx", uint64(h.nRetx)))
}
func errstr(err error) string {
if err == nil {
return "<nil>"
-674
View File
@@ -1,674 +0,0 @@
package tcp
import (
"math/rand"
"testing"
)
// TestRTOResetsOnNewACK is a regression test for a bug where prevUNA was
// captured AFTER ControlBlock.Recv updated snd.UNA, making the "new ACK"
// condition (seg.ACK != prevUNA) always false. This caused the RTO to never
// reset per RFC 6298 §5.3, leading to exponential backoff escalation even
// when the network was healthy.
//
// The fix: capture prevUNA before calling scb.Recv in Handler.Recv.
func TestRTOResetsOnNewACK(t *testing.T) {
const mtu = 1500
const maxpackets = 3
rng := rand.New(rand.NewSource(100))
client, server := newHandler(t, mtu, maxpackets), newHandler(t, mtu, maxpackets)
setupClientServer(t, rng, client, server)
var rawbuf [mtu]byte
establish(t, client, server, rawbuf[:])
// Write and send data from client.
data := []byte("hello retransmit")
n, err := client.Write(data)
if err != nil {
t.Fatal("client write:", err)
} else if n != len(data) {
t.Fatal("short write")
}
clear(rawbuf[:])
n, err = client.Send(rawbuf[:])
if err != nil {
t.Fatal("client send:", err)
}
// Simulate prior retransmissions: RTO has been backed off and nRetx > 0.
client.rto = rtoInitial * 4
client.nRetx = 2
// Server receives data and sends ACK.
err = server.Recv(rawbuf[:n])
if err != nil {
t.Fatal("server recv:", err)
}
clear(rawbuf[:])
n, err = server.Send(rawbuf[:])
if err != nil {
t.Fatal("server send ACK:", err)
}
if n == 0 {
t.Fatal("expected server to send ACK")
}
// Client receives ACK — RTO and nRetx should reset.
err = client.Recv(rawbuf[:n])
if err != nil {
t.Fatal("client recv ACK:", err)
}
if client.rto != rtoInitial {
t.Fatalf("BUG: RTO not reset on new ACK: got %d, want %d (RFC 6298 §5.3)", client.rto, rtoInitial)
}
if client.nRetx != 0 {
t.Fatalf("BUG: nRetx not reset on new ACK: got %d, want 0", client.nRetx)
}
if client.dupACKs != 0 {
t.Fatalf("dupACKs not reset on new ACK: got %d, want 0", client.dupACKs)
}
}
// TestPostRetransmitACKAccepted is a regression test for a bug where after
// Retransmit() rewound snd.NXT to snd.UNA, a valid cumulative ACK from the
// remote (acknowledging data sent pre-rewind) was rejected as "acks unsent
// data" because seg.ACK > snd.NXT.
//
// The fix: in validateIncomingSegment, when snd.NXT == snd.UNA (retransmit
// active) and seg.ACK is within the send window, accept the ACK and advance
// snd.NXT to seg.ACK.
func TestPostRetransmitACKAccepted(t *testing.T) {
const mtu = 1500
const maxpackets = 3
rng := rand.New(rand.NewSource(200))
client, server := newHandler(t, mtu, maxpackets), newHandler(t, mtu, maxpackets)
setupClientServer(t, rng, client, server)
var rawbuf [mtu]byte
establish(t, client, server, rawbuf[:])
// Client writes and sends data.
data := []byte("data before rewind")
n, err := client.Write(data)
if err != nil {
t.Fatal("client write:", err)
} else if n != len(data) {
t.Fatal("short write")
}
clear(rawbuf[:])
n, err = client.Send(rawbuf[:])
if err != nil {
t.Fatal("client send:", err)
}
// Server receives data — its next ACK will acknowledge up to the
// original snd.NXT.
err = server.Recv(rawbuf[:n])
if err != nil {
t.Fatal("server recv:", err)
}
// Client triggers retransmit: snd.NXT rewound to snd.UNA.
preRewindNXT := client.scb.snd.NXT
client.triggerRetransmit()
if client.scb.snd.NXT != client.scb.snd.UNA {
t.Fatal("retransmit did not rewind snd.NXT to snd.UNA")
}
// Server sends ACK for the data it already received. seg.ACK = preRewindNXT,
// which is > client.snd.NXT (now rewound to snd.UNA).
clear(rawbuf[:])
n, err = server.Send(rawbuf[:])
if err != nil {
t.Fatal("server send ACK:", err)
}
if n == 0 {
t.Fatal("expected server to send ACK")
}
// Client receives ACK — should NOT be rejected.
err = client.Recv(rawbuf[:n])
if err != nil {
t.Fatalf("BUG: post-retransmit ACK rejected: %v\n"+
"After Retransmit() rewound snd.NXT to snd.UNA, the remote's cumulative\n"+
"ACK (for data sent pre-rewind) exceeds the rewound snd.NXT and was\n"+
"incorrectly rejected as 'acks unsent data'.", err)
}
// snd.NXT should have advanced back to where it was before the rewind.
if client.scb.snd.NXT != preRewindNXT {
t.Fatalf("snd.NXT not restored: got %d, want %d", client.scb.snd.NXT, preRewindNXT)
}
// snd.UNA should have advanced to acknowledge the data.
if client.scb.snd.UNA != preRewindNXT {
t.Fatalf("snd.UNA not advanced: got %d, want %d", client.scb.snd.UNA, preRewindNXT)
}
}
// TestRecoveryACKSkipsSpuriousRetransmit verifies that after fast retransmit
// rewinds snd.NXT and the client re-sends the lost segment, a cumulative ACK
// from the remote (acknowledging all data received before and after the hole)
// is accepted — even though it exceeds the rewound snd.NXT.
//
// Without this fix, lneto rejects the cumulative ACK as "acks unsent data"
// and then spuriously retransmits data that was already received by the remote.
//
// Timeline:
// 1. Client sends packets 0..N; packet 1 is lost (the "hole")
// 2. Server ACKs packet 0; sends 3 dup ACKs → fast retransmit fires
// 3. Client rewinds to snd.UNA, re-sends lost segment → snd.NXT advances by 1 MSS
// 4. Server (having received all other packets) sends cumulative ACK for ALL data
// 5. Client should accept this ACK (not reject it) and NOT send spurious retransmissions
func TestRecoveryACKSkipsSpuriousRetransmit(t *testing.T) {
const mtu = 60 // 20-byte header + 40-byte payload per packet.
const txBuf = 2048
const maxpackets = 10
rng := rand.New(rand.NewSource(59))
client := new(Handler)
server := new(Handler)
err := client.SetBuffers(make([]byte, txBuf), make([]byte, txBuf), maxpackets)
if err != nil {
t.Fatal(err)
}
client.rto = rtoInitial
err = server.SetBuffers(make([]byte, txBuf), make([]byte, txBuf), maxpackets)
if err != nil {
t.Fatal(err)
}
server.rto = rtoInitial
err = server.OpenListen(uint16(rng.Uint32()), 0)
if err != nil {
t.Fatal(err)
}
err = client.OpenActive(uint16(rng.Uint32()), server.LocalPort(), 0)
if err != nil {
t.Fatal(err)
}
var rawbuf [mtu]byte
establish(t, client, server, rawbuf[:])
// Send enough data to fill several packets (MSS=40).
data := make([]byte, 40*6) // 6 packets worth of data.
for i := range data {
data[i] = byte(i)
}
written := 0
var packets [][]byte
for written < len(data) {
n, werr := client.Write(data[written:])
if werr != nil {
t.Fatal("client write:", werr)
}
written += n
for {
clear(rawbuf[:])
ns, serr := client.Send(rawbuf[:])
if serr != nil {
t.Fatal("client send:", serr)
}
if ns == 0 {
break
}
packets = append(packets, append([]byte(nil), rawbuf[:ns]...))
}
}
if len(packets) < 4 {
t.Fatalf("need at least 4 data packets, got %d", len(packets))
}
t.Logf("sent %d data packets", len(packets))
// Record the sequence endpoint: this is the ACK value the server will
// send once it receives all data (including the "lost" packet).
preRewindNXT := client.scb.snd.NXT
t.Logf("pre-rewind snd.NXT=%d, snd.UNA=%d", preRewindNXT, client.scb.snd.UNA)
// Server receives only packet 0 → ACKs it. This establishes lastACK on client.
err = server.Recv(packets[0])
if err != nil {
t.Fatal("server recv pkt0:", err)
}
clear(rawbuf[:])
n, err := server.Send(rawbuf[:])
if err != nil {
t.Fatal("server send ACK:", err)
}
if n == 0 {
t.Fatal("expected server to send ACK")
}
err = client.Recv(rawbuf[:n])
if err != nil {
t.Fatal("client recv ACK:", err)
}
dupACKValue := client.lastACK
t.Logf("lastACK=%d after first ACK", dupACKValue)
// Craft 3 dup ACKs (packet 1 is "lost", server keeps acking dupACKValue).
for i := 0; i < 3; i++ {
var buf [mtu]byte
frm, ferr := NewFrame(buf[:])
if ferr != nil {
t.Fatal(ferr)
}
frm.SetSourcePort(server.LocalPort())
frm.SetDestinationPort(client.LocalPort())
frm.SetSegment(Segment{
SEQ: server.scb.snd.NXT,
ACK: dupACKValue,
Flags: FlagACK,
WND: 65535,
}, 5)
rerr := client.Recv(buf[:sizeHeaderTCP])
if rerr != nil {
t.Logf("dup ACK %d recv err (expected): %v", i+1, rerr)
}
}
if client.nRetx == 0 {
t.Fatal("fast retransmit did not fire after 3 dup ACKs")
}
t.Logf("fast retransmit fired: snd.NXT=%d, snd.UNA=%d", client.scb.snd.NXT, client.scb.snd.UNA)
// Client re-sends the lost segment. After this, snd.NXT > snd.UNA
// (advanced by one MSS), but still < preRewindNXT.
clear(rawbuf[:])
n, err = client.Send(rawbuf[:])
if err != nil {
t.Fatal("client retransmit send:", err)
}
if n == 0 {
t.Fatal("expected client to send retransmit packet")
}
if client.scb.snd.NXT == client.scb.snd.UNA {
t.Fatal("expected snd.NXT to advance past snd.UNA after re-send")
}
t.Logf("after retransmit send: snd.NXT=%d, snd.UNA=%d (preRewind=%d)",
client.scb.snd.NXT, client.scb.snd.UNA, preRewindNXT)
// Craft cumulative ACK from server for ALL data (as if server had received
// everything and the lost packet just arrived, filling the hole).
{
var buf [mtu]byte
frm, ferr := NewFrame(buf[:])
if ferr != nil {
t.Fatal(ferr)
}
frm.SetSourcePort(server.LocalPort())
frm.SetDestinationPort(client.LocalPort())
frm.SetSegment(Segment{
SEQ: server.scb.snd.NXT,
ACK: preRewindNXT, // ACKs all data sent before the rewind.
Flags: FlagACK,
WND: 65535,
}, 5)
err = client.Recv(buf[:sizeHeaderTCP])
if err != nil {
t.Fatalf("BUG: cumulative recovery ACK rejected: %v\n"+
"After fast retransmit rewound snd.NXT and client re-sent one packet,\n"+
"the remote's cumulative ACK (seg.ACK=%d) exceeds the current snd.NXT=%d\n"+
"and is incorrectly rejected as 'acks unsent data'.\n"+
"This causes spurious retransmissions of already-received data.",
err, preRewindNXT, client.scb.snd.NXT)
}
}
// snd.UNA should have advanced to cover all original data.
if client.scb.snd.UNA != preRewindNXT {
t.Fatalf("snd.UNA not advanced: got %d, want %d", client.scb.snd.UNA, preRewindNXT)
}
// snd.NXT should be at least preRewindNXT.
if client.scb.snd.NXT.LessThan(preRewindNXT) {
t.Fatalf("snd.NXT behind preRewindNXT: got %d, want >= %d", client.scb.snd.NXT, preRewindNXT)
}
// No more data should be sent — any Send() output here is a spurious retransmission.
clear(rawbuf[:])
n, err = client.Send(rawbuf[:])
if err != nil {
t.Fatal("client send after recovery:", err)
}
if n != 0 {
t.Fatalf("BUG: spurious retransmission after recovery ACK: sent %d bytes.\n"+
"All data was already acknowledged by the cumulative ACK, but the client\n"+
"still has 'unsent' data in the TX buffer that was actually received.", n)
}
}
// TestFastRetransmitOncePerLoss is a regression test for
// https://github.com/soypat/lneto/issues/58
// where fast retransmit was triggered multiple times for the same lost segment.
//
// When N packets are in flight and one is lost, up to N dup ACKs arrive.
// The bug: triggerRetransmit() reset dupACKs to 0, so every 3 dup ACKs
// triggered another fast retransmit of the same sequence. With 10 packets
// in flight, a single loss caused 3 retransmissions instead of 1.
//
// Per RFC 5681 §3.2, fast retransmit should fire once per loss event.
// Subsequent dup ACKs (beyond the 3rd) should NOT re-trigger it.
func TestFastRetransmitOncePerLoss(t *testing.T) {
const mtu = 60 // Small MTU: 20 byte header + 40 bytes payload per packet.
const txBuf = 2048
const maxpackets = 10
rng := rand.New(rand.NewSource(58))
client := new(Handler)
server := new(Handler)
err := client.SetBuffers(make([]byte, txBuf), make([]byte, txBuf), maxpackets)
if err != nil {
t.Fatal(err)
}
client.rto = rtoInitial
err = server.SetBuffers(make([]byte, txBuf), make([]byte, txBuf), maxpackets)
if err != nil {
t.Fatal(err)
}
server.rto = rtoInitial
err = server.OpenListen(uint16(rng.Uint32()), 0)
if err != nil {
t.Fatal(err)
}
err = client.OpenActive(uint16(rng.Uint32()), server.LocalPort(), 0)
if err != nil {
t.Fatal(err)
}
var rawbuf [mtu]byte
establish(t, client, server, rawbuf[:])
// With MSS=40 (mtu-20), write enough to fill several packets.
data := make([]byte, 40*maxpackets)
for i := range data {
data[i] = byte(i)
}
// Write in chunks since TX buffer may limit us.
written := 0
var packets [][]byte
for written < len(data) {
n, werr := client.Write(data[written:])
if werr != nil {
t.Fatal("client write:", werr)
}
written += n
// Send as many packets as possible.
for {
clear(rawbuf[:])
ns, serr := client.Send(rawbuf[:])
if serr != nil {
t.Fatal("client send:", serr)
}
if ns == 0 {
break
}
packets = append(packets, append([]byte(nil), rawbuf[:ns]...))
}
}
if len(packets) < 6 {
t.Fatal("need at least 6 data packets, got", len(packets))
}
t.Logf("sent %d data packets", len(packets))
// Server receives only the first packet so it ACKs it, advancing client's lastACK.
err = server.Recv(packets[0])
if err != nil {
t.Fatal("server recv first packet:", err)
}
clear(rawbuf[:])
n, err := server.Send(rawbuf[:])
if err != nil {
t.Fatal("server send ACK:", err)
}
if n == 0 {
t.Fatal("expected server to send ACK")
}
// Client receives the ACK for the first packet, establishing lastACK.
err = client.Recv(rawbuf[:n])
if err != nil {
t.Fatal("client recv ACK:", err)
}
dupACKValue := client.lastACK
t.Logf("lastACK established at %d, client.dupACKs=%d", dupACKValue, client.dupACKs)
// Craft 9 identical dup ACKs: same ACK value, no data, no SYN/FIN.
// These simulate what the server would send on receiving out-of-order packets.
const numDupAcks = 9
var dupAcks [numDupAcks][]byte
for i := range dupAcks {
var buf [mtu]byte
frm, ferr := NewFrame(buf[:])
if ferr != nil {
t.Fatal("new frame:", ferr)
}
frm.SetSourcePort(server.LocalPort())
frm.SetDestinationPort(client.LocalPort())
frm.SetSegment(Segment{
SEQ: server.scb.snd.NXT,
ACK: dupACKValue,
Flags: FlagACK,
WND: 65535,
}, 5)
dupAcks[i] = append([]byte(nil), buf[:sizeHeaderTCP]...)
}
t.Logf("crafted %d dup ACKs", numDupAcks)
// Feed all dup ACKs to client, counting how many times fast retransmit fires.
// Between dup ACKs, call Send() to transmit retransmitted packets (as a real
// stack would do). This makes BufferedSent > 0 again, which is required for
// the dup ACK condition to be met.
retransmitCount := 0
prevNRetx := client.nRetx
for i, ack := range dupAcks[:] {
rerr := client.Recv(ack)
if rerr != nil {
continue
}
if client.nRetx > prevNRetx {
retransmitCount++
t.Logf("fast retransmit #%d triggered at dup ACK %d (nRetx=%d)", retransmitCount, i+1, client.nRetx)
prevNRetx = client.nRetx
}
// Simulate real behavior: Send() is called between receives,
// which re-sends retransmitted data and makes BufferedSent > 0.
clear(rawbuf[:])
client.Send(rawbuf[:])
}
if retransmitCount == 0 {
t.Fatal("fast retransmit never triggered (expected exactly 1)")
}
if retransmitCount > 1 {
t.Fatalf("BUG (issue #58): fast retransmit triggered %d times for a single loss event, want 1.\n"+
"triggerRetransmit() resets dupACKs to 0, causing every 3rd dup ACK to\n"+
"re-trigger fast retransmit for the same lost sequence.", retransmitCount)
}
}
// TestFastRetransmitResetsOnNewACK verifies that after recovering from a loss
// event (new data ACKed), the dup-ACK counter resets so that a subsequent loss
// can trigger fast retransmit again.
//
// Without the dupACKs=0 reset on new ACK (handler.go line 219), the counter
// would stay above 3 after the first loss event and never reach ==3 again,
// disabling fast retransmit for all subsequent losses.
func TestFastRetransmitResetsOnNewACK(t *testing.T) {
const mtu = 60 // Small MTU: 20 byte header + 40 bytes payload per packet.
const txBuf = 2048
const maxpackets = 10
rng := rand.New(rand.NewSource(59))
client := new(Handler)
server := new(Handler)
err := client.SetBuffers(make([]byte, txBuf), make([]byte, txBuf), maxpackets)
if err != nil {
t.Fatal(err)
}
client.rto = rtoInitial
err = server.SetBuffers(make([]byte, txBuf), make([]byte, txBuf), maxpackets)
if err != nil {
t.Fatal(err)
}
server.rto = rtoInitial
err = server.OpenListen(uint16(rng.Uint32()), 0)
if err != nil {
t.Fatal(err)
}
err = client.OpenActive(uint16(rng.Uint32()), server.LocalPort(), 0)
if err != nil {
t.Fatal(err)
}
var rawbuf [mtu]byte
establish(t, client, server, rawbuf[:])
// Helper: craft a dup ACK packet from server to client for the given ACK value.
craftDupACK := func(ackVal Value) []byte {
var buf [mtu]byte
frm, ferr := NewFrame(buf[:])
if ferr != nil {
t.Fatal("new frame:", ferr)
}
frm.SetSourcePort(server.LocalPort())
frm.SetDestinationPort(client.LocalPort())
frm.SetSegment(Segment{
SEQ: server.scb.snd.NXT,
ACK: ackVal,
Flags: FlagACK,
WND: 65535,
}, 5)
return append([]byte(nil), buf[:sizeHeaderTCP]...)
}
// Helper: write data, send packets, return them.
sendPackets := func(payload []byte) [][]byte {
written := 0
var pkts [][]byte
for written < len(payload) {
n, werr := client.Write(payload[written:])
if werr != nil {
t.Fatal("client write:", werr)
}
written += n
for {
clear(rawbuf[:])
ns, serr := client.Send(rawbuf[:])
if serr != nil {
t.Fatal("client send:", serr)
}
if ns == 0 {
break
}
pkts = append(pkts, append([]byte(nil), rawbuf[:ns]...))
}
}
return pkts
}
// Helper: deliver packet to server, get ACK, deliver ACK to client.
deliverAndACK := func(pkt []byte) {
rerr := server.Recv(pkt)
if rerr != nil {
t.Fatal("server recv:", rerr)
}
clear(rawbuf[:])
n, serr := server.Send(rawbuf[:])
if serr != nil {
t.Fatal("server send:", serr)
}
if n == 0 {
t.Fatal("expected server to send ACK")
}
rerr = client.Recv(rawbuf[:n])
if rerr != nil {
t.Fatal("client recv ACK:", rerr)
}
}
// === Loss event #1 ===
packets1 := sendPackets(make([]byte, 40*4))
if len(packets1) < 4 {
t.Fatal("need at least 4 packets for loss event #1, got", len(packets1))
}
// Deliver the first packet to establish lastACK.
deliverAndACK(packets1[0])
ackVal1 := client.lastACK
// Send 3 dup ACKs to trigger fast retransmit.
for i := 0; i < 3; i++ {
err = client.Recv(craftDupACK(ackVal1))
if err != nil {
t.Fatalf("loss #1: dup ACK %d recv: %v", i+1, err)
}
clear(rawbuf[:])
client.Send(rawbuf[:]) // Keep BufferedSent > 0.
}
if client.nRetx != 1 {
t.Fatalf("loss #1: expected nRetx=1 after 3 dup ACKs, got %d", client.nRetx)
}
t.Logf("loss #1: fast retransmit triggered (nRetx=%d)", client.nRetx)
// === Recovery: retransmit the lost packet, server ACKs all data ===
// Re-send all the packets that server missed (client retransmits from UNA).
for {
clear(rawbuf[:])
n, serr := client.Send(rawbuf[:])
if serr != nil {
t.Fatal("client retransmit send:", serr)
}
if n == 0 {
break
}
_ = server.Recv(rawbuf[:n]) // Deliver retransmitted + new data.
}
// Deliver remaining original packets too.
for i := 1; i < len(packets1); i++ {
_ = server.Recv(packets1[i])
}
// Server sends cumulative ACK for all received data.
clear(rawbuf[:])
n, serr := server.Send(rawbuf[:])
if serr != nil {
t.Fatal("server send recovery ACK:", serr)
}
if n == 0 {
t.Fatal("expected server to send recovery ACK")
}
err = client.Recv(rawbuf[:n])
if err != nil {
t.Fatal("client recv recovery ACK:", err)
}
t.Logf("recovery: nRetx=%d, lastACK=%d", client.nRetx, client.lastACK)
// === Loss event #2 ===
packets2 := sendPackets(make([]byte, 40*4))
if len(packets2) < 4 {
t.Fatal("need at least 4 packets for loss event #2, got", len(packets2))
}
// Deliver the first packet to advance lastACK.
deliverAndACK(packets2[0])
ackVal2 := client.lastACK
if ackVal2 == ackVal1 {
t.Fatal("lastACK did not advance between loss events")
}
// Send 3 dup ACKs — fast retransmit should trigger again.
prevNRetx := client.nRetx
for i := 0; i < 3; i++ {
err = client.Recv(craftDupACK(ackVal2))
if err != nil {
t.Fatalf("loss #2: dup ACK %d recv: %v", i+1, err)
}
clear(rawbuf[:])
client.Send(rawbuf[:]) // Keep BufferedSent > 0.
}
if client.nRetx <= prevNRetx {
t.Fatalf("BUG: fast retransmit did NOT trigger for loss event #2.\n"+
"nRetx=%d (was %d).\n"+
"The dup-ACK counter was not reset after recovery from loss event #1,\n"+
"so it never reached the threshold again.", client.nRetx, prevNRetx)
}
t.Logf("loss #2: fast retransmit triggered (nRetx=%d)", client.nRetx)
}
+190 -1
View File
@@ -2,6 +2,7 @@ package tcp
import (
"bytes"
"fmt"
"math/rand"
"testing"
)
@@ -53,7 +54,6 @@ func newHandler(t *testing.T, mtu, mintaxpackets int) *Handler {
if err != nil {
t.Fatal(err)
}
h.rto = rtoInitial // Fake time: now=0 and sentAt=0, so RTO never fires in tests.
return h
}
@@ -963,3 +963,192 @@ func TestChallengeACKWithBufferedData(t *testing.T) {
t.Fatal("expected data packet, got header-only")
}
}
func TestHandler_RetransmitAfter3DupACKs(t *testing.T) {
const (
mtu = 1500
maxpackets = 3
)
rng := rand.New(rand.NewSource(42))
client := newHandler(t, mtu, maxpackets)
server := newHandler(t, mtu, maxpackets)
setupClientServer(t, rng, client, server)
var pkt [mtu]byte
establish(t, client, server, pkt[:])
// Client sends some data in flight.
payload := []byte("0123456789")
written, err := client.Write(payload)
if err != nil || written != len(payload) {
t.Fatalf("client.Write failed: %v len=%d", err, written)
}
n, err := client.Send(pkt[:])
if err != nil {
t.Fatalf("client.Send initial data: %v", err)
}
if n <= sizeHeaderTCP {
t.Fatalf("expected non-empty data packet; got %d", n)
}
// Server does NOT receive the intended packet, but rather the retransmission later on.
// no server.Recv(pkt[:n]) -> Packet loss.
// Simulate 3 duplicate ACKs (ACK == UNA, no progress).
dup := server.scb.MakeDupACK()
if !client.scb.IncomingIsDupACK(dup.ACK) {
t.Fatal("MakeRetransmitDupACK return should be considered a duplicate ACK by remote")
}
for i := 0; i < 3; i++ {
fb, _ := NewFrame(pkt[:])
fb.SetSourcePort(server.LocalPort())
fb.SetDestinationPort(client.LocalPort())
fb.SetSegment(dup, 5)
if err := client.Recv(pkt[:sizeHeaderTCP]); err != nil {
t.Fatalf("client.Recv dupACK #%d failed: %v", i+1, err)
}
}
if client.scb.dupack != 3 {
t.Fatalf("expected dupack=3; got %d", client.scb.dupack)
}
if !client.scb.HasPendingRetransmit() {
t.Fatal("expected HasPendingRetransmit() true after 3 dupACKs")
}
oldUNA := client.scb.snd.UNA
n, err = client.Send(pkt[:])
if err != nil {
t.Fatalf("client.Send retransmit failed: %v", err)
}
if n <= sizeHeaderTCP {
t.Fatalf("expected retransmit segment (>=20 bytes); got %d", n)
} else if client.scb.HasPendingRetransmit() {
t.Fatal("expected client to satisfy pending retransmit after single Send call")
}
retransmitFrame, _ := NewFrame(pkt[:n])
rtSeg := retransmitFrame.Segment(0)
if rtSeg.SEQ != oldUNA {
t.Fatalf("retransmit SEQ = %d; expected UNA=%d", rtSeg.SEQ, oldUNA)
}
if !rtSeg.Flags.HasAny(FlagACK) {
t.Fatalf("retransmit missing ACK flag: %#v", rtSeg.Flags)
}
if client.scb.nRetransmit != 1 {
t.Fatalf("expected scb.nRetransmit = 1; got %d", client.scb.nRetransmit)
}
// Ensure remote side can receive the retransmit frame.
if err := server.Recv(pkt[:n]); err != nil {
t.Fatalf("server.Recv retransmit packet failed: %v", err)
}
}
func TestHandler_RetransmitAfterMultipleLossesBothDirections(t *testing.T) {
const (
mtu = 1500
maxpackets = 3
loops = 3
)
rng := rand.New(rand.NewSource(1))
client := newHandler(t, mtu, maxpackets)
server := newHandler(t, mtu, maxpackets)
setupClientServer(t, rng, client, server)
var pkt [mtu]byte
establish(t, client, server, pkt[:])
sendWithLoss := func(sender, receiver *Handler, pay []byte) {
n, err := sender.Write(pay)
if err != nil || n != len(pay) {
t.Fatalf("write failed: %v len=%d", err, n)
}
n, err = sender.Send(pkt[:])
if err != nil {
t.Fatalf("Send initial data: %v", err)
} else if n <= sizeHeaderTCP {
t.Fatalf("expected non-empty data packet; got %d", n)
} else if sender.BufferedUnsent() > 0 {
t.Fatal("buffer too small to send all data")
}
// Drop packet (simulate loss): NO receiver.Recv(pkt[:n]).
// Three dupACKs from receiver side (its rcv state has not advanced).
dup := receiver.scb.MakeDupACK()
if !sender.scb.IncomingIsDupACK(dup.ACK) {
t.Fatal("dup ACK not recognized as dupack by sender")
}
for i := 0; i < 3; i++ {
clear(pkt[:])
fb, _ := NewFrame(pkt[:])
fb.SetSourcePort(receiver.LocalPort())
fb.SetDestinationPort(sender.LocalPort())
fb.SetSegment(dup, 5)
if !sender.scb.IncomingIsDupACK(dup.ACK) {
t.Fatal("expected incoming segment to be dupack")
}
if err := sender.Recv(pkt[:sizeHeaderTCP]); err != nil {
t.Fatalf("sender.Recv dupACK #%d failed: %v", i+1, err)
}
}
t.Log("dupack", sender.scb.dupack)
if sender.scb.dupack != 3 {
t.Fatalf("expected dupack=3; got=%d", sender.scb.dupack)
} else if !sender.scb.HasPendingRetransmit() {
t.Fatal("expected pending retransmit after 3 dupacks")
}
// Now expect retransmission packet.
oldUNA := sender.scb.snd.UNA
clear(pkt[:])
n, err = sender.Send(pkt[:])
if err != nil {
t.Fatalf("sender.Send retransmit failed: %v", err)
} else if n <= sizeHeaderTCP {
t.Fatalf("expected retransmit packet; got %d", n)
} else if sender.scb.HasPendingRetransmit() {
t.Error("after one retransmit should be satisfied")
}
retrFrm, _ := NewFrame(pkt[:n])
seg := retrFrm.Segment(0)
if seg.SEQ != oldUNA {
t.Fatalf("retransmit SEQ=%d; want=%d", seg.SEQ, oldUNA)
}
// Receiver consumes retransmit
if err := receiver.Recv(pkt[:n]); err != nil {
t.Fatalf("receiver.Recv retransmit failed: %v", err)
}
if receiver.scb.dupack > 0 {
t.Fatal("receiver has dupack", receiver.scb.dupack)
}
// Receiver ACKs, so sender progresses and dupack should reset.
clear(pkt[:])
n, err = receiver.Send(pkt[:])
if err != nil {
t.Fatalf("receiver.Send ACK after retransmit: %v", err)
}
if n > 0 {
if err := sender.Recv(pkt[:n]); err != nil {
t.Fatalf("sender.Recv ACK after retransmit: %v", err)
}
}
if sender.scb.dupack != 0 {
t.Fatalf("expected sender.dupack reset, got %d", sender.scb.dupack)
}
}
// Do several losses in client->server direction
for i := 0; i < loops; i++ {
payload := []byte(fmt.Sprintf("C->S loss %d", i))
sendWithLoss(client, server, payload)
sendWithLoss(client, server, payload)
sendWithLoss(server, client, payload)
sendWithLoss(client, server, payload)
sendWithLoss(server, client, payload)
sendWithLoss(server, client, payload)
}
}
+98
View File
@@ -0,0 +1,98 @@
package tcp
import (
"testing"
"github.com/soypat/lneto/internal"
)
func FuzzTCPControlBlock(f *testing.F) {
const (
mutopFlags = 1 << iota
mutopSeq
mutopAck
mutopMaxBit
)
const mutopBits = mutopMaxBit - 1
const (
mutPacketBits = 4
mutFlags = 1 << iota
)
const wnd = 1500
var seed uint64
editPkt := func(rngSeed, ops uint64, seg *Segment) {
ops &= mutopBits
if ops&mutopFlags != 0 {
seg.Flags ^= Flags(seed & uint64(FlagACK|FlagFIN|FlagRST|FlagSYN))
}
if ops&mutopSeq != 0 {
seg.SEQ = Value(int32(seg.SEQ) + int32(int8(uint8(rngSeed>>32))))
}
if ops&mutopAck != 0 {
seg.ACK = Value(int32(seg.ACK) + int32(int8(uint8(rngSeed>>48))))
}
}
f.Add(seed, uint64(mutopFlags))
f.Fuzz(func(t *testing.T, seed, op uint64) {
var tcb0, tcb1 ControlBlock
iss0 := Value(seed)
iss1 := Value(seed >> 32)
err := tcb0.Open(iss0, wnd)
if err != nil {
t.Fatal(err)
}
synseg := ClientSynSegment(iss1, wnd)
err = tcb1.Send(synseg)
if err != nil {
t.Fatal(err)
}
err = tcb0.Recv(synseg)
if err != nil {
t.Fatal(err)
}
sent := 0
const maxpkts = 30
pktEdits := internal.Prand64(seed)
nextOp := internal.Prand64(op)
for range maxpkts {
nextOp = internal.Prand64(nextOp)
seg0, ok := tcb0.PendingSegment(10)
if ok {
edit := pktEdits&1 != 0
pktEdits >>= 1
if edit {
editPkt(seed, nextOp, &seg0)
}
err = tcb0.Send(seg0)
if err == nil {
sent++
err = tcb1.Recv(seg0)
if err != nil {
t.Fatal("packet sent from TCB0 to TCB1 failed:\n", StringExchange(seg0, tcb0.State(), tcb1.State(), false))
}
}
}
nextOp = internal.Prand64(nextOp)
seg1, ok := tcb1.PendingSegment(10)
if ok {
edit := pktEdits&1 != 0
pktEdits >>= 1
if edit {
editPkt(seed, nextOp, &seg1)
}
err = tcb1.Send(seg1)
if err == nil {
sent++
err = tcb0.Recv(seg1)
if err != nil {
t.Fatal("packet sent from TCB1 to TCB0 failed:\n", StringExchange(seg1, tcb0.State(), tcb1.State(), true))
}
}
}
}
if sent == 0 {
t.Fatal("no packets sent")
}
})
}
+20 -41
View File
@@ -45,9 +45,6 @@ type ringidx struct {
seq Value
// size is the size of the packet in bytes.
size Size
// sentAt is the time in milliseconds when this packet was first sent.
// Used for RTO detection per RFC 6298 §5.
sentAt uint32
}
// Reset resets the RingTx's internal state to use buf as the main ring buffer and creates or reuses
@@ -118,20 +115,31 @@ func (rtx *ringTx) Write(b []byte) (n int, err error) {
}
// MakePacket reads from the unsent data ring buffer and generates a new packet segment.
// It fails if the sent packet queue is full. sentAt is the current time in milliseconds,
// stamped on the packet for RTO detection per RFC 6298 §5.1.
func (rtx *ringTx) MakePacket(b []byte, currentSeq Value, sentAt uint32) (int, error) {
// It fails if the sent packet queue is full.
func (rtx *ringTx) MakePacket(b []byte, currentSeq Value) (int, error) {
free := rtx.slist.Free()
if free == 0 {
return 0, lneto.ErrBufferFull
}
endSeq, ok := rtx.sentEndSeq()
if ok && currentSeq.LessThan(endSeq) {
// maybe retransmit. Look for exact match.
for i := range rtx.slist.pkts {
pkt := &rtx.slist.pkts[i]
if pkt.seq == currentSeq {
// This packet to be retransmit.
data := rtx.ring(pkt.off, pkt.end)
return data.Read(b)
}
}
internal.LogAttrs(nil, slog.LevelError, "txqueue:seq<endseq", slog.Uint64("seq", uint64(currentSeq)), slog.Uint64("endseq", uint64(endSeq)))
return 0, lneto.ErrBug
}
// Reading unsent ring consumes unsent and converts it to "sent".
unsent, _ := rtx.unsentRing()
if unsent.IsEmpty() {
return 0, nil // No data to send.
}
oldUnsentOff := unsent.Off
n, err := unsent.Read(b)
if err != nil {
@@ -141,7 +149,7 @@ func (rtx *ringTx) MakePacket(b []byte, currentSeq Value, sentAt uint32) (int, e
// Start of buffer will be SENT, end of buffer will be UNSENT(or empty).
// Packet generated has offset at old unsentOff.
size := rtx.Size()
pkt := rtx.slist.AddPacket(n, oldUnsentOff, size, currentSeq, sentAt)
pkt := rtx.slist.AddPacket(n, oldUnsentOff, size, currentSeq)
if pkt.off != oldUnsentOff || pkt.end != addEnd(pkt.off, n, size) {
panic("invalid generated packet")
}
@@ -233,34 +241,6 @@ func (rtx *ringTx) RetransmitFromUNA() {
rtx.slist.Reset(cap(rtx.slist.pkts), unaSeq)
}
// RecoveryACK processes a cumulative ACK that covers data sent before a
// retransmit rewind. After RetransmitFromUNA merged sent→unsent and cleared
// the sentlist, a recovery ACK may exceed what's currently in the sentlist.
// This method acks any sentlist entries, then skips unsent bytes that were
// implicitly acknowledged (they were received by the remote before the rewind).
func (rtx *ringTx) RecoveryACK(ack Value) {
size := rtx.Size()
// First, ack everything in the sentlist (if any packets were re-sent).
if newest := rtx.slist.Newest(); newest != nil {
rtx.slist.RecvAck(newest.endSeq(), size)
}
rtx.sentoff = 0
rtx.sentend = 0
// Skip unsent data that was implicitly acked. The sequence of the first
// unsent byte is slist.ssn (the end-seq of the last acked packet).
excess := int32(ack - rtx.slist.ssn)
if excess > 0 && rtx.unsentend != 0 {
rtx.unsentoff = addOff(rtx.unsentoff, int(excess), size)
if rtx.unsentoff == rtx.unsentend {
rtx.unsentoff = 0
rtx.unsentend = 0
}
}
rtx.slist.Reset(cap(rtx.slist.pkts), ack)
rtx.consolidateBufs()
}
func (rtx *ringTx) consolidateBufs() {
unsentEmpty := rtx.unsentend == 0
sentEmpty := rtx.sentend == 0
@@ -351,7 +331,7 @@ func (sl *sentlist) Free() int {
return cap(sl.pkts) - len(sl.pkts)
}
func (sl *sentlist) AddPacket(datalen, off, bufsize int, seq Value, sentAt uint32) *ringidx {
func (sl *sentlist) AddPacket(datalen, off, bufsize int, seq Value) *ringidx {
free := sl.Free()
if free == 0 {
panic("pkt buffer full")
@@ -361,11 +341,10 @@ func (sl *sentlist) AddPacket(datalen, off, bufsize int, seq Value, sentAt uint3
panic("new sent packet offset must match last sent packet end")
}
sl.pkts = append(sl.pkts, ringidx{
off: off,
end: addEnd(off, datalen, bufsize),
seq: seq,
size: Size(datalen),
sentAt: sentAt,
off: off,
end: addEnd(off, datalen, bufsize),
seq: seq,
size: Size(datalen),
})
return &sl.pkts[len(sl.pkts)-1]
}
+11 -11
View File
@@ -84,7 +84,7 @@ func TestRingTx_op(t *testing.T) {
clear(opWriteData)
case opSend:
// oplen=num bytes to send in this operation.
nsgot, err := rtx.MakePacket(auxbuf[:oplen], currentSeq, 0)
nsgot, err := rtx.MakePacket(auxbuf[:oplen], currentSeq)
megafail := nsgot > nunsent
if err != nil && oplen <= nunsent && availPkt > 0 {
t.Fatal(itest, iop, err)
@@ -138,17 +138,17 @@ func TestSentlist_multi(t *testing.T) {
sl.Reset(3, 0)
// Test multi packet x2.
p1 := sl.AddPacket(5, 0, bufsize, 0, 0)
p2 := sl.AddPacket(5, p1.end, bufsize, p1.endSeq(), 0)
p1 := sl.AddPacket(5, 0, bufsize, 0)
p2 := sl.AddPacket(5, p1.end, bufsize, p1.endSeq())
sl.RecvAck(Value(p2.size+p1.size), bufsize)
if sl.Oldest() != nil {
t.Fatal("expected full ack")
}
// multi packet x3.
sl.Reset(3, 0)
p1 = sl.AddPacket(3, 0, bufsize, 0, 0)
p2 = sl.AddPacket(3, p1.end, bufsize, p1.endSeq(), 0)
p3 := sl.AddPacket(4, p2.end, bufsize, p2.endSeq(), 0)
p1 = sl.AddPacket(3, 0, bufsize, 0)
p2 = sl.AddPacket(3, p1.end, bufsize, p1.endSeq())
p3 := sl.AddPacket(4, p2.end, bufsize, p2.endSeq())
sl.RecvAck(2, bufsize)
oldest := sl.Oldest()
if oldest != p1 {
@@ -167,7 +167,7 @@ func TestSentlist_simple(t *testing.T) {
// Test full ack.
const bufsize = 16
const pkt = 10
sl.AddPacket(pkt, 0, bufsize, 0, 0)
sl.AddPacket(pkt, 0, bufsize, 0)
if sl.Oldest() == nil || sl.Newest() != sl.Oldest() {
t.Error("expected same oldest/newest non-nil packet")
}
@@ -179,7 +179,7 @@ func TestSentlist_simple(t *testing.T) {
}
// Test partial ack.
sl.AddPacket(pkt, 0, bufsize, sl.ssn, 0)
sl.AddPacket(pkt, 0, bufsize, sl.ssn)
for i := Value(0); i < pkt-1; i++ {
ack++
sl.RecvAck(ack, bufsize)
@@ -239,7 +239,7 @@ func TestTxQueue_multipacket(t *testing.T) {
pktlen := rng.Intn(maxToPacket) + 1
pkt := rbuf[roff : roff+pktlen]
expectPkt := wbuf[roff : roff+pktlen]
ngot, err := rtx.MakePacket(pkt, seq, 0)
ngot, err := rtx.MakePacket(pkt, seq)
testQueueSanity(t, &rtx)
roff += ngot
seq += Value(ngot)
@@ -374,7 +374,7 @@ func TestTxQueue(t *testing.T) {
datalens = datalens[:0]
for rtx.BufferedUnsent() != 0 {
nbytes := rng.Intn(maxPacketSize-minBufferSize) + minBufferSize
n, err := rtx.MakePacket(readBuf[:nbytes], currentSeq, 0)
n, err := rtx.MakePacket(readBuf[:nbytes], currentSeq)
if err != nil {
t.Fatal(err)
} else if n == 0 {
@@ -572,7 +572,7 @@ func operateOnRing(t *testing.T, rtx *ringTx, write, readPacket, aux []byte, new
if wantRead != len(wantBufRead) {
t.Fatalf("miscalculated expect read %d != %d", wantRead, len(wantBufRead))
}
n, err := rtx.MakePacket(readPacket, newPacketSeq, 0)
n, err := rtx.MakePacket(readPacket, newPacketSeq)
if err != nil && wantRead != 0 {
t.Errorf("error reading: %s", err)
} else if n != wantRead {
-1
View File
@@ -80,7 +80,6 @@ func NewTCPPool(cfg TCPPoolConfig) (*TCPPool, error) {
TxBuf: bufSpace[txOff : txOff+cfg.TxBufSize],
TxPacketQueueSize: cfg.QueueSize,
Logger: cfg.ConnLogger,
NanoTime: cfg.NanoTime,
})
if err != nil {
return nil, err