quic: no local idle connection timeout, ngtcp2 keep-alive

Do not set a transport parameter idle timeout, meaning we have no such
thing from our side. The remote setting then applies.

In ngtcp2, set its "keep-alive" timer to prevent a possible remote idle
timeout to tear down the connection while we have active transfers on
that connection.

Closes #17057
This commit is contained in:
Stefan Eissing 2025-04-15 10:55:59 +02:00 committed by Daniel Stenberg
parent 1102ea9f4b
commit 21fd64645b
No known key found for this signature in database
GPG key ID: 5CC908FDB71E12C2
4 changed files with 49 additions and 31 deletions

View file

@ -140,7 +140,6 @@ struct cf_ngtcp2_ctx {
struct dynbuf scratch; /* temp buffer for header construction */
struct uint_hash streams; /* hash `data->mid` to `h3_stream_ctx` */
size_t max_stream_window; /* max flow window for one stream */
uint64_t max_idle_ms; /* max idle time for QUIC connection */
uint64_t used_bidi_streams; /* bidi streams we have opened */
uint64_t max_bidi_streams; /* max bidi streams we can open */
size_t earlydata_max; /* max amount of early data supported by
@ -169,7 +168,6 @@ static void cf_ngtcp2_ctx_init(struct cf_ngtcp2_ctx *ctx)
ctx->qlogfd = -1;
ctx->version = NGTCP2_PROTO_VER_MAX;
ctx->max_stream_window = H3_STREAM_WINDOW_SIZE;
ctx->max_idle_ms = CURL_QUIC_MAX_IDLE_MS;
Curl_bufcp_init(&ctx->stream_bufcp, H3_STREAM_CHUNK_SIZE,
H3_STREAM_POOL_SPARES);
Curl_dyn_init(&ctx->scratch, CURL_MAX_HTTP_HEADER);
@ -190,6 +188,44 @@ static void cf_ngtcp2_ctx_free(struct cf_ngtcp2_ctx *ctx)
free(ctx);
}
static void cf_ngtcp2_setup_keep_alive(struct Curl_cfilter *cf,
struct Curl_easy *data)
{
struct cf_ngtcp2_ctx *ctx = cf->ctx;
const ngtcp2_transport_params *rp;
/* Peer should have sent us its transport parameters. If it
* announces a positive `max_idle_timeout` it will close the
* connection when it does not hear from us for that time.
*
* Some servers use this as a keep-alive timer at a rather low
* value. We are doing HTTP/3 here and waiting for the response
* to a request may take a considerable amount of time. We need
* to prevent the peer's QUIC stack from closing in this case.
*/
if(!ctx->qconn)
return;
rp = ngtcp2_conn_get_remote_transport_params(ctx->qconn);
if(!rp || !rp->max_idle_timeout) {
ngtcp2_conn_set_keep_alive_timeout(ctx->qconn, UINT64_MAX);
CURL_TRC_CF(data, cf, "no peer idle timeout, unset keep-alive");
}
else if(!Curl_uint_hash_count(&ctx->streams)) {
ngtcp2_conn_set_keep_alive_timeout(ctx->qconn, UINT64_MAX);
CURL_TRC_CF(data, cf, "no active streams, unset keep-alive");
}
else {
ngtcp2_duration keep_ns;
keep_ns = (rp->max_idle_timeout > 1) ? (rp->max_idle_timeout / 2) : 1;
ngtcp2_conn_set_keep_alive_timeout(ctx->qconn, keep_ns);
CURL_TRC_CF(data, cf, "peer idle timeout is %" FMT_PRIu64 "ms, "
"set keep-alive to %" FMT_PRIu64 " ms.",
(curl_uint64_t)(rp->max_idle_timeout / NGTCP2_MILLISECONDS),
(curl_uint64_t)(keep_ns / NGTCP2_MILLISECONDS));
}
}
struct pkt_io_ctx;
static CURLcode cf_progress_ingress(struct Curl_cfilter *cf,
struct Curl_easy *data,
@ -262,6 +298,9 @@ static CURLcode h3_data_setup(struct Curl_cfilter *cf,
return CURLE_OUT_OF_MEMORY;
}
if(Curl_uint_hash_count(&ctx->streams) == 1)
cf_ngtcp2_setup_keep_alive(cf, data);
return CURLE_OK;
}
@ -297,6 +336,8 @@ static void h3_data_done(struct Curl_cfilter *cf, struct Curl_easy *data)
stream->id);
cf_ngtcp2_stream_close(cf, data, stream);
Curl_uint_hash_remove(&ctx->streams, data->mid);
if(!Curl_uint_hash_count(&ctx->streams))
cf_ngtcp2_setup_keep_alive(cf, data);
}
}
@ -418,7 +459,7 @@ static void quic_settings(struct cf_ngtcp2_ctx *ctx,
t->initial_max_stream_data_uni = ctx->max_stream_window;
t->initial_max_streams_bidi = QUIC_MAX_STREAMS;
t->initial_max_streams_uni = QUIC_MAX_STREAMS;
t->max_idle_timeout = (ctx->max_idle_ms * NGTCP2_MILLISECONDS);
t->max_idle_timeout = 0; /* no idle timeout from our side */
if(ctx->qlogfd != -1) {
s->qlog_write = qlog_callback;
}
@ -2674,21 +2715,12 @@ static bool cf_ngtcp2_conn_is_alive(struct Curl_cfilter *cf,
if(!ctx->qconn || ctx->shutdown_started)
goto out;
/* Both sides of the QUIC connection announce they max idle times in
* the transport parameters. Look at the minimum of both and if
* we exceed this, regard the connection as dead. The other side
* may have completely purged it and will no longer respond
* to any packets from us. */
/* We do not announce a max idle timeout, but when the peer does
* it will close the connection when it expires. */
rp = ngtcp2_conn_get_remote_transport_params(ctx->qconn);
if(rp) {
timediff_t idletime;
uint64_t idle_ms = ctx->max_idle_ms;
if(rp->max_idle_timeout &&
(rp->max_idle_timeout / NGTCP2_MILLISECONDS) < idle_ms)
idle_ms = (rp->max_idle_timeout / NGTCP2_MILLISECONDS);
idletime = Curl_timediff(Curl_now(), ctx->q.last_io);
if(idletime > 0 && (uint64_t)idletime > idle_ms)
if(rp && rp->max_idle_timeout) {
timediff_t idletime = Curl_timediff(Curl_now(), ctx->q.last_io);
if(idletime > 0 && (uint64_t)idletime > rp->max_idle_timeout)
goto out;
}

View file

@ -1239,17 +1239,6 @@ static const struct alpn_spec ALPN_SPEC_H3 = {
goto out;
}
#ifdef SSL_VALUE_QUIC_IDLE_TIMEOUT
/* Added in OpenSSL v3.3.x */
if(!SSL_set_feature_request_uint(ctx->tls.ossl.ssl,
SSL_VALUE_QUIC_IDLE_TIMEOUT,
CURL_QUIC_MAX_IDLE_MS)) {
CURL_TRC_CF(data, cf, "error setting idle timeout, ");
result = CURLE_FAILED_INIT;
goto out;
}
#endif
SSL_set_bio(ctx->tls.ossl.ssl, bio, bio);
bio = NULL;
SSL_set_connect_state(ctx->tls.ossl.ssl);

View file

@ -1321,7 +1321,6 @@ static const struct alpn_spec ALPN_SPEC_H3 = {
return CURLE_FAILED_INIT;
}
quiche_config_enable_pacing(ctx->cfg, FALSE);
quiche_config_set_max_idle_timeout(ctx->cfg, CURL_QUIC_MAX_IDLE_MS);
quiche_config_set_initial_max_data(ctx->cfg, (1 * 1024 * 1024)
/* (QUIC_MAX_STREAMS/2) * H3_STREAM_WINDOW_SIZE */);
quiche_config_set_initial_max_streams_bidi(ctx->cfg, QUIC_MAX_STREAMS);

View file

@ -31,8 +31,6 @@
#define MAX_PKT_BURST 10
#define MAX_UDP_PAYLOAD_SIZE 1452
/* Default QUIC connection timeout we announce from our side */
#define CURL_QUIC_MAX_IDLE_MS (120 * 1000)
struct cf_quic_ctx {
curl_socket_t sockfd; /* connected UDP socket */