Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 53 additions & 37 deletions pkg/tcpip/transport/tcp/connect.go
Original file line number Diff line number Diff line change
Expand Up @@ -1178,46 +1178,62 @@ func (e *Endpoint) drainClosingSegmentQueue() {
}
}

// handleReset processes an inbound segment carrying the RST flag.
//
// Acceptance follows RFC 5961 section 3.2:
// - If the segment sequence number is out of window, the segment is
// silently dropped.
// - If the segment sequence number is in window but not exactly equal
// to RCV.NXT, the implementation sends a challenge ACK and drops
// the segment.
// - Only an exact match against RCV.NXT causes the connection to be
// reset.
//
// This is stricter than RFC 793 page 37, which accepted any in-window RST.
// The strict-match rule defends against off-path blind RST injection.
// Linux has implemented it since version 3.6 (2012); see
// net/ipv4/tcp_input.c tcp_validate_incoming(). Contribution to
// gvisor.dev/issues/1132.
//
// +checklocks:e.mu
func (e *Endpoint) handleReset(s *segment) (ok bool, err tcpip.Error) {
if e.rcv.acceptable(s.sequenceNumber, 0) {
// RFC 793, page 37 states that "in all states
// except SYN-SENT, all reset (RST) segments are
// validated by checking their SEQ-fields." So
// we only process it if it's acceptable.
switch e.EndpointState() {
// In case of a RST in CLOSE-WAIT linux moves
// the socket to closed state with an error set
// to indicate EPIPE.
//
// Technically this seems to be at odds w/ RFC.
// As per https://tools.ietf.org/html/rfc793#section-2.7
// page 69 the behavior for a segment arriving
// w/ RST bit set in CLOSE-WAIT is inlined below.
//
// ESTABLISHED
// FIN-WAIT-1
// FIN-WAIT-2
// CLOSE-WAIT

// If the RST bit is set then, any outstanding RECEIVEs and
// SEND should receive "reset" responses. All segment queues
// should be flushed. Users should also receive an unsolicited
// general "connection reset" signal. Enter the CLOSED state,
// delete the TCB, and return.
case StateCloseWait:
e.transitionToStateCloseLocked()
e.hardError = &tcpip.ErrAborted{}
return false, nil
default:
// RFC 793, page 37 states that "in all states
// except SYN-SENT, all reset (RST) segments are
// validated by checking their SEQ-fields." So
// we only process it if it's acceptable.
return false, &tcpip.ErrConnectionReset{}
}
if !e.rcv.acceptable(s.sequenceNumber, 0) {
// Out of window. Silent drop.
return true, nil
}

if s.sequenceNumber != e.rcv.RcvNxt {
// In window but not an exact match. Send a challenge ACK and drop the
// segment per RFC 5961 section 3.2. The challenge ACK helper rate-limits
// challenge transmission per RFC 5961 section 7.
e.snd.maybeSendOutOfWindowAck(s)
return true, nil
}

switch e.EndpointState() {
// In case of a RST in CLOSE-WAIT linux moves the socket to closed state
// with an error set to indicate EPIPE.
//
// As per https://tools.ietf.org/html/rfc793#section-2.7 page 69 the
// behavior for a segment arriving w/ RST bit set in CLOSE-WAIT is
// inlined below.
//
// ESTABLISHED
// FIN-WAIT-1
// FIN-WAIT-2
// CLOSE-WAIT
//
// If the RST bit is set then, any outstanding RECEIVEs and SEND should
// receive "reset" responses. All segment queues should be flushed.
// Users should also receive an unsolicited general "connection reset"
// signal. Enter the CLOSED state, delete the TCB, and return.
case StateCloseWait:
e.transitionToStateCloseLocked()
e.hardError = &tcpip.ErrAborted{}
return false, nil
default:
return false, &tcpip.ErrConnectionReset{}
}
return true, nil
}

// handleSegments processes all inbound segments.
Expand Down
15 changes: 15 additions & 0 deletions pkg/tcpip/transport/tcp/test/e2e/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,18 @@ go_test(
"//pkg/test/testutil",
],
)
go_test(
name = "handle_reset_rfc5961_test",
size = "small",
srcs = ["handle_reset_rfc5961_test.go"],
deps = [
":e2e",
"//pkg/tcpip",
"//pkg/tcpip/checker",
"//pkg/tcpip/header",
"//pkg/tcpip/seqnum",
"//pkg/tcpip/transport/tcp",
"//pkg/tcpip/transport/tcp/testing/context",
"//pkg/waiter",
],
)
153 changes: 153 additions & 0 deletions pkg/tcpip/transport/tcp/test/e2e/handle_reset_rfc5961_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// Copyright 2026 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 handle_reset_rfc5961_test

import (
"io"
"testing"
"time"

"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/checker"
"gvisor.dev/gvisor/pkg/tcpip/header"
"gvisor.dev/gvisor/pkg/tcpip/seqnum"
"gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
"gvisor.dev/gvisor/pkg/tcpip/transport/tcp/test/e2e"
"gvisor.dev/gvisor/pkg/waiter"
context "gvisor.dev/gvisor/pkg/tcpip/transport/tcp/testing/context"
)

