mirror of
https://github.com/soypat/lneto.git
synced 2026-08-05 23:43:40 +00:00
d5c4fa53c6
* 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
550 lines
18 KiB
Go
550 lines
18 KiB
Go
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")
|
||
}
|