blob: 1798d0536fb59d66d05903101572cb75d6008c1e [file] [log] [blame]
// 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"
)
// connState is the state of a connection.
type connState int
const (
// A connection is alive when it is first created.
connStateAlive = connState(iota)
// The connection has received a CONNECTION_CLOSE frame from the peer,
// and has not yet sent a CONNECTION_CLOSE in response.
//
// We will send a CONNECTION_CLOSE, and then enter the draining state.
connStatePeerClosed
// The connection is in the closing state.
//
// We will send CONNECTION_CLOSE frames to the peer
// (once upon entering the closing state, and possibly again in response to peer packets).
//
// If we receive a CONNECTION_CLOSE from the peer, we will enter the draining state.
// Otherwise, we will eventually time out and move to the done state.
//
// https://www.rfc-editor.org/rfc/rfc9000#section-10.2.1
connStateClosing
// The connection is in the draining state.
//
// We will neither send packets nor process received packets.
// When the drain timer expires, we move to the done state.
//
// https://www.rfc-editor.org/rfc/rfc9000#section-10.2.2
connStateDraining
// The connection is done, and the conn loop will exit.
connStateDone
)
// 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 {
state connState
readyc chan struct{} // closed when TLS handshake completes
donec chan struct{} // closed when finalErr is set
localErr error // error sent to the peer
finalErr error // error sent by the peer, or transport error; set before closing donec
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.donec = make(chan struct{})
}
var (
errNoPeerResponse = errors.New("peer did not respond to CONNECTION_CLOSE")
errConnClosed = errors.New("connection closed")
)
// 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.state != connStateDraining {
// We were in the closing state, waiting for a CONNECTION_CLOSE from the peer.
c.setFinalError(errNoPeerResponse)
}
c.setState(now, connStateDone)
return true
}
// setState sets the conn state.
func (c *Conn) setState(now time.Time, state connState) {
if c.lifetime.state == state {
return
}
c.lifetime.state = state
switch state {
case connStateClosing, connStateDraining:
if c.lifetime.drainEndTime.IsZero() {
c.lifetime.drainEndTime = now.Add(3 * c.loss.ptoBasePeriod())
}
case connStateDone:
c.setFinalError(nil)
}
if state != connStateAlive {
c.streamsCleanup()
}
}
// 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 {
switch c.lifetime.state {
case connStateDraining, connStateDone:
return true
}
return false
}
// isAlive reports whether the conn is handling packets.
func (c *Conn) isAlive() bool {
return c.lifetime.state == connStateAlive
}
// sendOK reports whether the conn can send frames at this time.
func (c *Conn) sendOK(now time.Time) bool {
switch c.lifetime.state {
case connStateAlive:
return true
case connStatePeerClosed:
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
}
// We should send a CONNECTION_CLOSE.
return true
case connStateClosing:
if c.lifetime.connCloseSentTime.IsZero() {
return true
}
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
}
return true
case connStateDraining:
// We are in the draining state, and will send no more packets.
return false
case connStateDone:
return false
default:
panic("BUG: unhandled connection state")
}
}
// sendConnectionClose reports that the conn has sent a CONNECTION_CLOSE to the peer.
func (c *Conn) sentConnectionClose(now time.Time) {
switch c.lifetime.state {
case connStatePeerClosed:
c.enterDraining(now)
}
if c.lifetime.connCloseSentTime.IsZero() {
// 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)
} else if !c.lifetime.connCloseSentTime.Equal(now) {
// If connCloseSentTime == now, we're sending two CONNECTION_CLOSE frames
// coalesced into the same datagram. We only want to increase the delay once.
c.lifetime.connCloseDelay *= 2
}
c.lifetime.connCloseSentTime = now
}
// handlePeerConnectionClose handles a CONNECTION_CLOSE from the peer.
func (c *Conn) handlePeerConnectionClose(now time.Time, err error) {
c.setFinalError(err)
switch c.lifetime.state {
case connStateAlive:
c.setState(now, connStatePeerClosed)
case connStatePeerClosed:
// Duplicate CONNECTION_CLOSE, ignore.
case connStateClosing:
if c.lifetime.connCloseSentTime.IsZero() {
c.setState(now, connStatePeerClosed)
} else {
c.setState(now, connStateDraining)
}
case connStateDraining:
case connStateDone:
}
}
// setFinalError records the final connection status we report to the user.
func (c *Conn) setFinalError(err error) {
select {
case <-c.lifetime.donec:
return // already set
default:
}
c.lifetime.finalErr = err
close(c.lifetime.donec)
}
func (c *Conn) waitReady(ctx context.Context) error {
select {
case <-c.lifetime.readyc:
return nil
case <-c.lifetime.donec:
return c.lifetime.finalErr
default:
}
select {
case <-c.lifetime.readyc:
return nil
case <-c.lifetime.donec:
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.donec
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.donec); 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{code: errNo}
}
c.sendMsg(func(now time.Time, c *Conn) {
c.enterClosing(now, err)
})
}
// abort terminates a connection with an error.
func (c *Conn) abort(now time.Time, err error) {
c.setFinalError(err) // this error takes precedence over the peer's CONNECTION_CLOSE
c.enterClosing(now, err)
}
// abortImmediately terminates a connection.
// The connection does not send a CONNECTION_CLOSE, and skips the draining period.
func (c *Conn) abortImmediately(now time.Time, err error) {
c.setFinalError(err)
c.setState(now, connStateDone)
}
// enterClosing starts an immediate close.
// We will send a CONNECTION_CLOSE to the peer and wait for their response.
func (c *Conn) enterClosing(now time.Time, err error) {
switch c.lifetime.state {
case connStateAlive:
c.lifetime.localErr = err
c.setState(now, connStateClosing)
case connStatePeerClosed:
c.lifetime.localErr = err
}
}
// enterDraining moves directly to the draining state, without sending a CONNECTION_CLOSE.
func (c *Conn) enterDraining(now time.Time) {
switch c.lifetime.state {
case connStateAlive, connStatePeerClosed, connStateClosing:
c.setState(now, connStateDraining)
}
}
// exit fully terminates a connection immediately.
func (c *Conn) exit() {
c.sendMsg(func(now time.Time, c *Conn) {
c.abortImmediately(now, errors.New("connection closed"))
})
}