Skip to content

Commit

Permalink
quic: don't send CONNECTION_CLOSE after stateless reset
Browse files Browse the repository at this point in the history
After receiving a stateless reset, we must enter the draining
state and send no further packets (including CONNECTION_CLOSE).
We were sending one last CONNECTION_CLOSE after the user
closed the Conn; fix this.

RFC 9000, Section 10.3.1.

Change-Id: I6a9cc6019470a25476df518022a32eefe0c50fcd
Reviewed-on: https://go-review.googlesource.com/c/net/+/540117
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
  • Loading branch information
neild committed Nov 6, 2023
1 parent 45fa414 commit 39c9d01
Show file tree
Hide file tree
Showing 4 changed files with 27 additions and 18 deletions.
16 changes: 11 additions & 5 deletions internal/quic/conn_close.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func (c *Conn) lifetimeAdvance(now time.Time) (done bool) {
c.lifetime.drainEndTime = time.Time{}
if c.lifetime.finalErr == nil {
// The peer never responded to our CONNECTION_CLOSE.
c.enterDraining(errNoPeerResponse)
c.enterDraining(now, errNoPeerResponse)
}
return true
}
Expand Down Expand Up @@ -152,11 +152,17 @@ func (c *Conn) sendOK(now time.Time) bool {
}

// enterDraining enters the draining state.
func (c *Conn) enterDraining(err error) {
func (c *Conn) enterDraining(now time.Time, err error) {
if c.isDraining() {
return
}
if e, ok := c.lifetime.localErr.(localTransportError); ok && e.code != errNo {
if err == errStatelessReset {
// If we've received a stateless reset, then we must not send a CONNECTION_CLOSE.
// Setting connCloseSentTime here prevents us from doing so.
c.lifetime.finalErr = errStatelessReset
c.lifetime.localErr = errStatelessReset
c.lifetime.connCloseSentTime = now
} else if e, ok := c.lifetime.localErr.(localTransportError); ok && e.code != errNo {
// If we've terminated the connection due to a peer protocol violation,
// record the final error on the connection as our reason for termination.
c.lifetime.finalErr = c.lifetime.localErr
Expand Down Expand Up @@ -239,14 +245,14 @@ func (c *Conn) abort(now time.Time, err error) {
// The connection does not send a CONNECTION_CLOSE, and skips the draining period.
func (c *Conn) abortImmediately(now time.Time, err error) {
c.abort(now, err)
c.enterDraining(err)
c.enterDraining(now, err)
c.exited = true
}

// exit fully terminates a connection immediately.
func (c *Conn) exit() {
c.sendMsg(func(now time.Time, c *Conn) {
c.enterDraining(errors.New("connection closed"))
c.enterDraining(now, errors.New("connection closed"))
c.exited = true
})
}
10 changes: 5 additions & 5 deletions internal/quic/conn_recv.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func (c *Conn) handleDatagram(now time.Time, dgram *datagram) {
if len(buf) == len(dgram.b) && len(buf) > statelessResetTokenLen {
var token statelessResetToken
copy(token[:], buf[len(buf)-len(token):])
c.handleStatelessReset(token)
c.handleStatelessReset(now, token)
}
// Invalid data at the end of a datagram is ignored.
break
Expand Down Expand Up @@ -525,7 +525,7 @@ func (c *Conn) handleConnectionCloseTransportFrame(now time.Time, payload []byte
if n < 0 {
return -1
}
c.enterDraining(peerTransportError{code: code, reason: reason})
c.enterDraining(now, peerTransportError{code: code, reason: reason})
return n
}

Expand All @@ -534,7 +534,7 @@ func (c *Conn) handleConnectionCloseApplicationFrame(now time.Time, payload []by
if n < 0 {
return -1
}
c.enterDraining(&ApplicationError{Code: code, Reason: reason})
c.enterDraining(now, &ApplicationError{Code: code, Reason: reason})
return n
}

Expand All @@ -556,9 +556,9 @@ func (c *Conn) handleHandshakeDoneFrame(now time.Time, space numberSpace, payloa

var errStatelessReset = errors.New("received stateless reset")

func (c *Conn) handleStatelessReset(resetToken statelessResetToken) {
func (c *Conn) handleStatelessReset(now time.Time, resetToken statelessResetToken) {
if !c.connIDState.isValidStatelessResetToken(resetToken) {
return
}
c.enterDraining(errStatelessReset)
c.enterDraining(now, errStatelessReset)
}
14 changes: 7 additions & 7 deletions internal/quic/listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,12 +253,18 @@ func (l *Listener) handleUnknownDestinationDatagram(m *datagram) {
if len(m.b) < minimumValidPacketSize {
return
}
var now time.Time
if l.testHooks != nil {
now = l.testHooks.timeNow()
} else {
now = time.Now()
}
// Check to see if this is a stateless reset.
var token statelessResetToken
copy(token[:], m.b[len(m.b)-len(token):])
if c := l.connsMap.byResetToken[token]; c != nil {
c.sendMsg(func(now time.Time, c *Conn) {
c.handleStatelessReset(token)
c.handleStatelessReset(now, token)
})
return
}
Expand Down Expand Up @@ -290,12 +296,6 @@ func (l *Listener) handleUnknownDestinationDatagram(m *datagram) {
// https://www.rfc-editor.org/rfc/rfc9000#section-10.3-16
return
}
var now time.Time
if l.testHooks != nil {
now = l.testHooks.timeNow()
} else {
now = time.Now()
}
cids := newServerConnIDs{
srcConnID: p.srcConnID,
dstConnID: p.dstConnID,
Expand Down
5 changes: 4 additions & 1 deletion internal/quic/stateless_reset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"errors"
"net/netip"
"testing"
"time"
)

func TestStatelessResetClientSendsStatelessResetTokenTransportParameter(t *testing.T) {
Expand Down Expand Up @@ -154,7 +155,9 @@ func TestStatelessResetSuccessfulNewConnectionID(t *testing.T) {
if err := tc.conn.Wait(canceledContext()); !errors.Is(err, errStatelessReset) {
t.Errorf("conn.Wait() = %v, want errStatelessReset", err)
}
tc.wantIdle("closed connection is idle")
tc.wantIdle("closed connection is idle in draining")
tc.advance(1 * time.Second) // long enough to exit the draining state
tc.wantIdle("closed connection is idle after draining")
}

func TestStatelessResetSuccessfulTransportParameter(t *testing.T) {
Expand Down

0 comments on commit 39c9d01

Please sign in to comment.