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
This commit is contained in:
Pat Whittingslow
2026-03-04 16:59:30 +01:00
committed by GitHub
parent 02b4e71c2a
commit d5c4fa53c6
10 changed files with 600 additions and 85 deletions
+9 -6
View File
@@ -111,18 +111,21 @@ func (conn *Conn) BufferedUnsent() int {
return conn.h.BufferedUnsent()
}
func (conn *Conn) AvailableInput() int {
// FreeInput returns the number of free bytes in the socket's receive(input) buffer.
// The TCP window mechanism advertises this space to the remote peer,
// preventing the sender from transmitting more data than the buffer can hold.
func (conn *Conn) FreeInput() int {
conn.mu.Lock()
defer conn.mu.Unlock()
return conn.h.FreeRx()
return conn.h.FreeInput()
}
// AvailableOutput returns amount of bytes available to write to output
// before [Conn.Write] returns an error due to insufficient space to store outgoing data.
func (conn *Conn) AvailableOutput() int {
// FreeOutput returns the number of free bytes in the socket's transmit(output) buffer.
// This is the amount of data that can be written via [Conn.Write] before it blocks.
func (conn *Conn) FreeOutput() int {
conn.mu.Lock()
defer conn.mu.Unlock()
return conn.h.AvailableOutput()
return conn.h.FreeOutput()
}
// OpenActive opens a connection to a remote peer with a known IP address and port combination.
+43 -16
View File
@@ -82,7 +82,10 @@ func (tcb *ControlBlock) MaxInFlightData() Size {
return 0 // SYN not yet received.
}
unacked := Sizeof(tcb.snd.UNA, tcb.snd.NXT)
return tcb.snd.WND - unacked - 1 // TODO: is this -1 supposed to be here?
if unacked >= tcb.snd.WND {
return 0
}
return tcb.snd.WND - unacked
}
// SetWindow sets the local receive window size. This represents the maximum amount of data
@@ -123,8 +126,8 @@ type sendSpace struct {
NXT Value // send next. This seq and up to UNA+WND-1 are allowed to be sent. Corresponds to local data.
WND Size // send window defined by remote. Permitted number of local unacked octets in flight.
MSS Size // maximum segment size advertised by remote peer. 0 means not set.
// WL1 Value // segment sequence number used for last window update
// WL2 Value // segment acknowledgment number used for last window update
WL1 Value // segment SEQ number of the last send-window update (RFC 9293 §3.10.7.4)
WL2 Value // segment ACK number of the last send-window update (RFC 9293 §3.10.7.4)
}
// inFlight returns amount of unacked bytes sent out.
@@ -184,7 +187,8 @@ func (tcb *ControlBlock) HasPending() bool { return tcb.pending[0] != 0 }
// It does not modify the ControlBlock state or pending segment queue.
func (tcb *ControlBlock) PendingSegment(payloadLen int) (_ Segment, ok bool) {
if tcb.challengeAck {
tcb.challengeAck = false
// 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
}
pending := tcb.pending[0]
@@ -202,7 +206,7 @@ func (tcb *ControlBlock) PendingSegment(payloadLen int) (_ Segment, ok bool) {
_ = inFlight
maxPayload := tcb.snd.maxSend()
if payloadLen > int(maxPayload) {
if maxPayload == 0 && !pending.HasAny(FlagFIN|FlagRST|FlagSYN) {
if maxPayload == 0 && pending == 0 {
return Segment{}, false
} else if maxPayload > tcb.snd.WND {
panic("seqs: bad calculation")
@@ -304,8 +308,21 @@ func (tcb *ControlBlock) Recv(seg Segment) (err error) {
}
// We accept the segment and update TCB state.
tcb.snd.WND = seg.WND
if seg.Flags.HasAny(FlagACK) {
// RFC 9293 §3.10.7.4 step 5: update send window only when WL1/WL2 conditions allow it.
// WL1==WL2==0 is the uninitialized sentinel; the first update is always allowed so that
// connections with a remote ISS in the upper half of the uint32 space still work
// (modular LessThan would otherwise return false for 0.LessThan(largeISS)).
// Within that, duplicate ACKs (non-advancing) may only open the window, never shrink it.
wlUnset := tcb.snd.WL1 == 0 && tcb.snd.WL2 == 0
if wlUnset || tcb.snd.WL1.LessThan(seg.SEQ) || (tcb.snd.WL1 == seg.SEQ && tcb.snd.WL2.LessThanEq(seg.ACK)) {
if tcb.snd.UNA.LessThan(seg.ACK) || seg.WND > tcb.snd.WND {
tcb.snd.WND = seg.WND
}
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
}
seglen := seg.LEN()
@@ -349,9 +366,8 @@ func (tcb *ControlBlock) Send(seg Segment) error {
case StateCloseWait:
if hasFIN {
tcb._state = StateLastAck
} else if hasACK {
newPending = finack // Queue finack.
}
// No auto-queue of FIN on ACK: user must call Close() to initiate local FIN.
}
// Advance pending flags queue.
@@ -362,6 +378,11 @@ func (tcb *ControlBlock) Send(seg Segment) error {
}
tcb.pending[0] |= newPending
// Sending an ACK satisfies any outstanding challenge-ACK obligation.
if tcb.challengeAck && seg.Flags.HasAny(FlagACK) {
tcb.challengeAck = false
}
// The segment is valid, we can update TCB state.
seglen := seg.LEN()
tcb.snd.NXT.UpdateForward(seglen)
@@ -465,8 +486,7 @@ func (tcb *ControlBlock) validateIncomingSegment(seg Segment) (err error) {
// Special treatment of duplicate ACKs on established connection and of ACKs of unsent data.
// https://www.rfc-editor.org/rfc/rfc9293.html#section-3.10.7.4-2.5.2.2.2.3.2.1
case established && acksOld && !ctlOrDataSegment:
err = errDropSegment
tcb.pending[0] &= FlagFIN // Completely ignore duplicate ACKs but do not erase fin bit.
// We don't drop packet.
if isDebug {
tcb.debug("rcv:ACK-dup", slog.String("state", tcb._state.String()),
slog.Uint64("seg.ack", uint64(seg.ACK)), slog.Uint64("snd.una", uint64(tcb.snd.UNA)))
@@ -474,7 +494,7 @@ func (tcb *ControlBlock) validateIncomingSegment(seg Segment) (err error) {
case established && acksUnsentData:
err = errDropSegment
tcb.pending[0] = FlagACK // Send ACK for unsent data.
tcb.pending[0] |= FlagACK // Send ACK for unsent data; |= preserves any pending FIN.
if isDebug {
tcb.debug("rcv:ACK-unsent", slog.String("state", tcb._state.String()),
slog.Uint64("seg.ack", uint64(seg.ACK)), slog.Uint64("snd.nxt", uint64(tcb.snd.NXT)))
@@ -512,16 +532,23 @@ func (tcb *ControlBlock) resetRcv(localWND Size, remoteISS Value) {
func (tcb *ControlBlock) handleRST(seq Value) error {
tcb.debug("rcv:RST", slog.String("state", tcb._state.String()))
if tcb._state.IsPreestablished() {
// RFC 9293 §3.5.3: non-synchronized states accept RST if SEQ is in window.
// No challenge ACK for non-synchronized states. Return to LISTEN.
switch tcb._state {
case StateSynSent:
// RFC 9293 §3.10.7.2: RST in SYN-SENT aborts the active open.
tcb.Abort()
return net.ErrClosed
case StateListen:
// RFC 9293 §3.5.3: RST in LISTEN state is ignored.
return errDropSegment
case StateSynRcvd:
// RFC 9293 §3.5.3: SYN-RCVD (passive open) returns to LISTEN on RST.
tcb.pending[0] = 0
tcb._state = StateListen
tcb.resetSnd(tcb.snd.ISS+tcb.rstJump(), tcb.snd.WND)
tcb.resetRcv(tcb.rcv.WND, 3_14159_2653^tcb.rcv.IRS)
return errDropSegment
}
// Synchronized states: exact match required, challenge ACK for in-window non-exact.
// Synchronized states: exact SEQ match required; challenge ACK for in-window non-exact.
if seq != tcb.rcv.NXT {
tcb.challengeAck = true
tcb.pending[0] |= FlagACK
+5 -3
View File
@@ -48,8 +48,9 @@ func (tcb *ControlBlock) rcvSynSent(seg Segment) (pending Flags, err error) {
func (tcb *ControlBlock) rcvSynRcvd(seg Segment) (pending Flags, err error) {
switch {
// case !seg.Flags.HasAll(FlagACK):
// err = errors.New("rcvSynRcvd: expected ACK")
case !seg.Flags.HasAll(FlagACK):
// RFC 9293 §3.10.7.4 step 5: "If the ACK bit is off, drop the segment and return."
err = errBadSegack
case seg.ACK != tcb.snd.UNA+1:
err = errBadSegack
}
@@ -69,7 +70,8 @@ func (tcb *ControlBlock) rcvEstablished(seg Segment) (pending Flags, err error)
if hasFin {
// See Figure 5: TCP Connection State Diagram of RFC 9293.
tcb._state = StateCloseWait
tcb.pending[1] = FlagFIN // Queue FIN for after the CloseWait ACK.
// RFC 9293 §3.5: CLOSE-WAIT allows local side to continue sending.
// Do NOT auto-queue FIN here; user must call Close() explicitly.
}
}
+295
View File
@@ -0,0 +1,295 @@
package tcp
import (
"testing"
)
// TestMaxInFlightData_Underflow verifies that MaxInFlightData never underflows
// when unacked data equals or exceeds the send window.
func TestMaxInFlightData_Underflow(t *testing.T) {
const (
iss Value = 100
remoteISS Value = 500
localWND Size = 1024
)
setup := func(wnd Size, unacked Size) ControlBlock {
var tcb ControlBlock
// snd.UNA = iss, snd.NXT = iss + unacked
tcb.HelperInitState(StateEstablished, iss, iss+Value(unacked), localWND)
tcb.HelperInitRcv(remoteISS, remoteISS+1, wnd) // snd.WND = wnd
// HelperInitState sets snd.UNA = iss; unacked = snd.NXT - snd.UNA = unacked.
return tcb
}
// unacked == snd.WND: usable = 0, but code returns 0 - 1 = 2^32-1.
t.Run("UnackedEqualsWindow", func(t *testing.T) {
const wnd Size = 200
tcb := setup(wnd, wnd) // unacked == wnd
got := tcb.MaxInFlightData()
if got != 0 {
t.Errorf("MaxInFlightData() = %d; want 0 (unacked=%d == WND=%d, uint32 underflow via -1)",
got, Sizeof(tcb.snd.UNA, tcb.snd.NXT), tcb.snd.WND)
}
})
// unacked > snd.WND: window shrank; usable = 0, but code underflows.
t.Run("UnackedExceedsWindow", func(t *testing.T) {
const wnd Size = 100
tcb := setup(wnd, 250) // unacked 250 > wnd 100
got := tcb.MaxInFlightData()
if got != 0 {
t.Errorf("MaxInFlightData() = %d; want 0 (unacked=%d > WND=%d, must not underflow)",
got, Sizeof(tcb.snd.UNA, tcb.snd.NXT), tcb.snd.WND)
}
})
}
// TestMaxInFlightData_BogusMinusOne verifies that MaxInFlightData returns
// snd.WND - unacked (RFC value), not snd.WND - unacked - 1.
func TestMaxInFlightData_BogusMinusOne(t *testing.T) {
const (
iss Value = 100
remoteISS Value = 500
localWND Size = 1024
wnd Size = 100
)
setup := func(unacked Size) ControlBlock {
var tcb ControlBlock
tcb.HelperInitState(StateEstablished, iss, iss+Value(unacked), localWND)
tcb.HelperInitRcv(remoteISS, remoteISS+1, wnd)
return tcb
}
// Zero unacked: RFC says wnd, code returns wnd-1.
t.Run("ZeroUnacked_WantWND", func(t *testing.T) {
tcb := setup(0)
want := wnd
got := tcb.MaxInFlightData()
if got != want {
t.Errorf("MaxInFlightData() = %d; want %d (RFC: WND - unacked, no bogus -1)", got, want)
}
})
// unacked = 50: RFC says 50, code returns 49.
t.Run("PartialUnacked_WantWNDMinusUnacked", func(t *testing.T) {
tcb := setup(50)
want := wnd - 50
got := tcb.MaxInFlightData()
if got != want {
t.Errorf("MaxInFlightData() = %d; want %d (RFC: WND - unacked, no bogus -1)", got, want)
}
})
}
// TestRecvAcksUnsent_PreservesPendingFIN verifies that receiving a segment whose
// ACK field acknowledges unsent data does not clobber other pending flags (FIN).
func TestRecvAcksUnsent_PreservesPendingFIN(t *testing.T) {
const (
iss Value = 100
remoteISS Value = 500
localWND Size = 2048
remoteWND Size = 2048
)
var tcb ControlBlock
// 50 bytes in flight (snd.NXT = iss+50), snd.UNA = iss.
tcb.HelperInitState(StateEstablished, iss, iss+50, localWND)
tcb.HelperInitRcv(remoteISS, remoteISS+1, remoteWND)
// Simulate a pending FIN+ACK that should not be clobbered.
tcb.pending[0] = FlagFIN | FlagACK
// Bogus ACK: seg.ACK > snd.NXT (acks data not yet sent).
bogusACK := Segment{
SEQ: remoteISS + 1, // == rcv.NXT
ACK: iss + 51, // > snd.NXT=iss+50 → acksUnsentData
Flags: FlagACK,
WND: remoteWND,
}
err := tcb.Recv(bogusACK)
if !IsDroppedErr(err) {
t.Errorf("expected errDropSegment for ACK of unsent data, got: %v", err)
}
// FIN must be preserved (|= not =). Currently FAILS: pending[0] = FlagACK only.
if !tcb.pending[0].HasAll(FlagFIN) {
t.Errorf("FIN cleared from pending[0] by ACK-unsent handler: got %s, want FlagFIN|FlagACK",
tcb.pending[0])
}
}
// TestHandleRST_SynSent_GoesToClosed verifies that receiving RST in SYN-SENT
// transitions to CLOSED (not LISTEN), per RFC 9293 §3.10.7.2.
func TestHandleRST_SynSent_GoesToClosed(t *testing.T) {
const (
iss Value = 1000
localWND Size = 2048
)
var tcb ControlBlock
// Active open: SYN-SENT. rcv.NXT=0, rcv.WND=localWND (SYN not yet received).
tcb.HelperInitState(StateSynSent, iss, iss+1, localWND)
// RST with SEQ in receive window [rcv.NXT=0, 0+localWND=2048).
rst := Segment{
SEQ: 0, // in [0, localWND)
Flags: FlagRST,
}
err := tcb.Recv(rst)
if err == nil {
t.Fatal("RST in SYN-SENT must return an error (connection reset)")
}
// RFC 9293 §3.10.7.2: "enter CLOSED state, delete TCB, and return."
// Bug: code sets state to StateListen instead.
if tcb.State() != StateClosed {
t.Errorf("state = %s after RST in SYN-SENT; want CLOSED (RFC 9293 §3.10.7.2)", tcb.State())
}
}
// TestRecvDuplicateACK_DoesNotShrinkWindow verifies that a duplicate ACK
// (ACK == snd.UNA) carrying a smaller window does not reduce snd.WND.
func TestRecvDuplicateACK_DoesNotShrinkWindow(t *testing.T) {
const (
iss Value = 100
remoteISS Value = 500
localWND Size = 2048
remoteWND Size = 1000 // initial send window
)
var tcb ControlBlock
tcb.HelperInitState(StateEstablished, iss, iss+10, localWND)
tcb.HelperInitRcv(remoteISS, remoteISS+1, remoteWND) // snd.WND = 1000
tcb.snd.UNA = iss + 10 // all sent data acked
// Duplicate ACK: ACK == snd.UNA (no new data acked), WND reduced to 100.
dupACK := Segment{
SEQ: remoteISS + 1, // == rcv.NXT
ACK: iss + 10, // == snd.UNA (duplicate)
Flags: FlagACK,
WND: 100, // smaller than current snd.WND=1000
}
err := tcb.Recv(dupACK)
if err != nil {
t.Fatalf("duplicate ACK must be silently accepted: %v", err)
}
// snd.WND must not shrink. Currently FAILS: snd.WND gets set to 100.
if tcb.snd.WND != remoteWND {
t.Errorf("snd.WND = %d after duplicate ACK; want %d (missing WL1/WL2 guard per RFC 9293 §3.10.7.4)",
tcb.snd.WND, remoteWND)
}
}
// TestPendingSegment_ChallengeACK_Idempotent verifies that calling PendingSegment
// does not consume the challengeAck flag, honouring its read-only contract.
func TestPendingSegment_ChallengeACK_Idempotent(t *testing.T) {
var tcb ControlBlock
tcb.HelperInitState(StateEstablished, 100, 101, 1024)
tcb.HelperInitRcv(500, 501, 1024)
tcb.challengeAck = true
seg1, ok1 := tcb.PendingSegment(0)
if !ok1 {
t.Fatal("PendingSegment returned !ok when challengeAck=true")
}
// PendingSegment must not consume challengeAck (read-only contract).
// Currently FAILS: challengeAck is set to false on the first call.
if !tcb.challengeAck {
t.Error("PendingSegment cleared challengeAck flag; violates documented read-only contract")
}
// A second call (e.g., before the segment is actually transmitted) must still succeed.
seg2, ok2 := tcb.PendingSegment(0)
if !ok2 {
t.Fatal("second PendingSegment call returned !ok; challengeAck was consumed on first call")
}
if seg1 != seg2 {
t.Errorf("PendingSegment not idempotent:\n first=%+v\nsecond=%+v", seg1, seg2)
}
}
// TestRcvSynRcvd_NoACKFlag_DoesNotCompleteHandshake verifies that a segment
// lacking the ACK flag cannot complete the 3-way handshake even if its ACK
// field value coincidentally matches snd.UNA+1.
func TestRcvSynRcvd_NoACKFlag_DoesNotCompleteHandshake(t *testing.T) {
const (
iss Value = 1000
remoteISS Value = 5000
localWND Size = 2048
remoteWND Size = 2048
)
var tcb ControlBlock
// SYN-RCVD: server received SYN, sent SYN-ACK. Waiting for client ACK.
// snd.UNA=iss, snd.NXT=iss+1 (SYN-ACK consumed one seq).
// rcv.NXT=remoteISS+1 (client SYN consumed one seq).
tcb.HelperInitState(StateSynRcvd, iss, iss+1, localWND)
tcb.HelperInitRcv(remoteISS, remoteISS+1, remoteWND)
// Segment with NO ACK flag but ACK value == snd.UNA+1 (coincidentally correct).
noACKSeg := Segment{
SEQ: remoteISS + 1, // == rcv.NXT
ACK: iss + 1, // == snd.UNA+1 (right value, wrong flag)
Flags: 0, // ACK bit NOT set
WND: remoteWND,
}
tcb.Recv(noACKSeg) //nolint:errcheck // error value not the focus here
// Handshake must not complete without the ACK flag.
// Currently FAILS: state becomes ESTABLISHED because the flag check is commented out.
if tcb.State() == StateEstablished {
t.Errorf("3-way handshake completed without FlagACK in SYN-RCVD; " +
"ACK flag validation is commented out (control_rcvhandlers.go:51-52)")
}
}
// TestCloseWait_NoAutoFINBeforeUserClose verifies that entering CLOSE-WAIT after
// a remote FIN does not auto-queue a local FIN until the user calls Close().
func TestCloseWait_NoAutoFINBeforeUserClose(t *testing.T) {
const (
iss Value = 100
remoteISS Value = 500
localWND Size = 2048
remoteWND Size = 2048
)
var tcb ControlBlock
tcb.HelperInitState(StateEstablished, iss, iss+1, localWND)
tcb.HelperInitRcv(remoteISS, remoteISS+1, remoteWND)
// Remote sends FIN-ACK → we should enter CLOSE-WAIT.
finAck := Segment{
SEQ: remoteISS + 1, // == rcv.NXT
ACK: iss + 1, // == snd.NXT
Flags: FlagFIN | FlagACK,
WND: remoteWND,
}
if err := tcb.Recv(finAck); err != nil {
t.Fatalf("recv FIN-ACK: %v", err)
}
if tcb.State() != StateCloseWait {
t.Fatalf("state = %s; want CLOSE-WAIT after receiving FIN", tcb.State())
}
// Retrieve and send the pending ACK for the FIN.
pendSeg, ok := tcb.PendingSegment(0)
if !ok {
t.Fatal("no pending ACK after receiving FIN")
}
if err := tcb.Send(pendSeg); err != nil {
t.Fatalf("send ACK in CLOSE-WAIT: %v", err)
}
if tcb.State() != StateCloseWait {
t.Fatalf("state = %s after sending ACK; want CLOSE-WAIT (user has not called Close())", tcb.State())
}
// RFC 9293 §3.5: user may still send data in CLOSE-WAIT.
// FIN must NOT be pending until the user calls Close().
// Currently FAILS: Send(ACK) in CLOSE-WAIT auto-queues FINACK into pending[0].
seg, hasPending := tcb.PendingSegment(0)
if hasPending && seg.Flags.HasAny(FlagFIN) {
t.Errorf("FIN auto-queued in CLOSE-WAIT before user calls Close(): pending flags=%s "+
"(control.go:353-354 queues finack on any ACK sent in CLOSE-WAIT)", seg.Flags)
}
}
+29 -24
View File
@@ -241,6 +241,12 @@ func (h *Handler) Send(b []byte) (int, error) {
}
awaitingSyn := h.AwaitingSynSend()
buffered := h.bufTx.BufferedUnsent()
if h.scb.State() == StateCloseWait && !h.closing && buffered == 0 && !h.scb.HasPending() {
// Remote closed with no application data left to send: initiate our own close.
// Checked here (not in Recv) so the application can still write in CLOSE-WAIT
// before Send is called, implementing the half-close per RFC 9293 §3.5.
h.closing = true
}
if !awaitingSyn && buffered == 0 && !h.closing && !h.scb.HasPending() {
// Early nop short circuit.
return 0, nil
@@ -249,8 +255,10 @@ func (h *Handler) Send(b []byte) (int, error) {
if err != nil {
return 0, err
}
if buffered == 0 && h.closing {
// If Close called and no more data to be sent, terminate connection!
if buffered == 0 && h.closing && (h.scb.State() != StateCloseWait || !h.scb.HasPending()) {
// If Close called and no more data to be sent, terminate connection.
// In CLOSE-WAIT: wait until the pending ACK is sent first, since scb.Close()
// overwrites pending with [FIN|ACK] (unlike ESTABLISHED which merges via bitmask).
h.closing = false
err = h.scb.Close()
if err != nil {
@@ -307,21 +315,6 @@ func (h *Handler) Send(b []byte) (int, error) {
return datalen, nil
}
// FreeTx returns the amount of space free in the transmit buffer. A call to [Handler.Write] with a larger buffer will fail.
func (h *Handler) FreeTx() int {
return h.bufTx.Free()
}
// FreeRx returns the amount of space free in the receive buffer.
func (h *Handler) FreeRx() int {
return h.bufRx.Free()
}
// SizeRx returns the size of the TCP receive ring buffer.
func (h *Handler) SizeRx() int {
return h.bufRx.Size()
}
// Write implements [io.Writer] by copying b to a internal buffer to be sent over the network on the next
// [Handler.Send] call that can send data to remote peer. Use [Handler.Free] to know the maximum length the argument slice can be before erroring.
func (h *Handler) Write(b []byte) (int, error) {
@@ -376,24 +369,36 @@ func (h *Handler) maybeQueueWindowUpdate() {
}
}
// BufferedInput returns amount of bytes buffered in receive(input) buffer and ready to read
// with a [Handler.Read] call.
// SizeOutput returns the total size of the transmit ring buffer.
func (h *Handler) SizeOutput() int {
return h.bufTx.Size()
}
// SizeInput returns the total size of the receive ring buffer.
func (h *Handler) SizeInput() int {
return h.bufRx.Size()
}
// BufferedInput returns the number of unread bytes in the receive buffer.
func (h *Handler) BufferedInput() int {
return h.bufRx.Buffered()
}
// BufferedUnsent returns the number of bytes in the socket's transmit(output) buffer
// that has yet to be sent.
// BufferedUnsent returns the number of written but unsent bytes in the transmit buffer.
func (h *Handler) BufferedUnsent() int {
return h.bufTx.BufferedUnsent()
}
// AvailableOutput returns amount of bytes available to write to output
// before [Handler.Write] returns an error.
func (h *Handler) AvailableOutput() int {
// FreeOutput returns the number of free bytes in the transmit buffer.
func (h *Handler) FreeOutput() int {
return h.bufTx.Free()
}
// FreeInput returns the number of free bytes in the receive buffer.
func (h *Handler) FreeInput() int {
return h.bufRx.Free()
}
// AwaitingSynResponse returns true if the Handler is an active client opened with [Handler.OpenActive] and has already sent out the first SYN packet to the remote client.
func (h *Handler) AwaitingSynResponse() bool {
return h.remotePort != 0 && h.scb.State() == StateSynSent
+10 -10
View File
@@ -266,7 +266,7 @@ func TestTxBufferFreedOnACK(t *testing.T) {
establish(t, client, server, rawbuf[:])
// Record initial available space.
initialAvailable := client.AvailableOutput()
initialAvailable := client.FreeOutput()
if initialAvailable == 0 {
t.Fatal("expected non-zero initial available output")
}
@@ -284,7 +284,7 @@ func TestTxBufferFreedOnACK(t *testing.T) {
}
// Available space should have decreased.
afterWriteAvailable := client.AvailableOutput()
afterWriteAvailable := client.FreeOutput()
if afterWriteAvailable >= initialAvailable {
t.Fatalf("expected available to decrease after write: before=%d, after=%d",
initialAvailable, afterWriteAvailable)
@@ -303,7 +303,7 @@ func TestTxBufferFreedOnACK(t *testing.T) {
// After sending, data moves from "unsent" to "sent" - available should still be reduced
// until we receive an ACK.
afterSendAvailable := client.AvailableOutput()
afterSendAvailable := client.FreeOutput()
// Server receives DATA.
err = server.Recv(dataPacket)
@@ -329,7 +329,7 @@ func TestTxBufferFreedOnACK(t *testing.T) {
// THE BUG: After receiving ACK, the TX buffer should be freed.
// Without the fix, AvailableOutput() stays at the post-send value.
afterAckAvailable := client.AvailableOutput()
afterAckAvailable := client.FreeOutput()
if afterAckAvailable <= afterSendAvailable {
t.Fatalf("BUG (issue #22): TX buffer not freed after receiving ACK\n"+
@@ -384,7 +384,7 @@ func TestWindowUpdateAfterRead(t *testing.T) {
establish(t, client, server, rawbuf[:])
// Fill the server's RX buffer completely (without reading).
fillData := make([]byte, server.FreeRx())
fillData := make([]byte, server.FreeInput())
n, err := client.Write(fillData)
if err != nil {
t.Fatal("client write:", err)
@@ -400,8 +400,8 @@ func TestWindowUpdateAfterRead(t *testing.T) {
if err != nil {
t.Fatal("server recv:", err)
}
if server.FreeRx() != 0 {
t.Fatalf("expected server RX buffer full, got %d free", server.FreeRx())
if server.FreeInput() != 0 {
t.Fatalf("expected server RX buffer full, got %d free", server.FreeInput())
}
// Server sends ACK — should advertise Window=0.
@@ -451,7 +451,7 @@ func TestWindowUpdateAfterRead(t *testing.T) {
if wnd := wndFrm.WindowSize(); wnd == 0 {
t.Fatal("BUG: window update ACK still has Window=0")
}
t.Logf("window update sent: Window=%d (buffer free=%d)", wndFrm.WindowSize(), server.FreeRx())
t.Logf("window update sent: Window=%d (buffer free=%d)", wndFrm.WindowSize(), server.FreeInput())
}
// TestWindowUpdateSWSAvoidance verifies that small reads that free less than
@@ -486,7 +486,7 @@ func TestWindowUpdateSWSAvoidance(t *testing.T) {
establish(t, client, server, rawbuf[:])
// Fill most of the server's RX buffer (leave a tiny amount free).
fillSize := server.FreeRx() - 10
fillSize := server.FreeInput() - 10
fillData := make([]byte, fillSize)
for i := range fillData {
fillData[i] = byte(i)
@@ -542,7 +542,7 @@ func TestWindowUpdateSWSAvoidance(t *testing.T) {
t.Logf("NOTE: window update sent after small read (freed %d of %d buffer)", len(smallRead), rxBufSize)
// This is acceptable if the threshold is met, but for SWS avoidance
// we expect no update when the freed increment is < bufSize/2.
freeAfterRead := Size(server.FreeRx())
freeAfterRead := Size(server.FreeInput())
if freeAfterRead < Size(rxBufSize/2) {
t.Fatalf("SWS violation: window update sent when free=%d < bufSize/2=%d", freeAfterRead, rxBufSize/2)
}
+173
View File
@@ -342,6 +342,102 @@ func TestWindowReject_ChallengeACK(t *testing.T) {
})
}
// 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) {
@@ -374,3 +470,80 @@ func TestSYNPreestablished_StillAllowed(t *testing.T) {
}
})
}
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")
}
+17 -11
View File
@@ -285,24 +285,26 @@ a FIN from A, acknowledges it, then later closes and sends its own FIN.
*/
func TestExchange_rfc9293_figure12_peerB(t *testing.T) {
const issA, issB, windowA, windowB = 100, 300, 1000, 1000
// Note: After B sends an ACK in CLOSE-WAIT, the implementation auto-queues FIN|ACK.
// This is an optimization that combines steps 3 and 4 of RFC 9293 Figure 12.
exchangeB := []tcp.Exchange{
0: { // B receives FIN|ACK from A, goes to CLOSE-WAIT with pending ACK.
// RFC 9293 Figure 12 steps 2-3: B receives FIN|ACK, then sends back ACK.
// B remains in CLOSE-WAIT, able to keep sending (RFC 9293 §3.5).
exchangeBeforeClose := []tcp.Exchange{
0: { // Step 2: B receives FIN|ACK from A, goes to CLOSE-WAIT with pending ACK.
Incoming: &tcp.Segment{SEQ: issA, ACK: issB, Flags: FINACK, WND: windowA},
WantState: tcp.StateCloseWait,
WantPending: &tcp.Segment{SEQ: issB, ACK: issA + 1, Flags: tcp.FlagACK, WND: windowB},
},
1: { // B sends ACK to A. Implementation auto-queues FIN|ACK for close.
Outgoing: &tcp.Segment{SEQ: issB, ACK: issA + 1, Flags: tcp.FlagACK, WND: windowB},
WantState: tcp.StateCloseWait,
WantPending: &tcp.Segment{SEQ: issB, ACK: issA + 1, Flags: FINACK, WND: windowB},
1: { // Step 3: B sends ACK to A. B remains in CLOSE-WAIT.
Outgoing: &tcp.Segment{SEQ: issB, ACK: issA + 1, Flags: tcp.FlagACK, WND: windowB},
WantState: tcp.StateCloseWait,
},
2: { // B sends FIN|ACK to A, goes to LAST-ACK.
}
// RFC 9293 Figure 12 step 4: B calls Close(). Queues FIN|ACK, goes to LAST-ACK.
exchangeAfterClose := []tcp.Exchange{
0: { // Step 4: B sends FIN|ACK to A, goes to LAST-ACK.
Outgoing: &tcp.Segment{SEQ: issB, ACK: issA + 1, Flags: FINACK, WND: windowB},
WantState: tcp.StateLastAck,
},
3: { // B receives final ACK from A, goes to CLOSED.
1: { // Step 5: B receives final ACK from A, goes to CLOSED.
Incoming: &tcp.Segment{SEQ: issA + 1, ACK: issB + 1, Flags: tcp.FlagACK, WND: windowA},
WantState: tcp.StateClosed,
},
@@ -310,7 +312,11 @@ func TestExchange_rfc9293_figure12_peerB(t *testing.T) {
var tcbB tcp.ControlBlock
tcbB.HelperInitState(tcp.StateEstablished, issB, issB, windowB)
tcbB.HelperInitRcv(issA, issA, windowA)
tcbB.HelperExchange(t, exchangeB)
tcbB.HelperExchange(t, exchangeBeforeClose)
if err := tcbB.Close(); err != nil { // Step 4: (Close) from RFC Figure 12.
t.Fatal("close:", err)
}
tcbB.HelperExchange(t, exchangeAfterClose)
}
/*
+17 -13
View File
@@ -34,16 +34,15 @@ func TestExchangeTest_PassiveClose_FINACKRegression(t *testing.T) {
BState: tcp.StateCloseWait,
BPending: &tcp.Segment{SEQ: issB, ACK: issA + 1, Flags: tcp.FlagACK, WND: windowB},
},
1: { // B sends ACK to A. Auto-queues FIN|ACK in CLOSE-WAIT.
Seg: tcp.Segment{SEQ: issB, ACK: issA + 1, Flags: tcp.FlagACK, WND: windowB},
Action: tcp.StepBSends,
AState: tcp.StateFinWait2,
BState: tcp.StateCloseWait,
BPending: &tcp.Segment{SEQ: issB, ACK: issA + 1, Flags: FINACK, WND: windowB},
1: { // B sends ACK to A (RFC 9293 Figure 12 step 3). B stays in CLOSE-WAIT.
Seg: tcp.Segment{SEQ: issB, ACK: issA + 1, Flags: tcp.FlagACK, WND: windowB},
Action: tcp.StepBSends,
AState: tcp.StateFinWait2,
BState: tcp.StateCloseWait,
},
2: { // B calls Close(). Goes to LAST-ACK. Pending must be FIN|ACK combined.
// This is the regression check: Close() must NOT overwrite the auto-queued
// FIN|ACK with [FlagFIN, FlagACK] in separate pending slots.
2: { // B calls Close() (RFC 9293 Figure 12 step 4). Goes to LAST-ACK.
// Regression check: Close() must queue FIN|ACK as a single combined flag
// in pending[0], not as [FlagFIN, FlagACK] in separate pending slots.
Action: tcp.StepBCloses,
AState: tcp.StateFinWait2, // A unchanged.
BState: tcp.StateLastAck,
@@ -79,7 +78,7 @@ func TestExchangeTest_figure12(t *testing.T) {
InitStateA: tcp.StateEstablished,
InitStateB: tcp.StateEstablished,
Steps: []tcp.SegmentStep{
0: { // A sends FIN|ACK to B.
0: { // A sends FIN|ACK to B (RFC 9293 Figure 12 step 2).
Seg: tcp.Segment{SEQ: issA, ACK: issB, Flags: FINACK, WND: windowA},
Action: tcp.StepASends,
AState: tcp.StateFinWait1,
@@ -87,15 +86,20 @@ func TestExchangeTest_figure12(t *testing.T) {
APending: nil,
BPending: &tcp.Segment{SEQ: issB, ACK: issA + 1, Flags: tcp.FlagACK, WND: windowB},
},
1: { // B sends ACK to A. (Auto-queues FIN|ACK in CLOSE-WAIT)
1: { // B sends ACK to A (RFC 9293 Figure 12 step 3). B stays in CLOSE-WAIT.
Seg: tcp.Segment{SEQ: issB, ACK: issA + 1, Flags: tcp.FlagACK, WND: windowB},
Action: tcp.StepBSends,
AState: tcp.StateFinWait2,
BState: tcp.StateCloseWait,
APending: &tcp.Segment{SEQ: issA + 1, ACK: issB, Flags: tcp.FlagACK, WND: windowA}, // TODO: should be nil?
},
2: { // B calls Close() (RFC 9293 Figure 12 step 4). B goes to LAST-ACK.
Action: tcp.StepBCloses,
AState: tcp.StateFinWait2,
BState: tcp.StateLastAck,
BPending: &tcp.Segment{SEQ: issB, ACK: issA + 1, Flags: FINACK, WND: windowB},
},
2: { // B sends FIN|ACK to A.
3: { // B sends FIN|ACK to A (RFC 9293 Figure 12 step 4 cont).
Seg: tcp.Segment{SEQ: issB, ACK: issA + 1, Flags: FINACK, WND: windowB},
Action: tcp.StepBSends,
AState: tcp.StateTimeWait,
@@ -103,7 +107,7 @@ func TestExchangeTest_figure12(t *testing.T) {
APending: &tcp.Segment{SEQ: issA + 1, ACK: issB + 1, Flags: tcp.FlagACK, WND: windowA},
BPending: nil,
},
3: { // A sends final ACK to B.
4: { // A sends final ACK to B (RFC 9293 Figure 12 step 5).
Seg: tcp.Segment{SEQ: issA + 1, ACK: issB + 1, Flags: tcp.FlagACK, WND: windowA},
Action: tcp.StepASends,
AState: tcp.StateTimeWait,
+2 -2
View File
@@ -287,8 +287,8 @@ func (tst *tester) TestTCPHandshake(stack1, stack2 *StackAsync) {
func (tst *tester) TestTCPEstablishedSingleData(srcStack, dstStack *StackAsync, srcConn, dstConn *tcp.Conn, sendData []byte) {
t := tst.t
t.Helper()
availTx := srcConn.AvailableOutput()
availRx := dstConn.AvailableInput()
availTx := srcConn.FreeOutput()
availRx := dstConn.FreeInput()
if availTx < len(sendData) {
t.Fatal("insufficient space for write call", availTx, len(sendData))
} else if len(sendData) <= 0 {