Files
lneto/tcp/seqsbug_test.go
T
Pat Whittingslow d5c4fa53c6 tcp: bugfix for seq not in snd/rcv.wnd error (#49)
* tcp: bugfix for seq not in snd/rcv.wnd error

* accept dupe ACKs for window updates and prevent decrement of UNA

* add fixes for incorrect tcp functioning

* remove Conn.Available* methods in favor of Conn.Free* methods due to ambiguous name
2026-03-04 12:59:30 -03:00

550 lines
18 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package tcp
// Tests for the "seqs: bad calculation" panic bug caused by a retransmitted SYN
// being accepted on an ESTABLISHED connection.
//
// Bug 1: validateIncomingSegment skips SEQ checks for any SYN, even in synchronized states.
// Bug 2: rcvEstablished silently accepts bare SYN segments (no data, no FIN).
// Combined: SYN overwrites snd.WND with a small value while data is in flight, causing
// uint32 underflow in maxSend() → panic in PendingSegment.
import "testing"
// TestSYNOnEstablished_PanicRegression replicates the exact crash sequence from the bug report:
// a retransmitted SYN arrives on an ESTABLISHED connection with 1200 bytes in flight,
// clobbers snd.WND to 1025, and triggers "seqs: bad calculation" in PendingSegment.
// To trigger run with GOARCH=386
func TestSYNOnEstablished_PanicRegression(t *testing.T) {
// Values from the bug report timeline.
const (
serverISS Value = 155000
clientISS Value = 4189094524 // The SYN's sequence number from the log.
serverWND Size = 1024
clientWND Size = 1025 // The SYN's small window.
payloadSent Size = 1200 // HTTP response bytes in flight.
)
// Recv must reject the SYN on an ESTABLISHED connection.
t.Run("Recv_rejects_SYN", func(t *testing.T) {
var tcb ControlBlock
tcb.HelperInitState(StateEstablished, serverISS, serverISS+1+Value(payloadSent), serverWND)
tcb.HelperInitRcv(clientISS, clientISS+1, 65535)
tcb.snd.UNA = serverISS + 1
syn := Segment{
SEQ: clientISS,
ACK: 0,
Flags: FlagSYN,
WND: clientWND,
}
err := tcb.Recv(syn)
if err == nil {
t.Fatal("Recv accepted SYN on ESTABLISHED connection; expected error")
}
if tcb.State() != StateEstablished {
t.Fatalf("state changed to %s; want ESTABLISHED", tcb.State())
}
if tcb.snd.WND == clientWND {
t.Fatalf("snd.WND was clobbered to %d by the SYN segment", clientWND)
}
})
// Directly simulate the corrupted state to trigger the panic in PendingSegment.
// This is the actual crash path: inFlight=1200, snd.WND=1025 → maxSend() underflows.
// On 32-bit targets int(underflowedUint32) wraps negative → panic.
t.Run("PendingSegment_panic", func(t *testing.T) {
var tcb ControlBlock
tcb.HelperInitState(StateEstablished, serverISS, serverISS+1+Value(payloadSent), serverWND)
tcb.HelperInitRcv(clientISS, clientISS+1, 65535)
tcb.snd.UNA = serverISS + 1
// Simulate what Recv does when the SYN is accepted (the bug):
// snd.WND gets overwritten with the SYN's small window.
tcb.snd.WND = clientWND // inFlight=1200 > snd.WND=1025 → underflow.
defer func() {
if r := recover(); r != nil {
t.Errorf("confirmed panic: %v", r)
}
}()
tcb.PendingSegment(1) // Any payload > 0 triggers the panic.
})
}
// TestSYNOnEstablished_SEQValidation verifies that SYN segments are subject to
// sequence number validation in synchronized states (Bug 1 from the report).
func TestSYNOnEstablished_SEQValidation(t *testing.T) {
const (
iss Value = 100
remoteISS Value = 5000
window Size = 1000
)
var tcb ControlBlock
tcb.HelperInitState(StateEstablished, iss, iss, window)
tcb.HelperInitRcv(remoteISS, remoteISS, window)
// SYN with SEQ outside the receive window should be rejected.
outOfWindowSYN := Segment{
SEQ: remoteISS - 2000, // Way before rcv.NXT.
Flags: FlagSYN,
WND: 1025,
}
err := tcb.Recv(outOfWindowSYN)
if err == nil {
t.Fatal("SYN with out-of-window SEQ was accepted in ESTABLISHED state")
}
// SYN with SEQ inside the window but not equal to rcv.NXT should also be rejected
// (we require sequential segments per SHLD-31).
inWindowSYN := Segment{
SEQ: remoteISS + 1, // In window but not NXT.
Flags: FlagSYN,
WND: 1025,
}
err = tcb.Recv(inWindowSYN)
if err == nil {
t.Fatal("SYN with in-window non-NXT SEQ was accepted in ESTABLISHED state")
}
}
// TestSYNOnEstablished_ChallengeACK verifies that per RFC 9293 §3.10.7.4,
// receiving a SYN on an ESTABLISHED connection results in a challenge ACK
// rather than silently accepting or resetting (Bug 2 from the report).
func TestSYNOnEstablished_ChallengeACK(t *testing.T) {
const (
issA Value = 100
issB Value = 300
window Size = 1000
)
var tcb ControlBlock
tcb.HelperInitState(StateEstablished, issA, issA, window)
tcb.HelperInitRcv(issB, issB, window)
// SYN at exact rcv.NXT (in-window).
syn := Segment{
SEQ: issB,
Flags: FlagSYN,
WND: 512,
}
err := tcb.Recv(syn)
// Must be rejected (either via validation or rcvEstablished).
if err == nil {
t.Fatal("SYN at rcv.NXT was accepted in ESTABLISHED state; expected rejection")
}
// Connection must stay ESTABLISHED.
if tcb.State() != StateEstablished {
t.Fatalf("state = %s; want ESTABLISHED", tcb.State())
}
// snd.WND must be preserved.
if tcb.snd.WND == 512 {
t.Fatal("snd.WND was overwritten by the SYN's window value")
}
// A challenge ACK should be pending.
seg, ok := tcb.PendingSegment(0)
if !ok {
t.Fatal("no pending segment after SYN on ESTABLISHED; expected challenge ACK")
}
if seg.Flags != FlagACK {
t.Errorf("pending flags = %s; want ACK (challenge ACK)", seg.Flags)
}
if seg.SEQ != issA {
t.Errorf("challenge ACK SEQ = %d; want snd.NXT=%d", seg.SEQ, issA)
}
if seg.ACK != issB {
t.Errorf("challenge ACK ACK = %d; want rcv.NXT=%d", seg.ACK, issB)
}
}
// TestSYNOnClosingStates verifies SYN rejection in other synchronized states
// beyond ESTABLISHED (FIN-WAIT-1, FIN-WAIT-2, CLOSE-WAIT).
func TestSYNOnClosingStates(t *testing.T) {
states := []State{StateFinWait1, StateFinWait2, StateCloseWait}
for _, state := range states {
t.Run(state.String(), func(t *testing.T) {
const iss, remoteISS, window Size = 100, 300, 1000
var tcb ControlBlock
tcb.HelperInitState(state, Value(iss), Value(iss), window)
tcb.HelperInitRcv(Value(remoteISS), Value(remoteISS), window)
syn := Segment{
SEQ: Value(remoteISS),
Flags: FlagSYN,
WND: 512,
}
err := tcb.Recv(syn)
if err == nil {
t.Fatalf("SYN accepted in %s state; expected rejection", state)
}
if tcb.State() != state {
t.Fatalf("state changed from %s to %s after SYN", state, tcb.State())
}
})
}
}
// TestWindowReject_ChallengeACK verifies that per RFC 9293 §3.4,
// segments failing the receive window acceptability test generate a challenge ACK
// rather than being silently dropped.
func TestWindowReject_ChallengeACK(t *testing.T) {
const (
issA Value = 1000
issB Value = 5000
window Size = 1024
)
// wantACK is the expected challenge ACK for all sub-tests.
wantACK := Segment{SEQ: issA, ACK: issB + 512, Flags: FlagACK, WND: window}
setup := func() ControlBlock {
var tcb ControlBlock
tcb.HelperInitState(StateEstablished, issA, issA, window)
tcb.HelperInitRcv(issB, issB+512, window) // rcv.NXT advanced by 512 (data already received)
return tcb
}
// errSeqNotInWindow: segment SEQ entirely before rcv.NXT (retransmission of acked data).
t.Run("SeqNotInWindow", func(t *testing.T) {
tcb := setup()
retransmit := Segment{
SEQ: issB, // Before rcv.NXT (issB+512).
ACK: issA,
Flags: FlagACK,
WND: 64240,
DATALEN: 512, // Covers [issB, issB+512) — all already received.
}
err := tcb.Recv(retransmit)
if err == nil {
t.Fatal("expected rejection for retransmitted segment")
}
seg, ok := tcb.PendingSegment(0)
if !ok {
t.Fatal("no pending segment; expected challenge ACK")
}
if seg != wantACK {
t.Errorf("challenge ACK:\n got=%+v\nwant=%+v", seg, wantACK)
}
})
// errLastNotInWindow: segment starts in window but end is beyond window.
t.Run("LastNotInWindow", func(t *testing.T) {
tcb := setup()
seg := Segment{
SEQ: issB + 512, // At rcv.NXT (in window).
ACK: issA,
Flags: FlagACK,
WND: 64240,
DATALEN: Size(window) + 100, // End extends beyond window.
}
err := tcb.Recv(seg)
if err == nil {
t.Fatal("expected rejection for segment extending past window")
}
got, ok := tcb.PendingSegment(0)
if !ok {
t.Fatal("no pending segment; expected challenge ACK")
}
if got != wantACK {
t.Errorf("challenge ACK:\n got=%+v\nwant=%+v", got, wantACK)
}
})
// errZeroWindow: data sent to zero-size receive window.
t.Run("ZeroWindow", func(t *testing.T) {
var tcb ControlBlock
tcb.HelperInitState(StateEstablished, issA, issA, 0) // zero receive window
tcb.HelperInitRcv(issB, issB+512, window)
wantZW := Segment{SEQ: issA, ACK: issB + 512, Flags: FlagACK, WND: 0}
seg := Segment{
SEQ: issB + 512, // At rcv.NXT.
ACK: issA,
Flags: FlagACK,
WND: 64240,
DATALEN: 100, // Data to zero window.
}
err := tcb.Recv(seg)
if err == nil {
t.Fatal("expected rejection for data on zero window")
}
got, ok := tcb.PendingSegment(0)
if !ok {
t.Fatal("no pending segment; expected challenge ACK")
}
if got != wantZW {
t.Errorf("challenge ACK:\n got=%+v\nwant=%+v", got, wantZW)
}
})
// errRequireSequential: segment in window but not at rcv.NXT (out-of-order).
t.Run("RequireSequential", func(t *testing.T) {
tcb := setup()
seg := Segment{
SEQ: issB + 512 + 100, // In window but not at rcv.NXT.
ACK: issA,
Flags: FlagACK,
WND: 64240,
DATALEN: 50,
}
err := tcb.Recv(seg)
if err == nil {
t.Fatal("expected rejection for out-of-order segment")
}
got, ok := tcb.PendingSegment(0)
if !ok {
t.Fatal("no pending segment; expected challenge ACK")
}
if got != wantACK {
t.Errorf("challenge ACK:\n got=%+v\nwant=%+v", got, wantACK)
}
})
// RST segments failing window check must NOT generate a challenge ACK.
t.Run("RST_NoACK", func(t *testing.T) {
tcb := setup()
rst := Segment{
SEQ: issB, // Before rcv.NXT, out of window.
Flags: FlagRST,
}
err := tcb.Recv(rst)
if err == nil {
t.Fatal("expected rejection for out-of-window RST")
}
_, ok := tcb.PendingSegment(0)
if ok {
t.Fatal("RST rejection should not generate a challenge ACK")
}
})
// Verify errWindowOverflow does NOT trigger challenge ACK.
t.Run("WindowOverflow_NoACK", func(t *testing.T) {
tcb := setup()
seg := Segment{
SEQ: issB + 512,
ACK: issA,
Flags: FlagACK,
WND: Size(1 << 17), // > MaxUint16.
}
err := tcb.Recv(seg)
if err == nil {
t.Fatal("expected rejection for window overflow")
}
_, ok := tcb.PendingSegment(0)
if ok {
t.Fatal("window overflow should not generate a challenge ACK")
}
})
}
// TestPendingSegment_ACKSuppressedWhenWindowFull replicates the root cause of the
// "reject in/out seg: seq not in snd/rcv.wnd" regression reported after eab43c4.
//
// When inFlight >= snd.WND (send window full) and the caller has buffered TX data
// (payloadLen > 0), PendingSegment suppresses ALL segments—including pending ACKs—
// because the maxPayload==0 guard only allows FIN/RST/SYN through.
//
// Before eab43c4, maxSend() underflowed to ~4 billion when inFlight > WND, so the
// guard on line 204 never triggered and ACKs always piggybacked on data segments.
// After the underflow fix, maxSend() correctly returns 0, but this exposes the
// latent bug: ACKs are starved when the send window is full and there's TX data.
//
// The deadlock chain in production:
// 1. Publisher keeps TX buffer non-empty (payloadLen > 0 always)
// 2. Send window fills → maxSend() returns 0
// 3. PendingSegment drops the pending ACK → remote never learns data was received
// 4. Remote retransmits with stale SEQ → "seq not in snd/rcv.wnd" rejection
// 5. (Before 3d0bc93: no challenge ACK → permanent deadlock → i/o timeout)
//
// The challenge ACK fix at 3d0bc93 masks this by allowing recovery, but the
// underlying ACK suppression still causes unnecessary retransmission delays.
func TestPendingSegment_ACKSuppressedWhenWindowFull(t *testing.T) {
const (
localISS Value = 1000
remoteISS Value = 5000
dataInFlight Size = 500
remoteWND Size = 500 // == dataInFlight, so inFlight >= WND → maxSend()=0
localWND Size = 1024
)
setup := func() ControlBlock {
var tcb ControlBlock
// snd.NXT = localISS + 1 + dataInFlight (SYN consumed 1 seq, then 500 bytes sent)
tcb.HelperInitState(StateEstablished, localISS, localISS+1+Value(dataInFlight), localWND)
// snd.WND = remoteWND (500), so inFlight(500) >= WND(500) → maxSend()=0
tcb.HelperInitRcv(remoteISS, remoteISS+1, remoteWND)
tcb.snd.UNA = localISS + 1 // SYN acked, 500 bytes unacked
return tcb
}
// Sanity check: maxSend() is actually 0 in our setup.
t.Run("precondition_maxSend_zero", func(t *testing.T) {
tcb := setup()
if ms := tcb.snd.maxSend(); ms != 0 {
t.Fatalf("maxSend()=%d; want 0 (test precondition broken)", ms)
}
})
// Control: PendingSegment(0) correctly returns the ACK when no payload requested.
t.Run("payloadLen_0_ACK_sent", func(t *testing.T) {
tcb := setup()
tcb.pending[0] |= FlagACK // Simulate pending ACK from receiving data.
seg, ok := tcb.PendingSegment(0)
if !ok {
t.Fatal("PendingSegment(0) returned !ok; pending ACK should be sent when no payload requested")
}
if !seg.Flags.HasAll(FlagACK) {
t.Errorf("segment flags = %s; want ACK", seg.Flags)
}
})
// THE BUG: PendingSegment(>0) suppresses the ACK when window is full.
// This is the exact scenario of a publisher with buffered TX data.
t.Run("payloadLen_gt0_ACK_must_not_be_suppressed", func(t *testing.T) {
tcb := setup()
tcb.pending[0] |= FlagACK // Simulate pending ACK from receiving data.
// Caller has 100 bytes buffered to send, but window is full.
// PendingSegment should return the ACK with DATALEN=0 (no data, window full)
// instead of returning false and dropping the ACK entirely.
seg, ok := tcb.PendingSegment(100)
if !ok {
t.Fatal("PendingSegment(100) returned !ok; pending ACK was suppressed because " +
"maxPayload==0 guard only allows FIN/RST/SYN, not ACK. " +
"This causes the remote to never learn data was received, " +
"leading to retransmissions and eventual 'seq not in snd/rcv.wnd' rejection")
}
if !seg.Flags.HasAll(FlagACK) {
t.Errorf("segment flags = %s; want ACK", seg.Flags)
}
if seg.DATALEN != 0 {
t.Errorf("segment DATALEN = %d; want 0 (window is full, no data should be sent)", seg.DATALEN)
}
})
// Verify FIN is still allowed through when window is full (existing behavior).
t.Run("payloadLen_gt0_FIN_allowed", func(t *testing.T) {
tcb := setup()
tcb.pending[0] |= FlagFIN | FlagACK
_, ok := tcb.PendingSegment(100)
if !ok {
t.Fatal("PendingSegment suppressed FIN+ACK when window full; FIN must always go through")
}
})
}
// TestSYNPreestablished_StillAllowed ensures the fix doesn't break normal SYN
// processing in pre-established states (LISTEN, SYN-SENT, SYN-RCVD).
func TestSYNPreestablished_StillAllowed(t *testing.T) {
const issA, issB, windowA, windowB = 100, 300, 1000, 1000
t.Run("LISTEN", func(t *testing.T) {
var tcb ControlBlock
tcb.HelperInitState(StateListen, issA, issA, windowA)
syn := Segment{SEQ: issB, Flags: FlagSYN, WND: windowB}
err := tcb.Recv(syn)
if err != nil {
t.Fatalf("SYN rejected in LISTEN state: %v", err)
}
if tcb.State() != StateSynRcvd {
t.Fatalf("state = %s; want SYN-RCVD", tcb.State())
}
})
t.Run("SYN-SENT", func(t *testing.T) {
var tcb ControlBlock
tcb.HelperInitState(StateSynSent, issA, issA, windowA)
// Simultaneous open: receive SYN from peer.
syn := Segment{SEQ: issB, Flags: FlagSYN, WND: windowB}
err := tcb.Recv(syn)
if err != nil {
t.Fatalf("SYN rejected in SYN-SENT state: %v", err)
}
if tcb.State() != StateSynRcvd {
t.Fatalf("state = %s; want SYN-RCVD", tcb.State())
}
})
}
func TestRecvAckUpdatesUnaCorrectly(t *testing.T) {
// Create a TCB in ESTABLISHED state with some data sent but not yet acknowledged.
tcb := &ControlBlock{
_state: StateEstablished,
snd: sendSpace{
UNA: 1000, // oldest unacknowledged sequence number
NXT: 2000, // next sequence number to send (1000 bytes outstanding)
ISS: 500, // initial send sequence number (not critical here)
WND: 65535, // large window to avoid window issues
},
rcv: recvSpace{
NXT: 3000, // any value, not used in these tests
WND: 65535,
},
// logger can be nil or a no-op for tests
}
// Helper to check that UNA stays at expected value after processing a segment.
checkUna := func(want Value, msg string) {
if got := tcb.snd.UNA; got != want {
t.Errorf("%s: UNA = %d, want %d", msg, got, want)
}
}
// 1. Send an old ACK (below current UNA) should be silently accepted but not advance UNA.
oldAckSeg := Segment{
Flags: FlagACK,
ACK: 500, // less than UNA=1000
WND: 65535,
SEQ: 3000, // any acceptable sequence (within rcv window)
}
err := tcb.Recv(oldAckSeg)
if err != nil {
t.Errorf("old ACK returned error: %v, want nil (silent accept)", err)
}
checkUna(1000, "after old ACK")
// 2. Send an ACK for unsent data (beyond NXT) should be rejected (error) and UNA unchanged.
futureAckSeg := Segment{
Flags: FlagACK,
ACK: 2500, // > NXT=2000
WND: 65535,
SEQ: 3000,
}
err = tcb.Recv(futureAckSeg)
if err == nil {
t.Error("ACK for unsent data returned nil, want error")
}
checkUna(1000, "after future ACK")
// 3. Send a valid ACK that acknowledges some, but not all, outstanding data.
validAckSeg := Segment{
Flags: FlagACK,
ACK: 1500, // between UNA and NXT
WND: 65535,
SEQ: 3000,
}
err = tcb.Recv(validAckSeg)
if err != nil {
t.Errorf("valid ACK returned error: %v, want nil", err)
}
checkUna(1500, "after valid ACK")
// 4. Send an ACK that acknowledges exactly all outstanding data (ACK == NXT).
allAckSeg := Segment{
Flags: FlagACK,
ACK: 2000, // == NXT
WND: 65535,
SEQ: 3000,
}
err = tcb.Recv(allAckSeg)
if err != nil {
t.Errorf("ACK == NXT returned error: %v, want nil", err)
}
checkUna(2000, "after ACK == NXT")
}