#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
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
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,
{
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,
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;
}
pktx = &local_pktx;
}
else {
- pktx->ts = timestamp();
+ pktx_update_time(pktx, cf);
}
expiry = ngtcp2_conn_get_expiry(ctx->qconn);
pktx = &local_pktx;
}
else {
- pktx->ts = timestamp();
+ pktx_update_time(pktx, cf);
}
#ifdef USE_OPENSSL
pktx = &local_pktx;
}
else {
- pktx->ts = timestamp();
+ pktx_update_time(pktx, cf);
ngtcp2_path_storage_zero(&pktx->ps);
}
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);
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);
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;
/* #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)
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 */
ssize_t nread = -1;
CURLcode result;
+ vquic_ctx_update_time(&ctx->q);
+
if(!stream) {
*err = CURLE_RECV_ERROR;
return -1;
CURLcode result;
ssize_t nwritten;
+ vquic_ctx_update_time(&ctx->q);
+
*err = cf_process_ingress(cf, data);
if(*err) {
nwritten = -1;
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;
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);
{
struct cf_quiche_ctx *ctx = cf->ctx;
CURLcode result = CURLE_OK;
- struct curltime now;
if(cf->connected) {
*done = TRUE;
}
*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;
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;
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");
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... */
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;
}
}
#endif
+ vquic_ctx_update_time(qctx);
return CURLE_OK;
}
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,
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) {
}
#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,
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;
}
/*
#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 */
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 */
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);