Apply SWS avoidance to ACKs with window updates

When recovering from a zero-receive-window situation, and asked to
send out an ACK, ensure that we apply SWS avoidance in our window
updates.

Fixes #5984

PiperOrigin-RevId: 373689578
This commit is contained in:
Mithun Iyer 2021-05-13 17:15:08 -07:00 committed by gVisor bot
parent 3894c9fcb9
commit 7ea2dcbaec
4 changed files with 122 additions and 28 deletions

View File

@ -1601,7 +1601,7 @@ func (e *endpoint) selectWindow() (wnd seqnum.Size) {
//
// For large receive buffers, the threshold is aMSS - once reader reads more
// than aMSS we'll send ACK. For tiny receive buffers, the threshold is half of
// receive buffer size. This is chosen arbitrairly.
// receive buffer size. This is chosen arbitrarily.
// crossed will be true if the window size crossed the ACK threshold.
// above will be true if the new window is >= ACK threshold and false
// otherwise.

View File

@ -148,6 +148,18 @@ func (r *receiver) getSendParams() (RcvNxt seqnum.Value, rcvWnd seqnum.Size) {
}
newWnd = curWnd
}
// Apply silly-window avoidance when recovering from zero-window situation.
// Keep advertising zero receive window up until the new window reaches a
// threshold.
if r.rcvWnd == 0 && newWnd != 0 {
r.ep.rcvQueueInfo.rcvQueueMu.Lock()
if crossed, above := r.ep.windowCrossedACKThresholdLocked(int(newWnd), int(r.ep.ops.GetReceiveBufferSize())); !crossed && !above {
newWnd = 0
}
r.ep.rcvQueueInfo.rcvQueueMu.Unlock()
}
// Stash away the non-scaled receive window as we use it for measuring
// receiver's estimated RTT.
r.rcvWnd = newWnd

View File

