diff --git a/tcp/tcp_test.go b/tcp/tcp_test.go index 69b1692..c7757de 100644 --- a/tcp/tcp_test.go +++ b/tcp/tcp_test.go @@ -590,112 +590,108 @@ func TestIssue19(t *testing.T) { } } -// TestRcvFinWait2_BareACK reproduces the bug where rcvFinWait2 rejects any segment -// without both FIN and ACK flags. Per RFC 9293 §3.10.7.4, FIN-WAIT-2 should: -// - Silently accept bare ACKs (duplicate ACKs, window updates) -// - Accept incoming data (remote hasn't sent FIN yet) -// - Transition to TIME-WAIT only when FIN is received -// -// The overly strict check causes connections to get stuck in FIN-WAIT-2 until timeout, -// exhausting the connection pool when multiple clients are affected. -func TestRcvFinWait2_BareACK(t *testing.T) { - var tcb tcp.ControlBlock +// TestRcvFinWait2 tests rcvFinWait2 behavior per RFC 9293 §3.10.7.4. +// In FIN-WAIT-2, the local side has sent FIN and received its ACK. +// The remote side has NOT closed yet and may still send data or ACKs. +func TestRcvFinWait2(t *testing.T) { const windowA, windowB = 1000, 1000 const issA, issB = 100, 300 - tcb.HelperInitState(tcp.StateEstablished, issA, issA, windowA) - tcb.HelperInitRcv(issB, issB, windowB) - assertState := func(want tcp.State) { + + // enterFinWait2 returns a ControlBlock in FIN-WAIT-2 with no pending flags. + // Simulates: A sent FIN|ACK, B acknowledged it, A drained pending ACK. + enterFinWait2 := func(t *testing.T) tcp.ControlBlock { t.Helper() - if tcb.State() != want { - t.Fatalf("want state %s; got %s", want, tcb.State()) + var tcb tcp.ControlBlock + tcb.HelperInitState(tcp.StateEstablished, issA, issA, windowA) + tcb.HelperInitRcv(issB, issB, windowB) + if err := tcb.Close(); err != nil { + t.Fatal(err) } + seg, _ := tcb.PendingSegment(0) + if err := tcb.Send(seg); err != nil { + t.Fatal(err) + } + if err := tcb.Recv(tcp.Segment{SEQ: issB, ACK: issA + 1, Flags: tcp.FlagACK, WND: windowB}); err != nil { + t.Fatal(err) + } + if tcb.State() != tcp.StateFinWait2 { + t.Fatalf("setup: want FIN-WAIT-2; got %s", tcb.State()) + } + // Drain pending ACK left from rcvFinWait1 so subtests start clean. + if seg, ok := tcb.PendingSegment(0); ok { + if err := tcb.Send(seg); err != nil { + t.Fatal(err) + } + } + return tcb } - // A initiates close → sends FIN|ACK. - err := tcb.Close() - if err != nil { - t.Fatal(err) - } - seg, ok := tcb.PendingSegment(0) - if !ok || !seg.Flags.HasAll(FINACK) { - t.Fatalf("expected FIN|ACK pending; got %+v (ok=%v)", seg, ok) - } - err = tcb.Send(seg) - if err != nil { - t.Fatal(err) - } - assertState(tcp.StateFinWait1) + // Bare ACK: no state change, no pending flags generated. + t.Run("BareACK", func(t *testing.T) { + tcb := enterFinWait2(t) + err := tcb.Recv(tcp.Segment{SEQ: issB, ACK: issA + 1, Flags: tcp.FlagACK, WND: windowB}) + if err != nil { + t.Fatalf("bare ACK rejected: %v", err) + } + if tcb.State() != tcp.StateFinWait2 { + t.Fatalf("want FIN-WAIT-2; got %s", tcb.State()) + } + checkNoPending(t, &tcb) + }) - // A receives ACK for its FIN → FIN-WAIT-2. - err = tcb.Recv(tcp.Segment{SEQ: issB, ACK: issA + 1, Flags: tcp.FlagACK, WND: windowB}) - if err != nil { - t.Fatal(err) - } - assertState(tcp.StateFinWait2) + // In-window data: ACK generated, RCV.NXT advanced by payload length. + t.Run("InWindowData", func(t *testing.T) { + tcb := enterFinWait2(t) + rcvNxtBefore := tcb.RecvNext() + const dataLen = 50 + err := tcb.Recv(tcp.Segment{SEQ: issB, ACK: issA + 1, Flags: PSHACK, WND: windowB, DATALEN: dataLen}) + if err != nil { + t.Fatalf("in-window data rejected: %v", err) + } + if tcb.State() != tcp.StateFinWait2 { + t.Fatalf("want FIN-WAIT-2; got %s", tcb.State()) + } + seg, ok := tcb.PendingSegment(0) + if !ok { + t.Fatal("expected ACK pending for received data") + } + if !seg.Flags.HasAll(tcp.FlagACK) { + t.Fatalf("pending flags: want ACK; got %s", seg.Flags) + } + if seg.ACK != rcvNxtBefore+dataLen { + t.Fatalf("pending ACK: want %d; got %d", rcvNxtBefore+dataLen, seg.ACK) + } + if tcb.RecvNext() != rcvNxtBefore+dataLen { + t.Fatalf("RCV.NXT: want %d; got %d", rcvNxtBefore+dataLen, tcb.RecvNext()) + } + }) - // A receives bare ACK (duplicate/window update) in FIN-WAIT-2. - // RFC 9293: this should be silently accepted, NOT rejected. - err = tcb.Recv(tcp.Segment{SEQ: issB, ACK: issA + 1, Flags: tcp.FlagACK, WND: windowB}) - if err != nil { - t.Fatalf("bare ACK in FIN-WAIT-2 rejected: %v", err) - } - assertState(tcp.StateFinWait2) - - // A receives FIN|ACK → TIME-WAIT. - err = tcb.Recv(tcp.Segment{SEQ: issB, ACK: issA + 1, Flags: FINACK, WND: windowB}) - if err != nil { - t.Fatal(err) - } - assertState(tcp.StateTimeWait) -} - -// TestRcvFinWait2_DataFromRemote tests that data segments are accepted in FIN-WAIT-2. -// The remote side has not sent FIN yet and may still send data per RFC 9293 §3.10.7.4. -func TestRcvFinWait2_DataFromRemote(t *testing.T) { - var tcb tcp.ControlBlock - const windowA, windowB = 1000, 1000 - const issA, issB = 100, 300 - tcb.HelperInitState(tcp.StateEstablished, issA, issA, windowA) - tcb.HelperInitRcv(issB, issB, windowB) - - // A sends FIN|ACK → FIN-WAIT-1. - err := tcb.Close() - if err != nil { - t.Fatal(err) - } - seg, _ := tcb.PendingSegment(0) - err = tcb.Send(seg) - if err != nil { - t.Fatal(err) - } - - // A receives ACK → FIN-WAIT-2. - err = tcb.Recv(tcp.Segment{SEQ: issB, ACK: issA + 1, Flags: tcp.FlagACK, WND: windowB}) - if err != nil { - t.Fatal(err) - } - if tcb.State() != tcp.StateFinWait2 { - t.Fatalf("want FIN-WAIT-2; got %s", tcb.State()) - } - - // A receives data from remote (remote hasn't closed yet). - const dataLen = 50 - err = tcb.Recv(tcp.Segment{SEQ: issB, ACK: issA + 1, Flags: PSHACK, WND: windowB, DATALEN: dataLen}) - if err != nil { - t.Fatalf("data in FIN-WAIT-2 rejected: %v", err) - } - if tcb.State() != tcp.StateFinWait2 { - t.Fatalf("want FIN-WAIT-2 after data; got %s", tcb.State()) - } - - // A receives FIN|ACK → TIME-WAIT. - err = tcb.Recv(tcp.Segment{SEQ: issB + dataLen, ACK: issA + 1, Flags: FINACK, WND: windowB}) - if err != nil { - t.Fatal(err) - } - if tcb.State() != tcp.StateTimeWait { - t.Fatalf("want TIME-WAIT; got %s", tcb.State()) - } + // FIN (without ACK): transition to TIME-WAIT, ACK generated, RCV.NXT advanced by 1. + t.Run("FIN", func(t *testing.T) { + tcb := enterFinWait2(t) + rcvNxtBefore := tcb.RecvNext() + // Bare FIN without ACK — tests that HasAny(FlagFIN) is used, not HasAll(finack). + err := tcb.Recv(tcp.Segment{SEQ: issB, Flags: tcp.FlagFIN, WND: windowB}) + if err != nil { + t.Fatalf("FIN rejected: %v", err) + } + if tcb.State() != tcp.StateTimeWait { + t.Fatalf("want TIME-WAIT; got %s", tcb.State()) + } + seg, ok := tcb.PendingSegment(0) + if !ok { + t.Fatal("expected ACK pending for FIN") + } + if !seg.Flags.HasAll(tcp.FlagACK) { + t.Fatalf("pending flags: want ACK; got %s", seg.Flags) + } + if seg.ACK != rcvNxtBefore+1 { + t.Fatalf("pending ACK: want %d; got %d", rcvNxtBefore+1, seg.ACK) + } + if tcb.RecvNext() != rcvNxtBefore+1 { + t.Fatalf("RCV.NXT: want %d; got %d", rcvNxtBefore+1, tcb.RecvNext()) + } + }) } func FuzzTCBActions(f *testing.F) {