Skip to content

Commit

Permalink
Ignore RST received for a TCP listener
Browse files Browse the repository at this point in the history
The current implementation has a bug where TCP listener does not ignore
RSTs from the peer. While handling RST+ACK from the peer, this bug can
complete handshakes that use syncookies. This results in half-open
connection delivered to the accept queue.

Fixes google#6076

PiperOrigin-RevId: 376868749
  • Loading branch information
iyermi authored and gvisor-bot committed Jun 1, 2021
1 parent 4f37469 commit 77dc0f5
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 32 deletions.
7 changes: 6 additions & 1 deletion pkg/tcpip/transport/tcp/accept.go
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,10 @@ func (e *endpoint) handleListenSegment(ctx *listenContext, s *segment) tcpip.Err
}

switch {
case s.flags.Contains(header.TCPFlagRst):
e.stack.Stats().DroppedPackets.Increment()
return nil

case s.flags == header.TCPFlagSyn:
if e.acceptQueueIsFull() {
e.stack.Stats().TCP.ListenOverflowSynDrop.Increment()
Expand Down Expand Up @@ -611,7 +615,7 @@ func (e *endpoint) handleListenSegment(ctx *listenContext, s *segment) tcpip.Err
e.stack.Stats().TCP.ListenOverflowSynCookieSent.Increment()
return nil

case (s.flags & header.TCPFlagAck) != 0:
case s.flags.Contains(header.TCPFlagAck):
if e.acceptQueueIsFull() {
// Silently drop the ack as the application can't accept
// the connection at this point. The ack will be
Expand Down Expand Up @@ -753,6 +757,7 @@ func (e *endpoint) handleListenSegment(ctx *listenContext, s *segment) tcpip.Err
return nil

default:
e.stack.Stats().DroppedPackets.Increment()
return nil
}
}
Expand Down
48 changes: 48 additions & 0 deletions pkg/tcpip/transport/tcp/tcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6238,6 +6238,54 @@ func TestPassiveFailedConnectionAttemptIncrement(t *testing.T) {
}
}

func TestListenDropIncrement(t *testing.T) {
c := context.New(t, defaultMTU)
defer c.Cleanup()

stats := c.Stack().Stats()
c.Create(-1 /*epRcvBuf*/)

if err := c.EP.Bind(tcpip.FullAddress{Addr: context.StackAddr, Port: context.StackPort}); err != nil {
t.Fatalf("Bind failed: %s", err)
}
if err := c.EP.Listen(1 /*backlog*/); err != nil {
t.Fatalf("Listen failed: %s", err)
}

initialDropped := stats.DroppedPackets.Value()

// Send RST, FIN segments, that are expected to be dropped by the listener.
c.SendPacket(nil, &context.Headers{
SrcPort: context.TestPort,
DstPort: context.StackPort,
Flags: header.TCPFlagRst,
})
c.SendPacket(nil, &context.Headers{
SrcPort: context.TestPort,
DstPort: context.StackPort,
Flags: header.TCPFlagFin,
})

// To ensure that the RST, FIN sent earlier are indeed received and ignored
// by the listener, send a SYN and wait for the SYN to be ACKd.
irs := seqnum.Value(context.TestInitialSequenceNumber)
c.SendPacket(nil, &context.Headers{
SrcPort: context.TestPort,
DstPort: context.StackPort,
Flags: header.TCPFlagSyn,
SeqNum: irs,
})
checker.IPv4(t, c.GetPacket(), checker.TCP(checker.SrcPort(context.StackPort),
checker.DstPort(context.TestPort),
checker.TCPFlags(header.TCPFlagAck|header.TCPFlagSyn),
checker.TCPAckNum(uint32(irs)+1),
))

if got, want := stats.DroppedPackets.Value(), initialDropped+2; got != want {
t.Fatalf("got stats.DroppedPackets.Value() = %d, want = %d", got, want)
}
}