@ -57,6 +57,11 @@ type DUTUname struct {
OperatingSystem string
}
// IsLinux returns true if we are running natively on Linux.
func (n *DUTUname) IsLinux() bool {
return Native && n.OperatingSystem == "GNU/Linux"
}
// DUTTestNet describes the test network setup on dut and how the testbench
// should connect with an existing DUT.
type DUTTestNet struct {

View File

@ -45,37 +45,114 @@ func TestZeroReceiveWindow(t *testing.T) {
dut.SetSockOptInt(t, acceptFd, unix.IPPROTO_TCP, unix.TCP_NODELAY, 1)
samplePayload := &testbench.Payload{Bytes: testbench.GenerateRandomPayload(t, payloadLen)}
// Expect the DUT to eventually advertise zero receive window.
// The test would timeout otherwise.
for readOnce := false; ; {
conn.Send(t, testbench.TCP{Flags: testbench.TCPFlags(header.TCPFlagAck | header.TCPFlagPsh)}, samplePayload)
gotTCP, err := conn.Expect(t, testbench.TCP{Flags: testbench.TCPFlags(header.TCPFlagAck)}, time.Second)
if err != nil {
t.Fatalf("expected packet was not received: %s", err)
}
// Read once to trigger the subsequent window update from the
// DUT to grow the right edge of the receive window from what
// was advertised in the SYN-ACK. This ensures that we test
// for the full default buffer size (1MB on gVisor at the time
// of writing this comment), thus testing for cases when the
// scaled receive window size ends up > 65535 (0xffff).
if !readOnce {
if got := dut.Recv(t, acceptFd, int32(payloadLen), 0); len(got) != payloadLen {
t.Fatalf("got dut.Recv(t, %d, %d, 0) = %d, want %d", acceptFd, payloadLen, len(got), payloadLen)
}
readOnce = true
}
windowSize := *gotTCP.WindowSize
t.Logf("got window size = %d", windowSize)
if windowSize == 0 {
break
}
}
fillRecvBuffer(t, &conn, &dut, acceptFd, payloadLen)
})
}
}
func fillRecvBuffer(t *testing.T, conn *testbench.TCPIPv4, dut *testbench.DUT, acceptFd int32, payloadLen int) {
// Expect the DUT to eventually advertise zero receive window.
// The test would timeout otherwise.
for readOnce := false; ; {
samplePayload := &testbench.Payload{Bytes: testbench.GenerateRandomPayload(t, payloadLen)}
conn.Send(t, testbench.TCP{Flags: testbench.TCPFlags(header.TCPFlagAck | header.TCPFlagPsh)}, samplePayload)
gotTCP, err := conn.Expect(t, testbench.TCP{Flags: testbench.TCPFlags(header.TCPFlagAck)}, time.Second)
if err != nil {
t.Fatalf("expected packet was not received: %s", err)
}
// Read once to trigger the subsequent window update from the
// DUT to grow the right edge of the receive window from what
// was advertised in the SYN-ACK. This ensures that we test
// for the full default buffer size (1MB on gVisor at the time
// of writing this comment), thus testing for cases when the
// scaled receive window size ends up > 65535 (0xffff).
if !readOnce {
if got := dut.Recv(t, acceptFd, int32(payloadLen), 0); len(got) != payloadLen {
t.Fatalf("got dut.Recv(t, %d, %d, 0) = %d, want %d", acceptFd, payloadLen, len(got), payloadLen)
}
readOnce = true
}
windowSize := *gotTCP.WindowSize
t.Logf("got window size = %d", windowSize)
if windowSize == 0 {
break
}
if payloadLen > int(windowSize) {
payloadLen = int(windowSize)
}
}
}
func TestZeroToNonZeroWindowUpdate(t *testing.T) {
dut := testbench.NewDUT(t)
listenFd, remotePort := dut.CreateListener(t, unix.SOCK_STREAM, unix.IPPROTO_TCP, 1)
defer dut.Close(t, listenFd)
conn := dut.Net.NewTCPIPv4(t, testbench.TCP{DstPort: &remotePort}, testbench.TCP{SrcPort: &remotePort})
defer conn.Close(t)
conn.Send(t, testbench.TCP{Flags: testbench.TCPFlags(header.TCPFlagSyn)})
synAck, err := conn.Expect(t, testbench.TCP{Flags: testbench.TCPFlags(header.TCPFlagSyn | header.TCPFlagAck)}, time.Second)
if err != nil {
t.Fatalf("didn't get synack during handshake: %s", err)
}
conn.Send(t, testbench.TCP{Flags: testbench.TCPFlags(header.TCPFlagAck)})
acceptFd, _ := dut.Accept(t, listenFd)
defer dut.Close(t, acceptFd)
dut.SetSockOptInt(t, acceptFd, unix.IPPROTO_TCP, unix.TCP_NODELAY, 1)
mss := header.ParseSynOptions(synAck.Options, true).MSS
fillRecvBuffer(t, &conn, &dut, acceptFd, int(mss))
// Read < mss worth of data from the receive buffer and expect the DUT to
// not send a non-zero window update.
payloadLen := mss - 1
if got := dut.Recv(t, acceptFd, int32(payloadLen), 0); len(got) != int(payloadLen) {
t.Fatalf("got dut.Recv(t, %d, %d, 0) = %d, want %d", acceptFd, payloadLen, len(got), payloadLen)
}
// Send a zero-window-probe to force an ACK from the receiver with any
// window updates.
conn.Send(t, testbench.TCP{SeqNum: testbench.Uint32(uint32(*conn.LocalSeqNum(t) - 1)), Flags: testbench.TCPFlags(header.TCPFlagAck)})
gotTCP, err := conn.Expect(t, testbench.TCP{Flags: testbench.TCPFlags(header.TCPFlagAck)}, time.Second)
if err != nil {
t.Fatalf("expected packet was not received: %s", err)
}
if windowSize := *gotTCP.WindowSize; windowSize != 0 {
t.Fatalf("got non zero window = %d", windowSize)
}
// Now, ensure that the DUT eventually sends non-zero window update.
seqNum := testbench.Uint32(uint32(*conn.LocalSeqNum(t) - 1))
ackNum := testbench.Uint32(uint32(*conn.LocalSeqNum(t)))
recvCheckWindowUpdate := func(readLen int) uint16 {
if got := dut.Recv(t, acceptFd, int32(readLen), 0); len(got) != readLen {
t.Fatalf("got dut.Recv(t, %d, %d, 0) = %d, want %d", acceptFd, readLen, len(got), readLen)
}
conn.Send(t, testbench.TCP{SeqNum: seqNum, Flags: testbench.TCPFlags(header.TCPFlagPsh | header.TCPFlagAck)}, &testbench.Payload{Bytes: make([]byte, 1)})
gotTCP, err := conn.Expect(t, testbench.TCP{AckNum: ackNum, Flags: testbench.TCPFlags(header.TCPFlagAck)}, time.Second)
if err != nil {
t.Fatalf("expected packet was not received: %s", err)
}
return *gotTCP.WindowSize
}
if !dut.Uname.IsLinux() {
if win := recvCheckWindowUpdate(1); win == 0 {
t.Fatal("expected non-zero window update")
}
} else {
// Linux stack takes additional socket reads to send out window update,
// its a function of sysctl_tcp_rmem among other things.
// https://github.com/torvalds/linux/blob/7acac4b3196/net/ipv4/tcp_input.c#L687
for {
if win := recvCheckWindowUpdate(int(payloadLen)); win != 0 {
break
}
}
}
}
// TestNonZeroReceiveWindow tests for the DUT to never send a zero receive
// window when the data is being read from the socket buffer.
func TestNonZeroReceiveWindow(t *testing.T) {