Skip to content

Commit d87f99b

Browse files
committed
quic: idle timeouts, handshake timeouts, and keepalive
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 <[email protected]> Reviewed-by: Jonathan Amsterdam <[email protected]>
1 parent 7b5abfa commit d87f99b

File tree

12 files changed

+721
-140
lines changed

12 files changed

+721
-140
lines changed

internal/quic/config.go

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ package quic
99
import (
1010
"crypto/tls"
1111
"log/slog"
12+
"math"
13+
"time"
1214
)
1315

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

79+
// HandshakeTimeout is the maximum time in which a connection handshake must complete.
80+
// If zero, the default of 10 seconds is used.
81+
// If negative, there is no handshake timeout.
82+
HandshakeTimeout time.Duration
83+
84+
// MaxIdleTimeout is the maximum time after which an idle connection will be closed.
85+
// If zero, the default of 30 seconds is used.
86+
// If negative, idle connections are never closed.
87+
//
88+
// The idle timeout for a connection is the minimum of the maximum idle timeouts
89+
// of the endpoints.
90+
MaxIdleTimeout time.Duration
91+
92+
// KeepAlivePeriod is the time after which a packet will be sent to keep
93+
// an idle connection alive.
94+
// If zero, keep alive packets are not sent.
95+
// If greater than zero, the keep alive period is the smaller of KeepAlivePeriod and
96+
// half the connection idle timeout.
97+
KeepAlivePeriod time.Duration
98+
7799
// QLogLogger receives qlog events.
78100
//
79101
// Events currently correspond to the definitions in draft-ietf-qlog-quic-events-03.
@@ -85,7 +107,7 @@ type Config struct {
85107
QLogLogger *slog.Logger
86108
}
87109

