Fix TCP segment retransmit timeout handling.

As per RFC 1122 and Linux retransmit timeout handling:
- The segment retransmit timeout needs to exponentially increase and
  cap at a predefined value.
- TCP connection needs to timeout after a predefined number of
  segment retransmissions.
- TCP connection should not timeout when the retranmission timeout
  exceeds MaxRTO, predefined upper bound.

Fixes #2673

PiperOrigin-RevId: 311463961
This commit is contained in:
Mithun Iyer
2020-05-13 21:26:54 -07:00
committed by gVisor bot
parent 8b8774d715
commit f1ad2d54ab
6 changed files with 284 additions and 22 deletions
+8
View File
@@ -698,6 +698,14 @@ type TCPDeferAcceptOption time.Duration
// default MinRTO used by the Stack.
type TCPMinRTOOption time.Duration
// TCPMaxRTOOption is use by SetSockOpt/GetSockOpt to allow overriding
// default MaxRTO used by the Stack.
type TCPMaxRTOOption time.Duration
// TCPMaxRetriesOption is used by SetSockOpt/GetSockOpt to set/get the
// maximum number of retransmits after which we time out the connection.
type TCPMaxRetriesOption uint64
// TCPSynRcvdCountThresholdOption is used by SetSockOpt/GetSockOpt to specify
// the number of endpoints that can be in SYN-RCVD state before the stack
// switches to using SYN cookies.
+31
View File
@@ -167,6 +167,8 @@ type protocol struct {
tcpLingerTimeout time.Duration
tcpTimeWaitTimeout time.Duration
minRTO time.Duration
maxRTO time.Duration
maxRetries uint32
synRcvdCount synRcvdCounter
synRetries uint8
dispatcher *dispatcher
@@ -345,6 +347,21 @@ func (p *protocol) SetOption(option interface{}) *tcpip.Error {
p.mu.Unlock()
return nil
case tcpip.TCPMaxRTOOption:
if v < 0 {
v = tcpip.TCPMaxRTOOption(MaxRTO)
}
p.mu.Lock()
p.maxRTO = time.Duration(v)
p.mu.Unlock()
return nil
case tcpip.TCPMaxRetriesOption:
p.mu.Lock()
p.maxRetries = uint32(v)
p.mu.Unlock()
return nil
case tcpip.TCPSynRcvdCountThresholdOption:
p.mu.Lock()
p.synRcvdCount.SetThreshold(uint64(v))
@@ -428,6 +445,18 @@ func (p *protocol) Option(option interface{}) *tcpip.Error {
p.mu.RUnlock()
return nil
case *tcpip.TCPMaxRTOOption:
p.mu.RLock()
*v = tcpip.TCPMaxRTOOption(p.maxRTO)
p.mu.RUnlock()
return nil
case *tcpip.TCPMaxRetriesOption:
p.mu.RLock()
*v = tcpip.TCPMaxRetriesOption(p.maxRetries)
p.mu.RUnlock()
return nil
case *tcpip.TCPSynRcvdCountThresholdOption:
p.mu.RLock()
*v = tcpip.TCPSynRcvdCountThresholdOption(p.synRcvdCount.Threshold())
@@ -474,5 +503,7 @@ func NewProtocol() stack.TransportProtocol {
dispatcher: newDispatcher(runtime.GOMAXPROCS(0)),
synRetries: DefaultSynRetries,
minRTO: MinRTO,
maxRTO: MaxRTO,
maxRetries: MaxRetries,
}
}
+41 -18
View File
@@ -43,7 +43,8 @@ const (
nDupAckThreshold = 3
// MaxRetries is the maximum number of probe retries sender does
// before timing out the connection, Linux default TCP_RETR2.
// before timing out the connection.
// Linux default TCP_RETR2, net.ipv4.tcp_retries2.
MaxRetries = 15
)
@@ -165,6 +166,12 @@ type sender struct {
// minRTO is the minimum permitted value for sender.rto.
minRTO time.Duration
// maxRTO is the maximum permitted value for sender.rto.
maxRTO time.Duration
// maxRetries is the maximum permitted retransmissions.
maxRetries uint32
// maxPayloadSize is the maximum size of the payload of a given segment.
// It is initialized on demand.
maxPayloadSize int
@@ -276,12 +283,24 @@ func newSender(ep *endpoint, iss, irs seqnum.Value, sndWnd seqnum.Size, mss uint
// etc.
s.ep.scoreboard = NewSACKScoreboard(uint16(s.maxPayloadSize), iss)
// Get Stack wide minRTO.
var v tcpip.TCPMinRTOOption
if err := ep.stack.TransportProtocolOption(ProtocolNumber, &v); err != nil {
// Get Stack wide config.
var minRTO tcpip.TCPMinRTOOption
if err := ep.stack.TransportProtocolOption(ProtocolNumber, &minRTO); err != nil {
panic(fmt.Sprintf("unable to get minRTO from stack: %s", err))
}
s.minRTO = time.Duration(v)
s.minRTO = time.Duration(minRTO)
var maxRTO tcpip.TCPMaxRTOOption
if err := ep.stack.TransportProtocolOption(ProtocolNumber, &maxRTO); err != nil {
panic(fmt.Sprintf("unable to get maxRTO from stack: %s", err))
}
s.maxRTO = time.Duration(maxRTO)
var maxRetries tcpip.TCPMaxRetriesOption
if err := ep.stack.TransportProtocolOption(ProtocolNumber, &maxRetries); err != nil {
panic(fmt.Sprintf("unable to get maxRetries from stack: %s", err))
}
s.maxRetries = uint32(maxRetries)
return s
}
@@ -485,7 +504,7 @@ func (s *sender) retransmitTimerExpired() bool {
}
elapsed := time.Since(s.firstRetransmittedSegXmitTime)
remaining := MaxRTO
remaining := s.maxRTO
if uto != 0 {
// Cap to the user specified timeout if one is specified.
remaining = uto - elapsed
@@ -494,24 +513,17 @@ func (s *sender) retransmitTimerExpired() bool {
// Always honor the user-timeout irrespective of whether the zero
// window probes were acknowledged.
// net/ipv4/tcp_timer.c::tcp_probe_timer()
if remaining <= 0 || s.unackZeroWindowProbes >= MaxRetries {
if remaining <= 0 || s.unackZeroWindowProbes >= s.maxRetries {
return false
}
if s.rto >= MaxRTO {
// RFC 1122 section: 4.2.2.17
// A TCP MAY keep its offered receive window closed
// indefinitely. As long as the receiving TCP continues to
// send acknowledgments in response to the probe segments, the
// sending TCP MUST allow the connection to stay open.
if !(s.zeroWindowProbing && s.unackZeroWindowProbes == 0) {
return false
}
}
// Set new timeout. The timer will be restarted by the call to sendData
// below.
s.rto *= 2
// Cap the RTO as per RFC 1122 4.2.3.1, RFC 6298 5.5
if s.rto > s.maxRTO {
s.rto = s.maxRTO
}
// Cap RTO to remaining time.
if s.rto > remaining {
@@ -565,9 +577,20 @@ func (s *sender) retransmitTimerExpired() bool {
// send.
if s.zeroWindowProbing {
s.sendZeroWindowProbe()
// RFC 1122 4.2.2.17: A TCP MAY keep its offered receive window closed
// indefinitely. As long as the receiving TCP continues to send
// acknowledgments in response to the probe segments, the sending TCP
// MUST allow the connection to stay open.
return true
}
seg := s.writeNext
// RFC 1122 4.2.3.5: Close the connection when the number of
// retransmissions for this segment is beyond a limit.
if seg != nil && seg.xmitCount > s.maxRetries {
return false
}
s.sendData()
return true
+110 -4
View File
@@ -2994,6 +2994,101 @@ func TestSendOnResetConnection(t *testing.T) {
}
}
// TestMaxRetransmitsTimeout tests if the connection is timed out after
// a segment has been retransmitted MaxRetries times.
func TestMaxRetransmitsTimeout(t *testing.T) {
c := context.New(t, defaultMTU)
defer c.Cleanup()
const numRetries = 2
if err := c.Stack().SetTransportProtocolOption(tcp.ProtocolNumber, tcpip.TCPMaxRetriesOption(numRetries)); err != nil {
t.Fatalf("could not set protocol option MaxRetries.\n")
}
c.CreateConnected(789 /* iss */, 30000 /* rcvWnd */, -1 /* epRcvBuf */)
waitEntry, notifyCh := waiter.NewChannelEntry(nil)
c.WQ.EventRegister(&waitEntry, waiter.EventHUp)
defer c.WQ.EventUnregister(&waitEntry)
_, _, err := c.EP.Write(tcpip.SlicePayload(buffer.NewView(1)), tcpip.WriteOptions{})
if err != nil {
t.Fatalf("Write failed: %v", err)
}
// Expect first transmit and MaxRetries retransmits.
for i := 0; i < numRetries+1; i++ {
checker.IPv4(t, c.GetPacket(),
checker.TCP(
checker.DstPort(context.TestPort),
checker.TCPFlags(header.TCPFlagAck|header.TCPFlagPsh),
),
)
}
// Wait for the connection to timeout after MaxRetries retransmits.
initRTO := 1 * time.Second
select {
case <-notifyCh:
case <-time.After((2 << numRetries) * initRTO):
t.Fatalf("connection still alive after maximum retransmits.\n")
}
// Send an ACK and expect a RST as the connection would have been closed.
c.SendPacket(nil, &context.Headers{
SrcPort: context.TestPort,
DstPort: c.Port,
Flags: header.TCPFlagAck,
})
checker.IPv4(t, c.GetPacket(),
checker.TCP(
checker.DstPort(context.TestPort),
checker.TCPFlags(header.TCPFlagRst),
),
)
if got := c.Stack().Stats().TCP.EstablishedTimedout.Value(); got != 1 {
t.Errorf("got c.Stack().Stats().TCP.EstablishedTimedout.Value() = %v, want = 1", got)
}
}
// TestMaxRTO tests if the retransmit interval caps to MaxRTO.
func TestMaxRTO(t *testing.T) {
c := context.New(t, defaultMTU)
defer c.Cleanup()
rto := 1 * time.Second
if err := c.Stack().SetTransportProtocolOption(tcp.ProtocolNumber, tcpip.TCPMaxRTOOption(rto)); err != nil {
t.Fatalf("c.stack.SetTransportProtocolOption(tcp, tcpip.TCPMaxRTO(%d) failed: %s", rto, err)
}
c.CreateConnected(789 /* iss */, 30000 /* rcvWnd */, -1 /* epRcvBuf */)
_, _, err := c.EP.Write(tcpip.SlicePayload(buffer.NewView(1)), tcpip.WriteOptions{})
if err != nil {
t.Fatalf("Write failed: %v", err)
}
checker.IPv4(t, c.GetPacket(),
checker.TCP(
checker.DstPort(context.TestPort),
checker.TCPFlagsMatch(header.TCPFlagAck, ^uint8(header.TCPFlagPsh)),
),
)
const numRetransmits = 2
for i := 0; i < numRetransmits; i++ {
start := time.Now()
checker.IPv4(t, c.GetPacket(),
checker.TCP(
checker.DstPort(context.TestPort),
checker.TCPFlagsMatch(header.TCPFlagAck, ^uint8(header.TCPFlagPsh)),
),
)
if time.Since(start).Round(time.Second).Seconds() != rto.Seconds() {
t.Errorf("Retransmit interval not capped to MaxRTO.\n")
}
}
}
func TestFinImmediately(t *testing.T) {
c := context.New(t, defaultMTU)
defer c.Cleanup()
@@ -6605,9 +6700,16 @@ func TestTCPUserTimeout(t *testing.T) {
c.CreateConnected(789, 30000, -1 /* epRcvBuf */)
waitEntry, notifyCh := waiter.NewChannelEntry(nil)
c.WQ.EventRegister(&waitEntry, waiter.EventHUp)
defer c.WQ.EventUnregister(&waitEntry)
origEstablishedTimedout := c.Stack().Stats().TCP.EstablishedTimedout.Value()
userTimeout := 50 * time.Millisecond
// Ensure that on the next retransmit timer fire, the user timeout has
// expired.
initRTO := 1 * time.Second
userTimeout := initRTO / 2
c.EP.SetSockOpt(tcpip.TCPUserTimeoutOption(userTimeout))
// Send some data and wait before ACKing it.
@@ -6627,9 +6729,13 @@ func TestTCPUserTimeout(t *testing.T) {
),
)
// Wait for a little over the minimum retransmit timeout of 200ms for
// the retransmitTimer to fire and close the connection.
time.Sleep(tcp.MinRTO + 10*time.Millisecond)
// Wait for the retransmit timer to be fired and the user timeout to cause
// close of the connection.
select {
case <-notifyCh:
case <-time.After(2 * initRTO):
t.Fatalf("connection still alive after %s, should have been closed after :%s", 2*initRTO, userTimeout)
}
// No packet should be received as the connection should be silently
// closed due to timeout.
+10
View File
@@ -81,6 +81,16 @@ packetimpact_go_test(
],
)
packetimpact_go_test(
name = "tcp_retransmits",
srcs = ["tcp_retransmits_test.go"],
deps = [
"//pkg/tcpip/header",
"//test/packetimpact/testbench",
"@org_golang_x_sys//unix:go_default_library",
],
)
packetimpact_go_test(
name = "tcp_outside_the_window",
srcs = ["tcp_outside_the_window_test.go"],
@@ -0,0 +1,84 @@
// 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_retransmits_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)
}
// TestRetransmits tests retransmits occur at exponentially increasing
// time intervals.
func TestRetransmits(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}, 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")
samplePayload := &tb.Payload{Bytes: sampleData}
dut.Send(acceptFd, sampleData, 0)
if _, err := conn.ExpectData(&tb.TCP{}, samplePayload, time.Second); err != nil {
t.Fatalf("expected a packet with payload %v: %s", samplePayload, err)
}
// Give a chance for the dut to estimate RTO with RTT from the DATA-ACK.
// TODO(gvisor.dev/issue/2685) Estimate RTO during handshake, after which
// we can skip sending this ACK.
conn.Send(tb.TCP{Flags: tb.Uint8(header.TCPFlagAck)})
startRTO := time.Second
current := startRTO
first := time.Now()
dut.Send(acceptFd, sampleData, 0)
seq := tb.Uint32(uint32(*conn.RemoteSeqNum()))
if _, err := conn.ExpectData(&tb.TCP{SeqNum: seq}, samplePayload, startRTO); err != nil {
t.Fatalf("expected a packet with payload %v: %s", samplePayload, err)
}
// Expect retransmits of the same segment.
for i := 0; i < 5; i++ {
start := time.Now()
if _, err := conn.ExpectData(&tb.TCP{SeqNum: seq}, samplePayload, 2*current); err != nil {
t.Fatalf("expected a packet with payload %v: %s loop %d", samplePayload, err, i)
}
if i == 0 {
startRTO = time.Now().Sub(first)
current = 2 * startRTO
continue
}
// Check if the probes came at exponentially increasing intervals.
if p := time.Since(start); p < current-startRTO {
t.Fatalf("retransmit came sooner interval %d probe %d\n", p, i)
}
current *= 2
}
}