From: Stefan Eissing Date: Tue, 10 Oct 2023 10:51:25 +0000 (+0200) Subject: quic: manage connection idle timeouts X-Git-Tag: curl-8_5_0~251 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=9cc57875777eacbb514bcc741350393999e14eca;p=thirdparty%2Fcurl.git quic: manage connection idle timeouts - 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 --- diff --git a/lib/vquic/curl_ngtcp2.c b/lib/vquic/curl_ngtcp2.c index 7d681e5853..7b765ac613 100644 --- a/lib/vquic/curl_ngtcp2.c +++ b/lib/vquic/curl_ngtcp2.c @@ -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; diff --git a/lib/vquic/curl_quiche.c b/lib/vquic/curl_quiche.c index 3f5d327430..6caa33ed31 100644 --- a/lib/vquic/curl_quiche.c +++ b/lib/vquic/curl_quiche.c @@ -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; diff --git a/lib/vquic/vquic.c b/lib/vquic/vquic.c index 9a1a1bbb3c..523b807bcc 100644 --- a/lib/vquic/vquic.c +++ b/lib/vquic/vquic.c @@ -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; } /* diff --git a/lib/vquic/vquic_int.h b/lib/vquic/vquic_int.h index dbcd009d72..a820f39aec 100644 --- a/lib/vquic/vquic_int.h +++ b/lib/vquic/vquic_int.h @@ -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);