88-
func configDefault(v, def, limit int64) int64 {
110+
func configDefault[T ~int64](v, def, limit T) T {
89111
switch {
90112
case v == 0:
91113
return def
@@ -115,3 +137,15 @@ func (c *Config) maxStreamWriteBufferSize() int64 {
115137
func (c *Config) maxConnReadBufferSize() int64 {
116138
return configDefault(c.MaxConnReadBufferSize, 1<<20, maxVarint)
117139
}
140+
141+
func (c *Config) handshakeTimeout() time.Duration {
142+
return configDefault(c.HandshakeTimeout, defaultHandshakeTimeout, math.MaxInt64)
143+
}
144+
145+
func (c *Config) maxIdleTimeout() time.Duration {
146+
return configDefault(c.MaxIdleTimeout, defaultMaxIdleTimeout, math.MaxInt64)
147+
}
148+
149+
func (c *Config) keepAlivePeriod() time.Duration {
150+
return configDefault(c.KeepAlivePeriod, defaultKeepAlivePeriod, math.MaxInt64)
151+
}

internal/quic/conn.go

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -26,22 +26,17 @@ type Conn struct {
2626
testHooks connTestHooks
2727
peerAddr netip.AddrPort
2828

29-
msgc chan any
30-
donec chan struct{} // closed when conn loop exits
31-
exited bool // set to make the conn loop exit immediately
29+
msgc chan any
30+
donec chan struct{} // closed when conn loop exits
3231

3332
w packetWriter
3433
acks [numberSpaceCount]ackState // indexed by number space
3534
lifetime lifetimeState
35+
idle idleState
3636
connIDState connIDState
3737
loss lossState
3838
streams streamsState
3939

40-
// idleTimeout is the time at which the connection will be closed due to inactivity.
41-
// https://www.rfc-editor.org/rfc/rfc9000#section-10.1
42-
maxIdleTimeout time.Duration
43-
idleTimeout time.Time
44-
4540
// Packet protection keys, CRYPTO streams, and TLS state.
4641
keysInitial fixedKeyPair
4742
keysHandshake fixedKeyPair
@@ -105,8 +100,6 @@ func newConn(now time.Time, side connSide, cids newServerConnIDs, peerAddr netip
105100
peerAddr: peerAddr,
106101
msgc: make(chan any, 1),
107102
donec: make(chan struct{}),
108-
maxIdleTimeout: defaultMaxIdleTimeout,
109-
idleTimeout: now.Add(defaultMaxIdleTimeout),
110103
peerAckDelayExponent: -1,
111104
}
112105
defer func() {
@@ -151,6 +144,7 @@ func newConn(now time.Time, side connSide, cids newServerConnIDs, peerAddr netip
151144
c.loss.init(c.side, maxDatagramSize, now)
152145
c.streamsInit()
153146
c.lifetimeInit()
147+
c.restartIdleTimer(now)
154148

155149
if err := c.startTLS(now, initialConnID, transportParameters{
156150
initialSrcConnID: c.connIDState.srcConnID(),
@@ -202,6 +196,7 @@ func (c *Conn) confirmHandshake(now time.Time) {
202196
// don't need to send anything.
203197
c.handshakeConfirmed.setReceived()
204198
}
199+
c.restartIdleTimer(now)
205200
c.loss.confirmHandshake()
206201
// "An endpoint MUST discard its Handshake keys when the TLS handshake is confirmed"
207202
// https://www.rfc-editor.org/rfc/rfc9001#section-4.9.2-1
@@ -232,6 +227,7 @@ func (c *Conn) receiveTransportParameters(p transportParameters) error {
232227
c.streams.peerInitialMaxStreamDataBidiLocal = p.initialMaxStreamDataBidiLocal
233228
c.streams.peerInitialMaxStreamDataRemote[bidiStream] = p.initialMaxStreamDataBidiRemote
234229
c.streams.peerInitialMaxStreamDataRemote[uniStream] = p.initialMaxStreamDataUni
230+
c.receivePeerMaxIdleTimeout(p.maxIdleTimeout)
235231
c.peerAckDelayExponent = p.ackDelayExponent
236232
c.loss.setMaxAckDelay(p.maxAckDelay)
237233
if err := c.connIDState.setPeerActiveConnIDLimit(c, p.activeConnIDLimit); err != nil {
@@ -248,7 +244,6 @@ func (c *Conn) receiveTransportParameters(p transportParameters) error {
248244
return err
249245
}
250246
}
251-
// TODO: max_idle_timeout
252247
// TODO: stateless_reset_token
253248
// TODO: max_udp_payload_size
254249
// TODO: disable_active_migration
@@ -261,6 +256,8 @@ type (
261256
wakeEvent struct{}
262257
)
263258

259+
var errIdleTimeout = errors.New("idle timeout")
260+
264261
// loop is the connection main loop.
265262
//
266263
// Except where otherwise noted, all connection state is owned by the loop goroutine.
@@ -288,14 +285,14 @@ func (c *Conn) loop(now time.Time) {
288285
defer timer.Stop()
289286
}
290287

291-
for !c.exited {
288+
for c.lifetime.state != connStateDone {
292289
sendTimeout := c.maybeSend(now) // try sending
293290

294291
// Note that we only need to consider the ack timer for the App Data space,
295292
// since the Initial and Handshake spaces always ack immediately.
296293
nextTimeout := sendTimeout
297-
nextTimeout = firstTime(nextTimeout, c.idleTimeout)
298-
if !c.isClosingOrDraining() {
294+
nextTimeout = firstTime(nextTimeout, c.idle.nextTimeout)
295+
if c.isAlive() {
299296
nextTimeout = firstTime(nextTimeout, c.loss.timer)
300297
nextTimeout = firstTime(nextTimeout, c.acks[appDataSpace].nextAck)
301298
} else {
@@ -329,11 +326,9 @@ func (c *Conn) loop(now time.Time) {
329326
m.recycle()
330327
case timerEvent:
331328
// A connection timer has expired.
332-
if !now.Before(c.idleTimeout) {
333-
// "[...] the connection is silently closed and
334-
// its state is discarded [...]"
335-
// https://www.rfc-editor.org/rfc/rfc9000#section-10.1-1
336-
c.exited = true
329+
if c.idleAdvance(now) {
330+
// The connection idle timer has expired.
331+
c.abortImmediately(now, errIdleTimeout)
337332
return
338333
}
339334
c.loss.advance(now, c.handleAckOrLoss)

0 commit comments

Comments
 (0)