quic: connection lifetime management
Manage the closing and draining states.
A connection enters the closing state after sending a CONNECTION_CLOSE
frame to terminate the connection.
A connection enters the draining state after receiving a
CONNECTION_CLOSE frame.
Handle retransmission of CONNECTION_CLOSE frames when in the
closing state, and properly ignore received frames when in the
draining state.
RFC 9000, Section 10.2.
For golang/go#58547
Change-Id: I550ca544bffc4de7c5626f87a32c8902d5e2bc86
Reviewed-on: https://go-review.googlesource.com/c/net/+/528016
Auto-Submit: Damien Neil <dneil@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
diff --git a/internal/quic/conn.go b/internal/quic/conn.go
index 0063965..26c25f8 100644
--- a/internal/quic/conn.go
+++ b/internal/quic/conn.go
@@ -27,19 +27,15 @@
msgc chan any
donec chan struct{} // closed when conn loop exits
- readyc chan struct{} // closed when TLS handshake completes
exited bool // set to make the conn loop exit immediately
w packetWriter
acks [numberSpaceCount]ackState // indexed by number space
+ lifetime lifetimeState
connIDState connIDState
loss lossState
streams streamsState
- // errForPeer is set when the connection is being closed.
- errForPeer error
- connCloseSent [numberSpaceCount]bool
-
// idleTimeout is the time at which the connection will be closed due to inactivity.
// https://www.rfc-editor.org/rfc/rfc9000#section-10.1
maxIdleTimeout time.Duration
@@ -79,7 +75,6 @@
peerAddr: peerAddr,
msgc: make(chan any, 1),
donec: make(chan struct{}),
- readyc: make(chan struct{}),
testHooks: hooks,
maxIdleTimeout: defaultMaxIdleTimeout,
idleTimeout: now.Add(defaultMaxIdleTimeout),
@@ -106,6 +101,7 @@
const maxDatagramSize = 1200
c.loss.init(c.side, maxDatagramSize, now)
c.streamsInit()
+ c.lifetimeInit()
// TODO: initial_source_connection_id, retry_source_connection_id
c.startTLS(now, initialConnID, transportParameters{
@@ -131,14 +127,6 @@
return fmt.Sprintf("quic.Conn(%v,->%v)", c.side, c.peerAddr)
}
-func (c *Conn) Close() error {
- // TODO: Implement shutdown for real.
- c.runOnLoop(func(now time.Time, c *Conn) {
- c.exited = true
- })
- return nil
-}
-
// confirmHandshake is called when the handshake is confirmed.
// https://www.rfc-editor.org/rfc/rfc9001#section-4.1.2
func (c *Conn) confirmHandshake(now time.Time) {
@@ -241,8 +229,12 @@
// since the Initial and Handshake spaces always ack immediately.
nextTimeout := sendTimeout
nextTimeout = firstTime(nextTimeout, c.idleTimeout)
- nextTimeout = firstTime(nextTimeout, c.loss.timer)
- nextTimeout = firstTime(nextTimeout, c.acks[appDataSpace].nextAck)
+ if !c.isClosingOrDraining() {
+ nextTimeout = firstTime(nextTimeout, c.loss.timer)
+ nextTimeout = firstTime(nextTimeout, c.acks[appDataSpace].nextAck)
+ } else {
+ nextTimeout = firstTime(nextTimeout, c.lifetime.drainEndTime)
+ }
var m any
if hooks != nil {
@@ -279,6 +271,11 @@
return
}
c.loss.advance(now, c.handleAckOrLoss)
+ if c.lifetimeAdvance(now) {
+ // The connection has completed the draining period,
+ // and may be shut down.
+ return
+ }
case wakeEvent:
// We're being woken up to try sending some frames.
case func(time.Time, *Conn):
@@ -350,21 +347,6 @@
return nil
}
-// abort terminates a connection with an error.
-func (c *Conn) abort(now time.Time, err error) {
- if c.errForPeer == nil {
- c.errForPeer = err
- }
-}
-
-// exit fully terminates a connection immediately.
-func (c *Conn) exit() {
- c.runOnLoop(func(now time.Time, c *Conn) {
- c.exited = true
- })
- <-c.donec
-}
-
// firstTime returns the earliest non-zero time, or zero if both times are zero.
func firstTime(a, b time.Time) time.Time {
switch {
diff --git a/internal/quic/conn_close.go b/internal/quic/conn_close.go
new file mode 100644
index 0000000..ec0b7a3
--- /dev/null
+++ b/internal/quic/conn_close.go
@@ -0,0 +1,238 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build go1.21
+
+package quic
+
+import (
+ "context"
+ "errors"
+ "time"
+)
+
+// lifetimeState tracks the state of a connection.
+//
+// This is fairly coupled to the rest of a Conn, but putting it in a struct of its own helps
+// reason about operations that cause state transitions.
+type lifetimeState struct {
+ readyc chan struct{} // closed when TLS handshake completes
+ drainingc chan struct{} // closed when entering the draining state
+
+ // Possible states for the connection:
+ //
+ // Alive: localErr and finalErr are both nil.
+ //
+ // Closing: localErr is non-nil and finalErr is nil.
+ // We have sent a CONNECTION_CLOSE to the peer or are about to
+ // (if connCloseSentTime is zero) and are waiting for the peer to respond.
+ // drainEndTime is set to the time the closing state ends.
+ // https://www.rfc-editor.org/rfc/rfc9000.html#section-10.2.1
+ //
+ // Draining: finalErr is non-nil.
+ // If localErr is nil, we're waiting for the user to provide us with a final status
+ // to send to the peer.
+ // Otherwise, we've either sent a CONNECTION_CLOSE to the peer or are about to
+ // (if connCloseSentTime is zero).
+ // drainEndTime is set to the time the draining state ends.
+ // https://www.rfc-editor.org/rfc/rfc9000.html#section-10.2.2
+ localErr error // error sent to the peer
+ finalErr error // error sent by the peer, or transport error; always set before draining
+
+ connCloseSentTime time.Time // send time of last CONNECTION_CLOSE frame
+ connCloseDelay time.Duration // delay until next CONNECTION_CLOSE frame sent
+ drainEndTime time.Time // time the connection exits the draining state
+}
+
+func (c *Conn) lifetimeInit() {
+ c.lifetime.readyc = make(chan struct{})
+ c.lifetime.drainingc = make(chan struct{})
+}
+
+var errNoPeerResponse = errors.New("peer did not respond to CONNECTION_CLOSE")
+
+// advance is called when time passes.
+func (c *Conn) lifetimeAdvance(now time.Time) (done bool) {
+ if c.lifetime.drainEndTime.IsZero() || c.lifetime.drainEndTime.After(now) {
+ return false
+ }
+ // The connection drain period has ended, and we can shut down.
+ // https://www.rfc-editor.org/rfc/rfc9000.html#section-10.2-7
+ c.lifetime.drainEndTime = time.Time{}
+ if c.lifetime.finalErr == nil {
+ // The peer never responded to our CONNECTION_CLOSE.
+ c.enterDraining(errNoPeerResponse)
+ }
+ return true
+}
+
+// confirmHandshake is called when the TLS handshake completes.
+func (c *Conn) handshakeDone() {
+ close(c.lifetime.readyc)
+}
+
+// isDraining reports whether the conn is in the draining state.
+//
+// The draining state is entered once an endpoint receives a CONNECTION_CLOSE frame.
+// The endpoint will no longer send any packets, but we retain knowledge of the connection
+// until the end of the drain period to ensure we discard packets for the connection
+// rather than treating them as starting a new connection.
+//
+// https://www.rfc-editor.org/rfc/rfc9000.html#section-10.2.2
+func (c *Conn) isDraining() bool {
+ return c.lifetime.finalErr != nil
+}
+
+// isClosingOrDraining reports whether the conn is in the closing or draining states.
+func (c *Conn) isClosingOrDraining() bool {
+ return c.lifetime.localErr != nil || c.lifetime.finalErr != nil
+}
+
+// sendOK reports whether the conn can send frames at this time.
+func (c *Conn) sendOK(now time.Time) bool {
+ if !c.isClosingOrDraining() {
+ return true
+ }
+ // We are closing or draining.
+ if c.lifetime.localErr == nil {
+ // We're waiting for the user to close the connection, providing us with
+ // a final status to send to the peer.
+ return false
+ }
+ // Past this point, returning true will result in the conn sending a CONNECTION_CLOSE
+ // due to localErr being set.
+ if c.lifetime.drainEndTime.IsZero() {
+ // The closing and draining states should last for at least three times
+ // the current PTO interval. We currently use exactly that minimum.
+ // https://www.rfc-editor.org/rfc/rfc9000.html#section-10.2-5
+ //
+ // The drain period begins when we send or receive a CONNECTION_CLOSE,
+ // whichever comes first.
+ // https://www.rfc-editor.org/rfc/rfc9000.html#section-10.2.2-3
+ c.lifetime.drainEndTime = now.Add(3 * c.loss.ptoBasePeriod())
+ }
+ if c.lifetime.connCloseSentTime.IsZero() {
+ // We haven't sent a CONNECTION_CLOSE yet. Do so.
+ // Either we're initiating an immediate close
+ // (and will enter the closing state as soon as we send CONNECTION_CLOSE),
+ // or we've read a CONNECTION_CLOSE from our peer
+ // (and may send one CONNECTION_CLOSE before entering the draining state).
+ //
+ // Set the initial delay before we will send another CONNECTION_CLOSE.
+ //
+ // RFC 9000 states that we should rate limit CONNECTION_CLOSE frames,
+ // but leaves the implementation of the limit up to us. Here, we start
+ // with the same delay as the PTO timer (RFC 9002, Section 6.2.1),
+ // not including max_ack_delay, and double it on every CONNECTION_CLOSE sent.
+ c.lifetime.connCloseDelay = c.loss.rtt.smoothedRTT + max(4*c.loss.rtt.rttvar, timerGranularity)
+ c.lifetime.drainEndTime = now.Add(3 * c.loss.ptoBasePeriod())
+ return true
+ }
+ if c.isDraining() {
+ // We are in the draining state, and will send no more packets.
+ return false
+ }
+ maxRecvTime := c.acks[initialSpace].maxRecvTime
+ if t := c.acks[handshakeSpace].maxRecvTime; t.After(maxRecvTime) {
+ maxRecvTime = t
+ }
+ if t := c.acks[appDataSpace].maxRecvTime; t.After(maxRecvTime) {
+ maxRecvTime = t
+ }
+ if maxRecvTime.Before(c.lifetime.connCloseSentTime.Add(c.lifetime.connCloseDelay)) {
+ // After sending CONNECTION_CLOSE, ignore packets from the peer for
+ // a delay. On the next packet received after the delay, send another
+ // CONNECTION_CLOSE.
+ return false
+ }
+ c.lifetime.connCloseSentTime = now
+ c.lifetime.connCloseDelay *= 2
+ return true
+}
+
+// enterDraining enters the draining state.
+func (c *Conn) enterDraining(err error) {
+ if c.isDraining() {
+ return
+ }
+ if e, ok := c.lifetime.localErr.(localTransportError); ok && transportError(e) != 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
+ } else {
+ c.lifetime.finalErr = err
+ }
+ close(c.lifetime.drainingc)
+ c.streams.queue.close(c.lifetime.finalErr)
+}
+
+func (c *Conn) waitReady(ctx context.Context) error {
+ select {
+ case <-c.lifetime.readyc:
+ return nil
+ case <-c.lifetime.drainingc:
+ return c.lifetime.finalErr
+ case <-ctx.Done():
+ return ctx.Err()
+ }
+}
+
+// Close closes the connection.
+//
+// Close is equivalent to:
+//
+// conn.Abort(nil)
+// err := conn.Wait(context.Background())
+func (c *Conn) Close() error {
+ c.Abort(nil)
+ <-c.lifetime.drainingc
+ return c.lifetime.finalErr
+}
+
+// Wait waits for the peer to close the connection.
+//
+// If the connection is closed locally and the peer does not close its end of the connection,
+// Wait will return with a non-nil error after the drain period expires.
+//
+// If the peer closes the connection with a NO_ERROR transport error, Wait returns nil.
+// If the peer closes the connection with an application error, Wait returns an ApplicationError
+// containing the peer's error code and reason.
+// If the peer closes the connection with any other status, Wait returns a non-nil error.
+func (c *Conn) Wait(ctx context.Context) error {
+ if err := c.waitOnDone(ctx, c.lifetime.drainingc); err != nil {
+ return err
+ }
+ return c.lifetime.finalErr
+}
+
+// Abort closes the connection and returns immediately.
+//
+// If err is nil, Abort sends a transport error of NO_ERROR to the peer.
+// If err is an ApplicationError, Abort sends its error code and text.
+// Otherwise, Abort sends a transport error of APPLICATION_ERROR with the error's text.
+func (c *Conn) Abort(err error) {
+ if err == nil {
+ err = localTransportError(errNo)
+ }
+ c.runOnLoop(func(now time.Time, c *Conn) {
+ c.abort(now, err)
+ })
+}
+
+// abort terminates a connection with an error.
+func (c *Conn) abort(now time.Time, err error) {
+ if c.lifetime.localErr != nil {
+ return // already closing
+ }
+ c.lifetime.localErr = err
+}
+
+// exit fully terminates a connection immediately.
+func (c *Conn) exit() {
+ c.runOnLoop(func(now time.Time, c *Conn) {
+ c.enterDraining(errors.New("connection closed"))
+ c.exited = true
+ })
+ <-c.donec
+}
diff --git a/internal/quic/conn_close_test.go b/internal/quic/conn_close_test.go
new file mode 100644
index 0000000..20c00e7
--- /dev/null
+++ b/internal/quic/conn_close_test.go
@@ -0,0 +1,186 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build go1.21
+
+package quic
+
+import (
+ "context"
+ "crypto/tls"
+ "errors"
+ "testing"
+ "time"
+)
+
+func TestConnCloseResponseBackoff(t *testing.T) {
+ tc := newTestConn(t, clientSide)
+ tc.handshake()
+
+ tc.conn.Abort(nil)
+ tc.wantFrame("aborting connection generates CONN_CLOSE",
+ packetType1RTT, debugFrameConnectionCloseTransport{
+ code: errNo,
+ })
+
+ waiting := runAsync(tc, func(ctx context.Context) (struct{}, error) {
+ return struct{}{}, tc.conn.Wait(ctx)
+ })
+ if _, err := waiting.result(); err != errNotDone {
+ t.Errorf("conn.Wait() = %v, want still waiting", err)
+ }
+
+ tc.writeFrames(packetType1RTT, debugFramePing{})
+ tc.wantIdle("packets received immediately after CONN_CLOSE receive no response")
+
+ tc.advance(1100 * time.Microsecond)
+ tc.writeFrames(packetType1RTT, debugFramePing{})
+ tc.wantFrame("receiving packet 1.1ms after CONN_CLOSE generates another CONN_CLOSE",
+ packetType1RTT, debugFrameConnectionCloseTransport{
+ code: errNo,
+ })
+
+ tc.advance(1100 * time.Microsecond)
+ tc.writeFrames(packetType1RTT, debugFramePing{})
+ tc.wantIdle("no response to packet, because CONN_CLOSE backoff is now 2ms")
+
+ tc.advance(1000 * time.Microsecond)
+ tc.writeFrames(packetType1RTT, debugFramePing{})
+ tc.wantFrame("2ms since last CONN_CLOSE, receiving a packet generates another CONN_CLOSE",
+ packetType1RTT, debugFrameConnectionCloseTransport{
+ code: errNo,
+ })
+ if _, err := waiting.result(); err != errNotDone {
+ t.Errorf("conn.Wait() = %v, want still waiting", err)
+ }
+
+ tc.advance(100000 * time.Microsecond)
+ tc.writeFrames(packetType1RTT, debugFramePing{})
+ tc.wantIdle("drain timer expired, no more responses")
+
+ if _, err := waiting.result(); !errors.Is(err, errNoPeerResponse) {
+ t.Errorf("blocked conn.Wait() = %v, want errNoPeerResponse", err)
+ }
+ if err := tc.conn.Wait(canceledContext()); !errors.Is(err, errNoPeerResponse) {
+ t.Errorf("non-blocking conn.Wait() = %v, want errNoPeerResponse", err)
+ }
+}
+
+func TestConnCloseWithPeerResponse(t *testing.T) {
+ tc := newTestConn(t, clientSide)
+ tc.handshake()
+
+ tc.conn.Abort(nil)
+ tc.wantFrame("aborting connection generates CONN_CLOSE",
+ packetType1RTT, debugFrameConnectionCloseTransport{
+ code: errNo,
+ })
+
+ waiting := runAsync(tc, func(ctx context.Context) (struct{}, error) {
+ return struct{}{}, tc.conn.Wait(ctx)
+ })
+ if _, err := waiting.result(); err != errNotDone {
+ t.Errorf("conn.Wait() = %v, want still waiting", err)
+ }
+
+ tc.writeFrames(packetType1RTT, debugFrameConnectionCloseApplication{
+ code: 20,
+ })
+
+ wantErr := &ApplicationError{
+ Code: 20,
+ }
+ if _, err := waiting.result(); !errors.Is(err, wantErr) {
+ t.Errorf("blocked conn.Wait() = %v, want %v", err, wantErr)
+ }
+ if err := tc.conn.Wait(canceledContext()); !errors.Is(err, wantErr) {
+ t.Errorf("non-blocking conn.Wait() = %v, want %v", err, wantErr)
+ }
+}
+
+func TestConnClosePeerCloses(t *testing.T) {
+ tc := newTestConn(t, clientSide)
+ tc.handshake()
+
+ wantErr := &ApplicationError{
+ Code: 42,
+ Reason: "why?",
+ }
+ tc.writeFrames(packetType1RTT, debugFrameConnectionCloseApplication{
+ code: wantErr.Code,
+ reason: wantErr.Reason,
+ })
+ tc.wantIdle("CONN_CLOSE response not sent until user closes this side")
+
+ if err := tc.conn.Wait(canceledContext()); !errors.Is(err, wantErr) {
+ t.Errorf("conn.Wait() = %v, want %v", err, wantErr)
+ }
+
+ tc.conn.Abort(&ApplicationError{
+ Code: 9,
+ Reason: "because",
+ })
+ tc.wantFrame("CONN_CLOSE sent after user closes connection",
+ packetType1RTT, debugFrameConnectionCloseApplication{
+ code: 9,
+ reason: "because",
+ })
+}
+
+func TestConnCloseReceiveInInitial(t *testing.T) {
+ tc := newTestConn(t, clientSide)
+ tc.wantFrame("client sends Initial CRYPTO frame",
+ packetTypeInitial, debugFrameCrypto{
+ data: tc.cryptoDataOut[tls.QUICEncryptionLevelInitial],
+ })
+ tc.writeFrames(packetTypeInitial, debugFrameConnectionCloseTransport{
+ code: errConnectionRefused,
+ })
+ tc.wantIdle("CONN_CLOSE response not sent until user closes this side")
+
+ wantErr := peerTransportError{code: errConnectionRefused}
+ if err := tc.conn.Wait(canceledContext()); !errors.Is(err, wantErr) {
+ t.Errorf("conn.Wait() = %v, want %v", err, wantErr)
+ }
+
+ tc.conn.Abort(&ApplicationError{Code: 1})
+ tc.wantFrame("CONN_CLOSE in Initial frame is APPLICATION_ERROR",
+ packetTypeInitial, debugFrameConnectionCloseTransport{
+ code: errApplicationError,
+ })
+ tc.wantIdle("no more frames to send")
+}
+
+func TestConnCloseReceiveInHandshake(t *testing.T) {
+ tc := newTestConn(t, clientSide)
+ tc.ignoreFrame(frameTypeAck)
+ tc.wantFrame("client sends Initial CRYPTO frame",
+ packetTypeInitial, debugFrameCrypto{
+ data: tc.cryptoDataOut[tls.QUICEncryptionLevelInitial],
+ })
+ tc.writeFrames(packetTypeInitial, debugFrameCrypto{
+ data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial],
+ })
+ tc.writeFrames(packetTypeHandshake, debugFrameConnectionCloseTransport{
+ code: errConnectionRefused,
+ })
+ tc.wantIdle("CONN_CLOSE response not sent until user closes this side")
+
+ wantErr := peerTransportError{code: errConnectionRefused}
+ if err := tc.conn.Wait(canceledContext()); !errors.Is(err, wantErr) {
+ t.Errorf("conn.Wait() = %v, want %v", err, wantErr)
+ }
+
+ // The conn has Initial and Handshake keys, so it will send CONN_CLOSE in both spaces.
+ tc.conn.Abort(&ApplicationError{Code: 1})
+ tc.wantFrame("CONN_CLOSE in Initial frame is APPLICATION_ERROR",
+ packetTypeInitial, debugFrameConnectionCloseTransport{
+ code: errApplicationError,
+ })
+ tc.wantFrame("CONN_CLOSE in Handshake frame is APPLICATION_ERROR",
+ packetTypeHandshake, debugFrameConnectionCloseTransport{
+ code: errApplicationError,
+ })
+ tc.wantIdle("no more frames to send")
+}
diff --git a/internal/quic/conn_recv.go b/internal/quic/conn_recv.go
index 92ee8ea..64e5f98 100644
--- a/internal/quic/conn_recv.go
+++ b/internal/quic/conn_recv.go
@@ -13,6 +13,9 @@
func (c *Conn) handleDatagram(now time.Time, dgram *datagram) {
buf := dgram.b
c.loss.datagramReceived(now, len(buf))
+ if c.isDraining() {
+ return
+ }
for len(buf) > 0 {
var n int
ptype := getPacketType(buf)
@@ -220,15 +223,13 @@
}
n = c.handleRetireConnectionIDFrame(now, space, payload)
case frameTypeConnectionCloseTransport:
- // CONNECTION_CLOSE is OK in all spaces.
- _, _, _, n = consumeConnectionCloseTransportFrame(payload)
- // TODO: https://www.rfc-editor.org/rfc/rfc9000.html#section-10.2.2
- c.abort(now, localTransportError(errNo))
+ // Transport CONNECTION_CLOSE is OK in all spaces.
+ n = c.handleConnectionCloseTransportFrame(now, payload)
case frameTypeConnectionCloseApplication:
- // CONNECTION_CLOSE is OK in all spaces.
- _, _, n = consumeConnectionCloseApplicationFrame(payload)
- // TODO: https://www.rfc-editor.org/rfc/rfc9000.html#section-10.2.2
- c.abort(now, localTransportError(errNo))
+ if !frameOK(c, ptype, __01) {
+ return
+ }
+ n = c.handleConnectionCloseApplicationFrame(now, payload)
case frameTypeHandshakeDone:
if !frameOK(c, ptype, ___1) {
return
@@ -385,6 +386,24 @@
return n
}
+func (c *Conn) handleConnectionCloseTransportFrame(now time.Time, payload []byte) int {
+ code, _, reason, n := consumeConnectionCloseTransportFrame(payload)
+ if n < 0 {
+ return -1
+ }
+ c.enterDraining(peerTransportError{code: code, reason: reason})
+ return n
+}
+
+func (c *Conn) handleConnectionCloseApplicationFrame(now time.Time, payload []byte) int {
+ code, reason, n := consumeConnectionCloseApplicationFrame(payload)
+ if n < 0 {
+ return -1
+ }
+ c.enterDraining(&ApplicationError{Code: code, Reason: reason})
+ return n
+}
+
func (c *Conn) handleHandshakeDoneFrame(now time.Time, space numberSpace, payload []byte) int {
if c.side == serverSide {
// Clients should never send HANDSHAKE_DONE.
@@ -392,6 +411,8 @@
c.abort(now, localTransportError(errProtocolViolation))
return -1
}
- c.confirmHandshake(now)
+ if !c.isClosingOrDraining() {
+ c.confirmHandshake(now)
+ }
return 1
}
diff --git a/internal/quic/conn_send.go b/internal/quic/conn_send.go
index 9d315fb..853c845 100644
--- a/internal/quic/conn_send.go
+++ b/internal/quic/conn_send.go
@@ -16,6 +16,8 @@
//
// If sending is blocked by pacing, it returns the next time
// a datagram may be sent.
+//
+// If sending is blocked indefinitely, it returns the zero Time.
func (c *Conn) maybeSend(now time.Time) (next time.Time) {
// Assumption: The congestion window is not underutilized.
// If congestion control, pacing, and anti-amplification all permit sending,
@@ -39,6 +41,9 @@
// If anti-amplification blocks sending, then no packet can be sent.
return next
}
+ if !c.sendOK(now) {
+ return time.Time{}
+ }
// We may still send ACKs, even if congestion control or pacing limit sending.
// Prepare to write a datagram of at most maxSendSize bytes.
@@ -162,23 +167,8 @@
}
func (c *Conn) appendFrames(now time.Time, space numberSpace, pnum packetNumber, limit ccLimit) {
- if c.errForPeer != nil {
- // This is the bare minimum required to send a CONNECTION_CLOSE frame
- // when closing a connection immediately, for example in response to a
- // protocol error.
- //
- // This does not handle the closing and draining states
- // (https://www.rfc-editor.org/rfc/rfc9000.html#section-10.2),
- // but it's enough to let us write tests that result in a CONNECTION_CLOSE,
- // and have those tests still pass when we finish implementing
- // connection shutdown.
- //
- // TODO: Finish implementing connection shutdown.
- if !c.connCloseSent[space] {
- c.exited = true
- c.appendConnectionCloseFrame(c.errForPeer)
- c.connCloseSent[space] = true
- }
+ if c.lifetime.localErr != nil {
+ c.appendConnectionCloseFrame(now, space, c.lifetime.localErr)
return
}
@@ -322,11 +312,20 @@
return c.w.appendAckFrame(seen, d)
}
-func (c *Conn) appendConnectionCloseFrame(err error) {
- // TODO: Send application errors.
+func (c *Conn) appendConnectionCloseFrame(now time.Time, space numberSpace, err error) {
+ c.lifetime.connCloseSentTime = now
switch e := err.(type) {
case localTransportError:
c.w.appendConnectionCloseTransportFrame(transportError(e), 0, "")
+ case *ApplicationError:
+ if space != appDataSpace {
+ // "CONNECTION_CLOSE frames signaling application errors (type 0x1d)
+ // MUST only appear in the application data packet number space."
+ // https://www.rfc-editor.org/rfc/rfc9000#section-12.5-2.2
+ c.w.appendConnectionCloseTransportFrame(errApplicationError, 0, "")
+ } else {
+ c.w.appendConnectionCloseApplicationFrame(e.Code, e.Reason)
+ }
default:
// TLS alerts are sent using error codes [0x0100,0x01ff).
// https://www.rfc-editor.org/rfc/rfc9000#section-20.1-2.36.1
@@ -335,8 +334,8 @@
// tls.AlertError is a uint8, so this can't exceed 0x01ff.
code := errTLSBase + transportError(alert)
c.w.appendConnectionCloseTransportFrame(code, 0, "")
- return
+ } else {
+ c.w.appendConnectionCloseTransportFrame(errInternal, 0, "")
}
- c.w.appendConnectionCloseTransportFrame(errInternal, 0, "")
}
}
diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go
index cdbd466..4228ce7 100644
--- a/internal/quic/conn_test.go
+++ b/internal/quic/conn_test.go
@@ -304,6 +304,8 @@
select {
case <-idlec:
case <-tc.conn.donec:
+ // We may have async ops that can proceed now that the conn is done.
+ tc.wakeAsync()
}
if fail {
panic(fail)
diff --git a/internal/quic/errors.go b/internal/quic/errors.go
index f156859..8e01bb7 100644
--- a/internal/quic/errors.go
+++ b/internal/quic/errors.go
@@ -114,7 +114,13 @@
Reason string
}
-func (e ApplicationError) Error() string {
+func (e *ApplicationError) Error() string {
// TODO: Include the Reason string here, but sanitize it first.
return fmt.Sprintf("AppError %v", e.Code)
}
+
+// Is reports a match if err is an *ApplicationError with a matching Code.
+func (e *ApplicationError) Is(err error) bool {
+ e2, ok := err.(*ApplicationError)
+ return ok && e2.Code == e.Code
+}
diff --git a/internal/quic/listener.go b/internal/quic/listener.go
index 9869f6e..a84286e 100644
--- a/internal/quic/listener.go
+++ b/internal/quic/listener.go
@@ -141,11 +141,9 @@
if err != nil {
return nil, err
}
- select {
- case <-c.readyc:
- case <-ctx.Done():
- c.Close()
- return nil, ctx.Err()
+ if err := c.waitReady(ctx); err != nil {
+ c.Abort(nil)
+ return nil, err
}
return c, nil
}
diff --git a/internal/quic/tls.go b/internal/quic/tls.go
index 1d07f17..e3a430e 100644
--- a/internal/quic/tls.go
+++ b/internal/quic/tls.go
@@ -73,7 +73,7 @@
// https://www.rfc-editor.org/rfc/rfc9001#section-4.1.2-1
c.confirmHandshake(now)
}
- close(c.readyc)
+ c.handshakeDone()
case tls.QUICTransportParameters:
params, err := unmarshalTransportParams(e.Data)
if err != nil {
diff --git a/internal/quic/tls_test.go b/internal/quic/tls_test.go
index 0f22f4f..1c7b36d 100644
--- a/internal/quic/tls_test.go
+++ b/internal/quic/tls_test.go
@@ -353,6 +353,7 @@
tc.writeFrames(packetType1RTT,
debugFrameConnectionCloseTransport{code: errInternal})
+ tc.conn.Abort(nil)
tc.wantFrame("client closes connection after 1-RTT CONNECTION_CLOSE",
packetType1RTT, debugFrameConnectionCloseTransport{
code: errNo,
@@ -406,6 +407,7 @@
tc.writeFrames(packetType1RTT,
debugFrameConnectionCloseTransport{code: errInternal})
+ tc.conn.Abort(nil)
tc.wantFrame("server closes connection after 1-RTT CONNECTION_CLOSE",
packetType1RTT, debugFrameConnectionCloseTransport{
code: errNo,