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
This commit is contained in:
Stefan Eissing 2023-10-10 12:51:25 +02:00 committed by Daniel Stenberg
parent f1e05a6e6e
commit 9cc5787577
No known key found for this signature in database
GPG Key ID: 5CC908FDB71E12C2
4 changed files with 103 additions and 26 deletions

View 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;

View 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;

View 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;
}
/*

View 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);