]> git.ipfire.org Git - thirdparty/curl.git/commitdiff
quic: no local idle connection timeout, ngtcp2 keep-alive
authorStefan Eissing <stefan@eissing.org>
Tue, 15 Apr 2025 08:55:59 +0000 (10:55 +0200)
committerDaniel Stenberg <daniel@haxx.se>
Fri, 18 Apr 2025 21:52:51 +0000 (23:52 +0200)
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

lib/vquic/curl_ngtcp2.c
lib/vquic/curl_osslq.c
lib/vquic/curl_quiche.c
lib/vquic/vquic_int.h

index 9269323d178905de776713b7768f0803100c937c..8b6d53857017206fe2f01b6ca4a6ebc6ed068a5c 100644 (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;
   }
 
index 1065bb228453039e09c09ba742864a6ead7cd49d..824ae62ff6c54a62c43b99284345602a7ecb1749 100644 (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);
index f2b96d733d737a0c20c746042c5d9574781fa6ee..d5e045779b8c6909b6fca33172ccca5f6b6cd3e8 100644 (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);
index 894b7695c8a0ee6fd0610de3019fe3f2672d6f1d..0810b4dab15d3ed533f27ec367a747a8d911b5b6 100644 (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 */