func TestEndpointBindListenAcceptState(t *testing.T) {
c := context.New(t, defaultMTU)
defer c.Cleanup()
Expand Down
127 changes: 96 additions & 31 deletions test/packetimpact/tests/tcp_syncookie_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ package tcp_syncookie_test

import (
"flag"
"fmt"
"math"
"testing"
"time"

Expand All @@ -28,43 +30,106 @@ func init() {
testbench.Initialize(flag.CommandLine)
}

// TestSynCookie test if the DUT listener is replying back using syn cookies.
// The test does not complete the handshake by not sending the ACK to SYNACK.
// When syncookies are not used, this forces the listener to retransmit SYNACK.
// And when syncookies are being used, there is no such retransmit.
// TestTCPSynCookie tests for ACK handling for connections in SYNRCVD state
// connections with and without syncookies. It verifies if the passive open
// connection is indeed using syncookies before proceeding.
func TestTCPSynCookie(t *testing.T) {
dut := testbench.NewDUT(t)
for _, tt := range []struct {
accept bool
flags header.TCPFlags
}{
{accept: true, flags: header.TCPFlagAck},
{accept: true, flags: header.TCPFlagAck | header.TCPFlagPsh},
{accept: false, flags: header.TCPFlagAck | header.TCPFlagSyn},
{accept: true, flags: header.TCPFlagAck | header.TCPFlagFin},
{accept: false, flags: header.TCPFlagAck | header.TCPFlagRst},
{accept: false, flags: header.TCPFlagRst},
} {
t.Run(fmt.Sprintf("flags=%s", tt.flags), func(t *testing.T) {
// Make a copy before parallelizing the test and refer to that
// within the test. Otherwise, the test reference could be pointing
// to an incorrect variant based on how it is scheduled.
test := tt

// Listening endpoint accepts one more connection than the listen backlog.
_, remotePort := dut.CreateListener(t, unix.SOCK_STREAM, unix.IPPROTO_TCP, 1 /*backlog*/)
t.Parallel()

var withoutSynCookieConn testbench.TCPIPv4
var withSynCookieConn testbench.TCPIPv4
// Listening endpoint accepts one more connection than the listen
// backlog. Listener starts using syncookies when it sees a new SYN
// and has backlog size of connections in SYNRCVD state. Keep the
// listen backlog 1, so that the test can define 2 connections
// without and with using syncookies.
listenFD, remotePort := dut.CreateListener(t, unix.SOCK_STREAM, unix.IPPROTO_TCP, 1 /*backlog*/)
defer dut.Close(t, listenFD)

// Test if the DUT listener replies to more SYNs than listen backlog+1
for _, conn := range []*testbench.TCPIPv4{&withoutSynCookieConn, &withSynCookieConn} {
*conn = dut.Net.NewTCPIPv4(t, testbench.TCP{DstPort: &remotePort}, testbench.TCP{SrcPort: &remotePort})
}
defer withoutSynCookieConn.Close(t)
defer withSynCookieConn.Close(t)
var withoutSynCookieConn testbench.TCPIPv4
var withSynCookieConn testbench.TCPIPv4

checkSynAck := func(t *testing.T, conn *testbench.TCPIPv4, expectRetransmit bool) {
// Expect dut connection to have transitioned to SYN-RCVD state.
conn.Send(t, testbench.TCP{Flags: testbench.TCPFlags(header.TCPFlagSyn)})
if _, err := conn.ExpectData(t, &testbench.TCP{Flags: testbench.TCPFlags(header.TCPFlagSyn | header.TCPFlagAck)}, nil, time.Second); err != nil {
t.Fatalf("expected SYN-ACK, but got %s", err)
}
for _, conn := range []*testbench.TCPIPv4{&withoutSynCookieConn, &withSynCookieConn} {
*conn = dut.Net.NewTCPIPv4(t, testbench.TCP{DstPort: &remotePort}, testbench.TCP{SrcPort: &remotePort})
}
defer withoutSynCookieConn.Close(t)
defer withSynCookieConn.Close(t)

// If the DUT listener is using syn cookies, it will not retransmit SYNACK
got, err := conn.ExpectData(t, &testbench.TCP{SeqNum: testbench.Uint32(uint32(*conn.RemoteSeqNum(t) - 1)), Flags: testbench.TCPFlags(header.TCPFlagSyn | header.TCPFlagAck)}, nil, 2*time.Second)
if expectRetransmit && err != nil {
t.Fatalf("expected retransmitted SYN-ACK, but got %s", err)
}
if !expectRetransmit && err == nil {
t.Fatalf("expected no retransmitted SYN-ACK, but got %s", got)
}
}
// Setup the 2 connections in SYNRCVD state and verify if one of the
// connection is indeed using syncookies by checking for absence of
// SYNACK retransmits.
for _, c := range []struct {
desc string
conn *testbench.TCPIPv4
expectRetransmit bool
}{
{desc: "without syncookies", conn: &withoutSynCookieConn, expectRetransmit: true},
{desc: "with syncookies", conn: &withSynCookieConn, expectRetransmit: false},
} {
t.Run(c.desc, func(t *testing.T) {
// Expect dut connection to have transitioned to SYNRCVD state.
c.conn.Send(t, testbench.TCP{Flags: testbench.TCPFlags(header.TCPFlagSyn)})
if _, err := c.conn.ExpectData(t, &testbench.TCP{Flags: testbench.TCPFlags(header.TCPFlagSyn | header.TCPFlagAck)}, nil, time.Second); err != nil {
t.Fatalf("expected SYNACK, but got %s", err)
}

// If the DUT listener is using syn cookies, it will not retransmit SYNACK.
got, err := c.conn.ExpectData(t, &testbench.TCP{SeqNum: testbench.Uint32(uint32(*c.conn.RemoteSeqNum(t) - 1)), Flags: testbench.TCPFlags(header.TCPFlagSyn | header.TCPFlagAck)}, nil, 2*time.Second)
if c.expectRetransmit && err != nil {
t.Fatalf("expected retransmitted SYNACK, but got %s", err)
}
if !c.expectRetransmit && err == nil {
t.Fatalf("expected no retransmitted SYNACK, but got %s", got)
}
})
}

t.Run("without syncookies", func(t *testing.T) { checkSynAck(t, &withoutSynCookieConn, true /*expectRetransmit*/) })
t.Run("with syncookies", func(t *testing.T) { checkSynAck(t, &withSynCookieConn, false /*expectRetransmit*/) })
// Check whether ACKs with the given flags completes the handshake.
for _, c := range []struct {
desc string
conn *testbench.TCPIPv4
}{
{desc: "with syncookies", conn: &withSynCookieConn},
{desc: "without syncookies", conn: &withoutSynCookieConn},
} {
t.Run(c.desc, func(t *testing.T) {
pfds := dut.Poll(t, []unix.PollFd{{Fd: listenFD, Events: math.MaxInt16}}, 0 /*timeout*/)
if got, want := len(pfds), 0; got != want {
t.Fatalf("dut.Poll(...) = %d, want = %d", got, want)
}

c.conn.Send(t, testbench.TCP{Flags: testbench.TCPFlags(test.flags)})
pfds = dut.Poll(t, []unix.PollFd{{Fd: listenFD, Events: unix.POLLIN}}, time.Second)
want := 0
if test.accept {
want = 1
}
if got := len(pfds); got != want {
t.Fatalf("got dut.Poll(...) = %d, want = %d", got, want)
}
// Accept the connection to enable poll on any subsequent connection.
if test.accept {
fd, _ := dut.Accept(t, listenFD)
dut.Close(t, fd)
}
})
}
})
}
}

0 comments on commit 77dc0f5

Please sign in to comment.