Avoid TCP segment split when out of sender window.

If the entire segment cannot be accommodated in the receiver advertised
window and if there are still unacknowledged pending segments, skip
splitting the segment. The segment transmit would get retried by the
retransmit handler.

PiperOrigin-RevId: 314538523
This commit is contained in:
Mithun Iyer 2020-06-03 08:48:23 -07:00 committed by gVisor bot
parent e6334e81ca
commit 162848e129
4 changed files with 126 additions and 68 deletions

View File

@ -816,6 +816,25 @@ func (s *sender) maybeSendSegment(seg *segment, limit int, end seqnum.Value) (se
panic("Netstack queues FIN segments without data.")
}
segEnd = seg.sequenceNumber.Add(seqnum.Size(seg.data.Size()))
// If the entire segment cannot be accomodated in the receiver
// advertized window, skip splitting and sending of the segment.
// ref: net/ipv4/tcp_output.c::tcp_snd_wnd_test()
//
// Linux checks this for all segment transmits not triggered
// by a probe timer. On this condition, it defers the segment
// split and transmit to a short probe timer.
// ref: include/net/tcp.h::tcp_check_probe_timer()
// ref: net/ipv4/tcp_output.c::tcp_write_wakeup()
//
// Instead of defining a new transmit timer, we attempt to split the
// segment right here if there are no pending segments.
// If there are pending segments, segment transmits are deferred
// to the retransmit timer handler.
if s.sndUna != s.sndNxt && !segEnd.LessThan(end) {
return false
}
if !seg.sequenceNumber.LessThan(end) {
return false
}

View File

