Skip to content

Commit

Permalink
quic: idle timeouts, handshake timeouts, and keepalive
Browse files Browse the repository at this point in the history
Negotiate the connection idle timeout based on the sent and received
max_idle_timeout transport parameter values.

Set a configurable limit on how long a handshake can take to complete.

Add a configuration option to send keep-alive PING frames to avoid
connection closure due to the idle timeout.

RFC 9000, Section 10.1.

For golang/go#58547

Change-Id: If6a611090ab836cd6937fcfbb1360a0f07425102
Reviewed-on: https://go-review.googlesource.com/c/net/+/540895
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 17, 2023
1 parent 7b5abfa commit d87f99b
Show file tree
Hide file tree
Showing 12 changed files with 721 additions and 140 deletions.
36 changes: 35 additions & 1 deletion internal/quic/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ package quic
import (
"crypto/tls"
"log/slog"
"math"
"time"
)

// A Config structure configures a QUIC endpoint.
Expand Down Expand Up @@ -74,6 +76,26 @@ type Config struct {
// If this field is left as zero, stateless reset is disabled.
StatelessResetKey [32]byte

// HandshakeTimeout is the maximum time in which a connection handshake must complete.
// If zero, the default of 10 seconds is used.
// If negative, there is no handshake timeout.
HandshakeTimeout time.Duration

// MaxIdleTimeout is the maximum time after which an idle connection will be closed.
// If zero, the default of 30 seconds is used.
// If negative, idle connections are never closed.
//
// The idle timeout for a connection is the minimum of the maximum idle timeouts
// of the endpoints.
MaxIdleTimeout time.Duration

// KeepAlivePeriod is the time after which a packet will be sent to keep
// an idle connection alive.
// If zero, keep alive packets are not sent.
// If greater than zero, the keep alive period is the smaller of KeepAlivePeriod and
// half the connection idle timeout.
KeepAlivePeriod time.Duration

// QLogLogger receives qlog events.
//
// Events currently correspond to the definitions in draft-ietf-qlog-quic-events-03.
Expand All @@ -85,7 +107,7 @@ type Config struct {
QLogLogger *slog.Logger
}

func configDefault(v, def, limit int64) int64 {
func configDefault[T ~int64](v, def, limit T) T {
switch {
case v == 0:
return def
Expand Down Expand Up @@ -115,3 +137,15 @@ func (c *Config) maxStreamWriteBufferSize() int64 {
func (c *Config) maxConnReadBufferSize() int64 {
return configDefault(c.MaxConnReadBufferSize, 1<<20, maxVarint)
}

func (c *Config) handshakeTimeout() time.Duration {
return configDefault(c.HandshakeTimeout, defaultHandshakeTimeout, math.MaxInt64)
}

func (c *Config) maxIdleTimeout() time.Duration {
return configDefault(c.MaxIdleTimeout, defaultMaxIdleTimeout, math.MaxInt64)
}

func (c *Config) keepAlivePeriod() time.Duration {
return configDefault(c.KeepAlivePeriod, defaultKeepAlivePeriod, math.MaxInt64)
}
33 changes: 14 additions & 19 deletions internal/quic/conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,17 @@ type Conn struct {
testHooks connTestHooks
peerAddr netip.AddrPort

msgc chan any
donec chan struct{} // closed when conn loop exits
exited bool // set to make the conn loop exit immediately
msgc chan any
donec chan struct{} // closed when conn loop exits

w packetWriter
acks [numberSpaceCount]ackState // indexed by number space
lifetime lifetimeState
idle idleState
connIDState connIDState
loss lossState
streams streamsState

// 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
idleTimeout time.Time

// Packet protection keys, CRYPTO streams, and TLS state.
keysInitial fixedKeyPair
keysHandshake fixedKeyPair
Expand Down Expand Up @@ -105,8 +100,6 @@ func newConn(now time.Time, side connSide, cids newServerConnIDs, peerAddr netip
peerAddr: peerAddr,
msgc: make(chan any, 1),
donec: make(chan struct{}),
maxIdleTimeout: defaultMaxIdleTimeout,
idleTimeout: now.Add(defaultMaxIdleTimeout),
peerAckDelayExponent: -1,
}
defer func() {
Expand Down Expand Up @@ -151,6 +144,7 @@ func newConn(now time.Time, side connSide, cids newServerConnIDs, peerAddr netip
c.loss.init(c.side, maxDatagramSize, now)
c.streamsInit()
c.lifetimeInit()
c.restartIdleTimer(now)

if err := c.startTLS(now, initialConnID, transportParameters{
initialSrcConnID: c.connIDState.srcConnID(),
Expand Down Expand Up @@ -202,6 +196,7 @@ func (c *Conn) confirmHandshake(now time.Time) {
// don't need to send anything.
c.handshakeConfirmed.setReceived()
}
c.restartIdleTimer(now)
c.loss.confirmHandshake()
// "An endpoint MUST discard its Handshake keys when the TLS handshake is confirmed"
// https://www.rfc-editor.org/rfc/rfc9001#section-4.9.2-1
Expand Down Expand Up @@ -232,6 +227,7 @@ func (c *Conn) receiveTransportParameters(p transportParameters) error {
c.streams.peerInitialMaxStreamDataBidiLocal = p.initialMaxStreamDataBidiLocal
c.streams.peerInitialMaxStreamDataRemote[bidiStream] = p.initialMaxStreamDataBidiRemote
c.streams.peerInitialMaxStreamDataRemote[uniStream] = p.initialMaxStreamDataUni
c.receivePeerMaxIdleTimeout(p.maxIdleTimeout)
c.peerAckDelayExponent = p.ackDelayExponent
c.loss.setMaxAckDelay(p.maxAckDelay)
if err := c.connIDState.setPeerActiveConnIDLimit(c, p.activeConnIDLimit); err != nil {
Expand All @@ -248,7 +244,6 @@ func (c *Conn) receiveTransportParameters(p transportParameters) error {
return err
}
}
// TODO: max_idle_timeout
// TODO: stateless_reset_token
// TODO: max_udp_payload_size
// TODO: disable_active_migration
Expand All @@ -261,6 +256,8 @@ type (
wakeEvent struct{}
)

var errIdleTimeout = errors.New("idle timeout")

// loop is the connection main loop.
//
// Except where otherwise noted, all connection state is owned by the loop goroutine.
Expand Down Expand Up @@ -288,14 +285,14 @@ func (c *Conn) loop(now time.Time) {
defer timer.Stop()
}

for !c.exited {
for c.lifetime.state != connStateDone {
sendTimeout := c.maybeSend(now) // try sending

// Note that we only need to consider the ack timer for the App Data space,
// since the Initial and Handshake spaces always ack immediately.
nextTimeout := sendTimeout
nextTimeout = firstTime(nextTimeout, c.idleTimeout)
if !c.isClosingOrDraining() {
nextTimeout = firstTime(nextTimeout, c.idle.nextTimeout)
if c.isAlive() {
nextTimeout = firstTime(nextTimeout, c.loss.timer)
nextTimeout = firstTime(nextTimeout, c.acks[appDataSpace].nextAck)
} else {
Expand Down Expand Up @@ -329,11 +326,9 @@ func (c *Conn) loop(now time.Time) {
m.recycle()
case timerEvent:
// A connection timer has expired.
if !now.Before(c.idleTimeout) {
// "[...] the connection is silently closed and
// its state is discarded [...]"
// https://www.rfc-editor.org/rfc/rfc9000#section-10.1-1
c.exited = true
if c.idleAdvance(now) {
// The connection idle timer has expired.
c.abortImmediately(now, errIdleTimeout)
return
}
c.loss.advance(now, c.handleAckOrLoss)
Expand Down
Loading

0 comments on commit d87f99b

Please sign in to comment.