// 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. package quic import ( "time" ) // idleState tracks connection idle events. // // Before the handshake is confirmed, the idle timeout is Config.HandshakeTimeout. // // After the handshake is confirmed, the idle timeout is // the minimum of Config.MaxIdleTimeout and the peer's max_idle_timeout transport parameter. // // If KeepAlivePeriod is set, keep-alive pings are sent. // Keep-alives are only sent after the handshake is confirmed. // // https://www.rfc-editor.org/rfc/rfc9000#section-10.1 type idleState struct { // idleDuration is the negotiated idle timeout for the connection. idleDuration time.Duration // idleTimeout is the time at which the connection will be closed due to inactivity. idleTimeout time.Time // nextTimeout is the time of the next idle event. // If nextTimeout == idleTimeout, this is the idle timeout. // Otherwise, this is the keep-alive timeout. nextTimeout time.Time // sentSinceLastReceive is set if we have sent an ack-eliciting packet // since the last time we received and processed a packet from the peer. sentSinceLastReceive bool // shouldSendKeepAlive is set when the keep-alive timer expires. shouldSendKeepAlive bool } // receivePeerMaxIdleTimeout handles the peer's max_idle_timeout transport parameter. func (c *Conn) receivePeerMaxIdleTimeout(peerMaxIdleTimeout time.Duration) { localMaxIdleTimeout := c.config.maxIdleTimeout() switch { case localMaxIdleTimeout == 0: c.idle.idleDuration = peerMaxIdleTimeout case peerMaxIdleTimeout == 0: c.idle.idleDuration = localMaxIdleTimeout default: c.idle.idleDuration = min(localMaxIdleTimeout, peerMaxIdleTimeout) } } func (c *Conn) idleHandlePacketReceived(now time.Time) { if !c.handshakeConfirmed.isSet() { return } // "An endpoint restarts its idle timer when a packet from its peer is // received and processed successfully." // https://www.rfc-editor.org/rfc/rfc9000#section-10.1-3 c.idle.sentSinceLastReceive = false c.restartIdleTimer(now) } func (c *Conn) idleHandlePacketSent(now time.Time, sent *sentPacket) { // "An endpoint also restarts its idle timer when sending an ack-eliciting packet // if no other ack-eliciting packets have been sent since // last receiving and processing a packet." // https://www.rfc-editor.org/rfc/rfc9000#section-10.1-3 if c.idle.sentSinceLastReceive || !sent.ackEliciting || !c.handshakeConfirmed.isSet() { return } c.idle.sentSinceLastReceive = true c.restartIdleTimer(now) } func (c *Conn) restartIdleTimer(now time.Time) { if !c.isAlive() { // Connection is closing, disable timeouts. c.idle.idleTimeout = time.Time{} c.idle.nextTimeout = time.Time{} return } var idleDuration time.Duration if c.handshakeConfirmed.isSet() { idleDuration = c.idle.idleDuration } else { idleDuration = c.config.handshakeTimeout() } if idleDuration == 0 { c.idle.idleTimeout = time.Time{} } else { // "[...] endpoints MUST increase the idle timeout period to be // at least three times the current Probe Timeout (PTO)." // https://www.rfc-editor.org/rfc/rfc9000#section-10.1-4 idleDuration = max(idleDuration, 3*c.loss.ptoPeriod()) c.idle.idleTimeout = now.Add(idleDuration) } // Set the time of our next event: // The idle timer if no keep-alive is set, or the keep-alive timer if one is. c.idle.nextTimeout = c.idle.idleTimeout c.idle.shouldSendKeepAlive = false keepAlive := c.config.keepAlivePeriod() switch { case !c.handshakeConfirmed.isSet(): // We do not send keep-alives before the handshake is complete. case keepAlive <= 0: // Keep-alives are not enabled. case c.idle.sentSinceLastReceive: // We have sent an ack-eliciting packet to the peer. // If they don't acknowledge it, loss detection will follow up with PTO probes, // which will function as keep-alives. // We don't need to send further pings. case idleDuration == 0: // The connection does not have a negotiated idle timeout. // Send keep-alives anyway, since they may be required to keep middleboxes // from losing state. c.idle.nextTimeout = now.Add(keepAlive) default: // Schedule our next keep-alive. // If our configured keep-alive period is greater than half the negotiated // connection idle timeout, we reduce the keep-alive period to half // the idle timeout to ensure we have time for the ping to arrive. c.idle.nextTimeout = now.Add(min(keepAlive, idleDuration/2)) } } func (c *Conn) appendKeepAlive() bool { if !c.idle.shouldSendKeepAlive { return true } sent := false switch { case c.idle.sentSinceLastReceive: sent = true // already sent an ack-eliciting packet case c.w.sent.ackEliciting: sent = true // this packet is already ack-eliciting default: // Send an ack-eliciting PING frame to the peer to keep the connection alive. sent = c.w.appendPingFrame() } if sent { c.idle.shouldSendKeepAlive = false } return sent } var errHandshakeTimeout error = localTransportError{ code: errConnectionRefused, reason: "handshake timeout", } func (c *Conn) idleAdvance(now time.Time) (shouldExit bool) { if idle := c.idle.idleTimeout; !idle.IsZero() && !now.Before(idle) { c.idle.idleTimeout = time.Time{} c.idle.nextTimeout = time.Time{} if !c.handshakeConfirmed.isSet() { // Handshake timeout has expired. // If we're a server, we're refusing the too-slow client. // If we're a client, we're giving up. // In either case, we're going to send a CONNECTION_CLOSE frame and // enter the closing state rather than unceremoniously dropping // the connection, since the peer might still be trying to // complete the handshake. c.abort(now, errHandshakeTimeout) return false } // Idle timeout has expired. // // "[...] the connection is silently closed and its state is discarded [...]" // https://www.rfc-editor.org/rfc/rfc9000#section-10.1-1 return true } // If nextTimeout != idleTimeout, then nextTimeout is the keep-alive timeout. if next := c.idle.nextTimeout; !next.Equal(c.idle.idleTimeout) && !next.IsZero() && !now.Before(next) { // Keep-alive timeout has expired. // Queue a keep-alive and switch to the idle timeout. c.idle.shouldSendKeepAlive = true c.idle.nextTimeout = c.idle.idleTimeout return false } return false }