mirror of
https://github.com/netbirdio/gvisor.git
synced 2026-05-22 17:12:49 -07:00
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:
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user