// TestRFC5961_RSTOutOfWindowIsDropped verifies that an RST whose sequence
// number falls outside the advertised receive window is silently dropped.
// Pre-RFC-5961 behavior (RFC 793 page 37) already silently dropped these,
// so this asserts the unchanged path.
func TestRFC5961_RSTOutOfWindowIsDropped(t *testing.T) {
c := context.New(t, e2e.DefaultMTU)
defer c.Cleanup()

c.CreateConnected(context.TestInitialSequenceNumber, 30000, -1 /* epRcvBuf */)

// Send a RST far outside the receive window.
c.SendPacket(nil, &context.Headers{
SrcPort: context.TestPort,
DstPort: c.Port,
Flags: header.TCPFlagRst,
SeqNum: seqnum.Value(context.TestInitialSequenceNumber).Add(1 << 30),
RcvWnd: 30000,
})

// Allow the stack to process the packet, then assert no abort.
time.Sleep(50 * time.Millisecond)

// Read must NOT return ErrConnectionReset.
_, err := c.EP.Read(io.Discard, tcpip.ReadOptions{})
if _, ok := err.(*tcpip.ErrConnectionReset); ok {
t.Fatalf("connection aborted on out-of-window RST")
}

if got, want := tcp.EndpointState(c.EP.State()), tcp.StateEstablished; got != want {
t.Errorf("endpoint state = %v, want %v", got, want)
}
}

// TestRFC5961_RSTInWindowNotExactSendsChallengeAck verifies that an in-window
// RST whose sequence number does not exactly match RCV.NXT does NOT abort the
// connection and triggers a challenge ACK per RFC 5961 section 3.2. This is
// the security-relevant behavior change introduced by the patch: pre-patch,
// such a RST would have aborted the connection (RFC 793 window-test).
func TestRFC5961_RSTInWindowNotExactSendsChallengeAck(t *testing.T) {
c := context.New(t, e2e.DefaultMTU)
defer c.Cleanup()

c.CreateConnected(context.TestInitialSequenceNumber, 30000, -1 /* epRcvBuf */)

rcvNxt := seqnum.Value(context.TestInitialSequenceNumber).Add(1)
// Pick a sequence number inside the advertised window but not equal to RCV.NXT.
offSeq := rcvNxt.Add(1024)

c.SendPacket(nil, &context.Headers{
SrcPort: context.TestPort,
DstPort: c.Port,
Flags: header.TCPFlagRst,
SeqNum: offSeq,
RcvWnd: 30000,
})

// We expect a challenge ACK to be emitted in response.
v := c.GetPacket()
defer v.Release()
checker.IPv4(t, v,
checker.TCP(
checker.DstPort(context.TestPort),
checker.TCPFlags(header.TCPFlagAck),
checker.TCPSeqNum(uint32(c.IRS)+1),
checker.TCPAckNum(uint32(rcvNxt)),
),
)

// Read must NOT return ErrConnectionReset.
_, err := c.EP.Read(io.Discard, tcpip.ReadOptions{})
if _, ok := err.(*tcpip.ErrConnectionReset); ok {
t.Fatalf("connection aborted on in-window non-exact RST (RFC 5961 strict-match rule violated)")
}

if got, want := tcp.EndpointState(c.EP.State()), tcp.StateEstablished; got != want {
t.Errorf("endpoint state = %v, want %v", got, want)
}
}

// TestRFC5961_RSTExactMatchAbortsConnection verifies that an RST whose
// sequence number equals RCV.NXT does abort the connection. This preserves
// the existing behavior for legitimate RSTs.
func TestRFC5961_RSTExactMatchAbortsConnection(t *testing.T) {
c := context.New(t, e2e.DefaultMTU)
defer c.Cleanup()

c.CreateConnected(context.TestInitialSequenceNumber, 30000, -1 /* epRcvBuf */)

rcvNxt := seqnum.Value(context.TestInitialSequenceNumber).Add(1)

c.SendPacket(nil, &context.Headers{
SrcPort: context.TestPort,
DstPort: c.Port,
Flags: header.TCPFlagRst,
SeqNum: rcvNxt,
RcvWnd: 30000,
})

// Wait for the readable event signaling the connection went into the
// error state.
we, ch := waiter.NewChannelEntry(waiter.ReadableEvents)
c.WQ.EventRegister(&we)
defer c.WQ.EventUnregister(&we)

for {
_, err := c.EP.Read(io.Discard, tcpip.ReadOptions{})
switch err.(type) {
case *tcpip.ErrWouldBlock:
select {
case <-ch:
// Loop back and Read again to surface the hard error.
continue
case <-time.After(2 * time.Second):
t.Fatalf("connection did not abort on exact-match RST within 2s")
}
case *tcpip.ErrConnectionReset:
return
default:
t.Fatalf("c.EP.Read after exact-match RST: err = %v, want ErrConnectionReset", err)
}
}
}