@ -124,10 +124,8 @@ packetimpact_go_test(
)
packetimpact_go_test(
name = "tcp_should_piggyback",
srcs = ["tcp_should_piggyback_test.go"],
# TODO(b/153680566): Fix netstack then remove the line below.
expect_netstack_failure = True,
name = "tcp_send_window_sizes_piggyback",
srcs = ["tcp_send_window_sizes_piggyback_test.go"],
deps = [
"//pkg/tcpip/header",
"//test/packetimpact/testbench",

View File

@ -0,0 +1,105 @@
// Copyright 2020 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tcp_send_window_sizes_piggyback_test
import (
"flag"
"fmt"
"testing"
"time"
"golang.org/x/sys/unix"
"gvisor.dev/gvisor/pkg/tcpip/header"
tb "gvisor.dev/gvisor/test/packetimpact/testbench"
)
func init() {
tb.RegisterFlags(flag.CommandLine)
}
// TestSendWindowSizesPiggyback tests cases where segment sizes are close to
// sender window size and checks for ACK piggybacking for each of those case.
func TestSendWindowSizesPiggyback(t *testing.T) {
sampleData := []byte("Sample Data")
segmentSize := uint16(len(sampleData))
// Advertise receive window sizes that are lesser, equal to or greater than
// enqueued segment size and check for segment transmits. The test attempts
// to enqueue a segment on the dut before acknowledging previous segment and
// lets the dut piggyback any ACKs along with the enqueued segment.
for _, tt := range []struct {
description string
windowSize uint16
expectedPayload1 []byte
expectedPayload2 []byte
enqueue bool
}{
// Expect the first segment to be split as it cannot be accomodated in
// the sender window. This means we need not enqueue a new segment after
// the first segment.
{"WindowSmallerThanSegment", segmentSize - 1, sampleData[:(segmentSize - 1)], sampleData[(segmentSize - 1):], false /* enqueue */},
{"WindowEqualToSegment", segmentSize, sampleData, sampleData, true /* enqueue */},
// Expect the second segment to not be split as its size is greater than
// the available sender window size. The segments should not be split
// when there is pending unacknowledged data and the segment-size is
// greater than available sender window.
{"WindowGreaterThanSegment", segmentSize + 1, sampleData, sampleData, true /* enqueue */},
} {
t.Run(fmt.Sprintf("%s%d", tt.description, tt.windowSize), func(t *testing.T) {
dut := tb.NewDUT(t)
defer dut.TearDown()
listenFd, remotePort := dut.CreateListener(unix.SOCK_STREAM, unix.IPPROTO_TCP, 1)
defer dut.Close(listenFd)
conn := tb.NewTCPIPv4(t, tb.TCP{DstPort: &remotePort, WindowSize: tb.Uint16(tt.windowSize)}, tb.TCP{SrcPort: &remotePort})
defer conn.Close()
conn.Handshake()
acceptFd, _ := dut.Accept(listenFd)
defer dut.Close(acceptFd)
dut.SetSockOptInt(acceptFd, unix.IPPROTO_TCP, unix.TCP_NODELAY, 1)
expectedTCP := tb.TCP{Flags: tb.Uint8(header.TCPFlagAck | header.TCPFlagPsh)}
dut.Send(acceptFd, sampleData, 0)
expectedPayload := tb.Payload{Bytes: tt.expectedPayload1}
if _, err := conn.ExpectData(&expectedTCP, &expectedPayload, time.Second); err != nil {
t.Fatalf("Expected %s but didn't get one: %s", tb.Layers{&expectedTCP, &expectedPayload}, err)
}
// Expect any enqueued segment to be transmitted by the dut along with
// piggybacked ACK for our data.
if tt.enqueue {
// Enqueue a segment for the dut to transmit.
dut.Send(acceptFd, sampleData, 0)
}
// Send ACK for the previous segment along with data for the dut to
// receive and ACK back. Sending this ACK would make room for the dut
// to transmit any enqueued segment.
conn.Send(tb.TCP{Flags: tb.Uint8(header.TCPFlagAck | header.TCPFlagPsh), WindowSize: tb.Uint16(tt.windowSize)}, &tb.Payload{Bytes: sampleData})
// Expect the dut to piggyback the ACK for received data along with
// the segment enqueued for transmit.
expectedPayload = tb.Payload{Bytes: tt.expectedPayload2}
if _, err := conn.ExpectData(&expectedTCP, &expectedPayload, time.Second); err != nil {
t.Fatalf("Expected %s but didn't get one: %s", tb.Layers{&expectedTCP, &expectedPayload}, err)
}
})
}
}

View File

@ -1,64 +0,0 @@
// Copyright 2020 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tcp_should_piggyback_test
import (
"flag"
"testing"
"time"
"golang.org/x/sys/unix"
"gvisor.dev/gvisor/pkg/tcpip/header"
tb "gvisor.dev/gvisor/test/packetimpact/testbench"
)
func init() {
tb.RegisterFlags(flag.CommandLine)
}
func TestPiggyback(t *testing.T) {
dut := tb.NewDUT(t)
defer dut.TearDown()
listenFd, remotePort := dut.CreateListener(unix.SOCK_STREAM, unix.IPPROTO_TCP, 1)
defer dut.Close(listenFd)
conn := tb.NewTCPIPv4(t, tb.TCP{DstPort: &remotePort, WindowSize: tb.Uint16(12)}, tb.TCP{SrcPort: &remotePort})
defer conn.Close()
conn.Handshake()
acceptFd, _ := dut.Accept(listenFd)
defer dut.Close(acceptFd)
dut.SetSockOptInt(acceptFd, unix.IPPROTO_TCP, unix.TCP_NODELAY, 1)
sampleData := []byte("Sample Data")
dut.Send(acceptFd, sampleData, 0)
expectedTCP := tb.TCP{Flags: tb.Uint8(header.TCPFlagAck | header.TCPFlagPsh)}
expectedPayload := tb.Payload{Bytes: sampleData}
if _, err := conn.ExpectData(&expectedTCP, &expectedPayload, time.Second); err != nil {
t.Fatalf("Expected %v but didn't get one: %s", tb.Layers{&expectedTCP, &expectedPayload}, err)
}
// Cause DUT to send us more data as soon as we ACK their first data segment because we have
// a small window.
dut.Send(acceptFd, sampleData, 0)
// DUT should ACK our segment by piggybacking ACK to their outstanding data segment instead of
// sending a separate ACK packet.
conn.Send(expectedTCP, &expectedPayload)
if _, err := conn.ExpectData(&expectedTCP, &expectedPayload, time.Second); err != nil {
t.Fatalf("Expected %v but didn't get one: %s", tb.Layers{&expectedTCP, &expectedPayload}, err)
}
}