]> git.ipfire.org Git - thirdparty/curl.git/commitdiff
quic: manage connection idle timeouts
authorStefan Eissing <stefan@eissing.org>
Tue, 10 Oct 2023 10:51:25 +0000 (12:51 +0200)
committerDaniel Stenberg <daniel@haxx.se>
Fri, 13 Oct 2023 21:15:47 +0000 (23:15 +0200)
- configure a 120s idle timeout on our side of the connection
- track the timestamp when actual socket IO happens
- check IO timestamp to our *and* the peer's idle timeouts
  in "is this connection alive" checks

Reported-by: calvin2021y on github
Fixes #12064
Closes #12077

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

index 7d681e58530342d6bcc0dfd3256d904db0dd80e2..7b765ac61367f21a416cdb031ec722b3b491bc8f 100644 (file)
@@ -78,7 +78,6 @@
 
 #define QUIC_MAX_STREAMS (256*1024)
 #define QUIC_MAX_DATA (1*1024*1024)
-#define QUIC_IDLE_TIMEOUT (60*NGTCP2_SECONDS)
 #define QUIC_HANDSHAKE_TIMEOUT (10*NGTCP2_SECONDS)
 
 /* A stream window is the maximum amount we need to buffer for
@@ -161,6 +160,7 @@ struct cf_ngtcp2_ctx {
   struct curltime reconnect_at;      /* time the next attempt should start */
   struct bufc_pool stream_bufcp;     /* chunk pool for streams */
   size_t max_stream_window;          /* max flow window for one stream */
+  uint64_t max_idle_ms;              /* max idle time for QUIC connection */
   int qlogfd;
   BIT(got_first_byte);               /* if first byte was received */
 #ifdef USE_OPENSSL
@@ -261,10 +261,14 @@ struct pkt_io_ctx {
   ngtcp2_path_storage ps;
 };
 
-static ngtcp2_tstamp timestamp(void)
+static void pktx_update_time(struct pkt_io_ctx *pktx,
+                             struct Curl_cfilter *cf)
 {
-  struct curltime ct = Curl_now();
-  return ct.tv_sec * NGTCP2_SECONDS + ct.tv_usec * NGTCP2_MICROSECONDS;
+  struct cf_ngtcp2_ctx *ctx = cf->ctx;
+
+  vquic_ctx_update_time(&ctx->q);
+  pktx->ts = ctx->q.last_op.tv_sec * NGTCP2_SECONDS +
+             ctx->q.last_op.tv_usec * NGTCP2_MICROSECONDS;
 }
 
 static void pktx_init(struct pkt_io_ctx *pktx,
@@ -273,9 +277,9 @@ static void pktx_init(struct pkt_io_ctx *pktx,
 {
   pktx->cf = cf;
   pktx->data = data;
-  pktx->ts = timestamp();
   pktx->pkt_count = 0;
   ngtcp2_path_storage_zero(&pktx->ps);
+  pktx_update_time(pktx, cf);
 }
 
 static CURLcode cf_progress_ingress(struct Curl_cfilter *cf,
@@ -354,7 +358,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 = QUIC_IDLE_TIMEOUT;
+  t->max_idle_timeout = (ctx->max_idle_ms * NGTCP2_MILLISECONDS);
   if(ctx->qlogfd != -1) {
     s->qlog_write = qlog_callback;
   }
@@ -1038,7 +1042,7 @@ static CURLcode check_and_set_expiry(struct Curl_cfilter *cf,
     pktx = &local_pktx;
   }
   else {
-    pktx->ts = timestamp();
+    pktx_update_time(pktx, cf);
   }
 
   expiry = ngtcp2_conn_get_expiry(ctx->qconn);
@@ -1993,7 +1997,7 @@ static CURLcode cf_progress_ingress(struct Curl_cfilter *cf,
     pktx = &local_pktx;
   }
   else {
-    pktx->ts = timestamp();
+    pktx_update_time(pktx, cf);
   }
 
 #ifdef USE_OPENSSL
@@ -2145,7 +2149,7 @@ static CURLcode cf_progress_egress(struct Curl_cfilter *cf,
     pktx = &local_pktx;
   }
   else {
-    pktx->ts = timestamp();
+    pktx_update_time(pktx, cf);
     ngtcp2_path_storage_zero(&pktx->ps);
   }
 
@@ -2358,15 +2362,15 @@ static void cf_ngtcp2_close(struct Curl_cfilter *cf, struct Curl_easy *data)
   CF_DATA_SAVE(save, cf, data);
   if(ctx && ctx->qconn) {
     char buffer[NGTCP2_MAX_UDP_PAYLOAD_SIZE];
-    ngtcp2_tstamp ts;
+    struct pkt_io_ctx pktx;
     ngtcp2_ssize rc;
 
     CURL_TRC_CF(data, cf, "close");
-    ts = timestamp();
+    pktx_init(&pktx, cf, data);
     rc = ngtcp2_conn_write_connection_close(ctx->qconn, NULL, /* path */
                                             NULL, /* pkt_info */
                                             (uint8_t *)buffer, sizeof(buffer),
-                                            &ctx->last_error, ts);
+                                            &ctx->last_error, pktx.ts);
     if(rc > 0) {
       while((send(ctx->q.sockfd, buffer, (SEND_TYPE_ARG3)rc, 0) == -1) &&
             SOCKERRNO == EINTR);
@@ -2411,6 +2415,7 @@ static CURLcode cf_connect_start(struct Curl_cfilter *cf,
 
   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);
 
@@ -2657,9 +2662,32 @@ static bool cf_ngtcp2_conn_is_alive(struct Curl_cfilter *cf,
                                     struct Curl_easy *data,
                                     bool *input_pending)
 {
+  struct cf_ngtcp2_ctx *ctx = cf->ctx;
   bool alive = TRUE;
+  const ngtcp2_transport_params *rp;
 
   *input_pending = FALSE;
+  if(!ctx->qconn)
+    return FALSE;
+
+  /* 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. */
+  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)
+      return FALSE;
+  }
+
   if(!cf->next || !cf->next->cft->is_alive(cf->next, data, input_pending))
     return FALSE;
 
index 3f5d32743050350a6fb7ea7910553ce9ddb79867..6caa33ed31a05e57004117b1c6b689b39203a021 100644 (file)
@@ -58,7 +58,6 @@
 /* #define DEBUG_QUICHE */
 
 #define QUIC_MAX_STREAMS              (100)
-#define QUIC_IDLE_TIMEOUT        (60 * 1000) /* milliseconds */
 
 #define H3_STREAM_WINDOW_SIZE  (128 * 1024)
 #define H3_STREAM_CHUNK_SIZE    (16 * 1024)
@@ -105,6 +104,7 @@ struct cf_quiche_ctx {
   struct curltime reconnect_at;      /* time the next attempt should start */
   struct bufc_pool stream_bufcp;     /* chunk pool for streams */
   curl_off_t data_recvd;
+  uint64_t max_idle_ms;              /* max idle time for QUIC conn */
   size_t sends_on_hold;              /* # of streams with SEND_HOLD set */
   BIT(goaway);                       /* got GOAWAY from server */
   BIT(got_first_byte);               /* if first byte was received */
@@ -883,6 +883,8 @@ static ssize_t cf_quiche_recv(struct Curl_cfilter *cf, struct Curl_easy *data,
   ssize_t nread = -1;
   CURLcode result;
 
+  vquic_ctx_update_time(&ctx->q);
+
   if(!stream) {
     *err = CURLE_RECV_ERROR;
     return -1;
@@ -1081,6 +1083,8 @@ static ssize_t cf_quiche_send(struct Curl_cfilter *cf, struct Curl_easy *data,
   CURLcode result;
   ssize_t nwritten;
 
+  vquic_ctx_update_time(&ctx->q);
+
   *err = cf_process_ingress(cf, data);
   if(*err) {
     nwritten = -1;
@@ -1345,6 +1349,7 @@ static CURLcode cf_connect_start(struct Curl_cfilter *cf,
     debug_log_init = 1;
   }
 #endif
+  ctx->max_idle_ms = CURL_QUIC_MAX_IDLE_MS;
   Curl_bufcp_init(&ctx->stream_bufcp, H3_STREAM_CHUNK_SIZE,
                   H3_STREAM_POOL_SPARES);
   ctx->data_recvd = 0;
@@ -1359,7 +1364,7 @@ static CURLcode cf_connect_start(struct Curl_cfilter *cf,
     return CURLE_FAILED_INIT;
   }
   quiche_config_enable_pacing(ctx->cfg, false);
-  quiche_config_set_max_idle_timeout(ctx->cfg, QUIC_IDLE_TIMEOUT);
+  quiche_config_set_max_idle_timeout(ctx->cfg, ctx->max_idle_ms * 1000);
   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);
@@ -1449,7 +1454,6 @@ static CURLcode cf_quiche_connect(struct Curl_cfilter *cf,
 {
   struct cf_quiche_ctx *ctx = cf->ctx;
   CURLcode result = CURLE_OK;
-  struct curltime now;
 
   if(cf->connected) {
     *done = TRUE;
@@ -1464,9 +1468,10 @@ static CURLcode cf_quiche_connect(struct Curl_cfilter *cf,
   }
 
   *done = FALSE;
-  now = Curl_now();
+  vquic_ctx_update_time(&ctx->q);
 
-  if(ctx->reconnect_at.tv_sec && Curl_timediff(now, ctx->reconnect_at) < 0) {
+  if(ctx->reconnect_at.tv_sec &&
+     Curl_timediff(ctx->q.last_op, ctx->reconnect_at) < 0) {
     /* Not time yet to attempt the next connect */
     CURL_TRC_CF(data, cf, "waiting for reconnect time");
     goto out;
@@ -1476,7 +1481,7 @@ static CURLcode cf_quiche_connect(struct Curl_cfilter *cf,
     result = cf_connect_start(cf, data);
     if(result)
       goto out;
-    ctx->started_at = now;
+    ctx->started_at = ctx->q.last_op;
     result = cf_flush_egress(cf, data);
     /* we do not expect to be able to recv anything yet */
     goto out;
@@ -1491,9 +1496,9 @@ static CURLcode cf_quiche_connect(struct Curl_cfilter *cf,
     goto out;
 
   if(quiche_conn_is_established(ctx->qconn)) {
+    ctx->handshake_at = ctx->q.last_op;
     CURL_TRC_CF(data, cf, "handshake complete after %dms",
-                (int)Curl_timediff(now, ctx->started_at));
-    ctx->handshake_at = now;
+                (int)Curl_timediff(ctx->handshake_at, ctx->started_at));
     result = cf_verify_peer(cf, data);
     if(!result) {
       CURL_TRC_CF(data, cf, "peer verified");
@@ -1550,6 +1555,7 @@ static void cf_quiche_close(struct Curl_cfilter *cf, struct Curl_easy *data)
 
   if(ctx) {
     if(ctx->qconn) {
+      vquic_ctx_update_time(&ctx->q);
       (void)quiche_conn_close(ctx->qconn, TRUE, 0, NULL, 0);
       /* flushing the egress is not a failsafe way to deliver all the
          outstanding packets, but we also don't want to get stuck here... */
@@ -1617,9 +1623,31 @@ static bool cf_quiche_conn_is_alive(struct Curl_cfilter *cf,
                                     struct Curl_easy *data,
                                     bool *input_pending)
 {
+  struct cf_quiche_ctx *ctx = cf->ctx;
   bool alive = TRUE;
 
   *input_pending = FALSE;
+  if(!ctx->qconn)
+    return FALSE;
+
+  /* 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. */
+  {
+    quiche_stats qstats;
+    timediff_t idletime;
+    uint64_t idle_ms = ctx->max_idle_ms;
+
+    quiche_conn_stats(ctx->qconn, &qstats);
+    if(qstats.peer_max_idle_timeout && qstats.peer_max_idle_timeout < idle_ms)
+      idle_ms = qstats.peer_max_idle_timeout;
+    idletime = Curl_timediff(Curl_now(), cf->conn->lastused);
+    if(idletime > 0 && (uint64_t)idletime > idle_ms)
+      return FALSE;
+  }
+
   if(!cf->next || !cf->next->cft->is_alive(cf->next, data, input_pending))
     return FALSE;
 
index 9a1a1bbb3c39e926f8440f5aa93584bf7ea0f60e..523b807bcc8bb1ddb729037dcabebc38970224b5 100644 (file)
@@ -100,6 +100,7 @@ CURLcode vquic_ctx_init(struct cf_quic_ctx *qctx)
     }
   }
 #endif
+  vquic_ctx_update_time(qctx);
 
   return CURLE_OK;
 }
@@ -109,6 +110,11 @@ void vquic_ctx_free(struct cf_quic_ctx *qctx)
   Curl_bufq_free(&qctx->sendbuf);
 }
 
+void vquic_ctx_update_time(struct cf_quic_ctx *qctx)
+{
+  qctx->last_op = Curl_now();
+}
+
 static CURLcode send_packet_no_gso(struct Curl_cfilter *cf,
                                    struct Curl_easy *data,
                                    struct cf_quic_ctx *qctx,
@@ -242,6 +248,7 @@ static CURLcode vquic_send_packets(struct Curl_cfilter *cf,
                                    const uint8_t *pkt, size_t pktlen,
                                    size_t gsolen, size_t *psent)
 {
+  CURLcode result;
 #ifdef DEBUGBUILD
   /* simulate network blocking/partial writes */
   if(qctx->wblock_percent > 0) {
@@ -254,10 +261,14 @@ static CURLcode vquic_send_packets(struct Curl_cfilter *cf,
   }
 #endif
   if(qctx->no_gso && pktlen > gsolen) {
-    return send_packet_no_gso(cf, data, qctx, pkt, pktlen, gsolen, psent);
+    result = send_packet_no_gso(cf, data, qctx, pkt, pktlen, gsolen, psent);
   }
-
-  return do_sendmsg(cf, data, qctx, pkt, pktlen, gsolen, psent);
+  else {
+    result = do_sendmsg(cf, data, qctx, pkt, pktlen, gsolen, psent);
+  }
+  if(!result)
+    qctx->last_io = qctx->last_op;
+  return result;
 }
 
 CURLcode vquic_flush(struct Curl_cfilter *cf, struct Curl_easy *data,
@@ -524,13 +535,17 @@ CURLcode vquic_recv_packets(struct Curl_cfilter *cf,
                             size_t max_pkts,
                             vquic_recv_pkt_cb *recv_cb, void *userp)
 {
+  CURLcode result;
 #if defined(HAVE_SENDMMSG)
-  return recvmmsg_packets(cf, data, qctx, max_pkts, recv_cb, userp);
+  result = recvmmsg_packets(cf, data, qctx, max_pkts, recv_cb, userp);
 #elif defined(HAVE_SENDMSG)
-  return recvmsg_packets(cf, data, qctx, max_pkts, recv_cb, userp);
+  result = recvmsg_packets(cf, data, qctx, max_pkts, recv_cb, userp);
 #else
-  return recvfrom_packets(cf, data, qctx, max_pkts, recv_cb, userp);
+  result = recvfrom_packets(cf, data, qctx, max_pkts, recv_cb, userp);
 #endif
+  if(!result)
+    qctx->last_io = qctx->last_op;
+  return result;
 }
 
 /*
index dbcd009d72121d211582e9b183f7f89c266dc735..a820f39aec48a673379ed2106a8766e8c6cf3f91 100644 (file)
@@ -31,6 +31,8 @@
 
 #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 */
@@ -38,6 +40,8 @@ struct cf_quic_ctx {
   socklen_t local_addrlen; /* length of local address */
 
   struct bufq sendbuf; /* buffer for sending one or more packets */
+  struct curltime last_op; /* last (attempted) send/recv operation */
+  struct curltime last_io; /* last successful socket IO */
   size_t gsolen; /* length of individual packets in send buf */
   size_t split_len; /* if != 0, buffer length after which GSO differs */
   size_t split_gsolen; /* length of individual packets after split_len */
@@ -50,6 +54,8 @@ struct cf_quic_ctx {
 CURLcode vquic_ctx_init(struct cf_quic_ctx *qctx);
 void vquic_ctx_free(struct cf_quic_ctx *qctx);
 
+void vquic_ctx_update_time(struct cf_quic_ctx *qctx);
+
 void vquic_push_blocked_pkt(struct Curl_cfilter *cf,
                             struct cf_quic_ctx *qctx,
                             const uint8_t *pkt, size_t pktlen, size_t gsolen);