/***************************************************************************
 *                                  _   _ ____  _
 *  Project                     ___| | | |  _ \| |
 *                             / __| | | | |_) | |
 *                            | (__| |_| |  _ <| |___
 *                             \___|\___/|_| \_\_____|
 *
 * Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution. The terms
 * are also available at https://curl.se/docs/copyright.html.
 *
 * You may opt to use, copy, modify, merge, publish, distribute and/or sell
 * copies of the Software, and permit persons to whom the Software is
 * furnished to do so, under the terms of the COPYING file.
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 * SPDX-License-Identifier: curl
 *
 ***************************************************************************/
#include "curl_setup.h"

#if !defined(CURL_DISABLE_HTTP) && !defined(CURL_DISABLE_PROXY) && \
  defined(USE_PROXY_HTTP3) && defined(USE_NGHTTP3) &&              \
  defined(USE_NGTCP2) && defined(USE_OPENSSL)

#include <ngtcp2/ngtcp2.h>
#include <ngtcp2/ngtcp2_crypto.h>

#ifdef USE_OPENSSL
#include <openssl/err.h>
#if defined(OPENSSL_IS_AWSLC) || defined(OPENSSL_IS_BORINGSSL)
#include <ngtcp2/ngtcp2_crypto_boringssl.h>
#elif defined(OPENSSL_QUIC_API2)
#include <ngtcp2/ngtcp2_crypto_ossl.h>
#else
#include <ngtcp2/ngtcp2_crypto_quictls.h>
#endif
#include "vtls/openssl.h"
#endif

#include <nghttp3/nghttp3.h>

#include "urldata.h"
#include "url.h"
#include "uint-hash.h"
#include "curl_trc.h"
#include "rand.h"
#include "hash.h"
#include "sendf.h"
#include "multiif.h"
#include "cfilters.h"
#include "cf-capsule.h"
#include "cf-socket.h"
#include "connect.h"
#include "progress.h"
#include "curlx/fopen.h"
#include "curlx/dynbuf.h"
#include "dynhds.h"
#include "http_proxy.h"
#include "select.h"
#include "vquic/vquic.h"
#include "vquic/vquic_int.h"
#include "vquic/vquic-tls.h"
#include "vtls/vtls.h"
#include "vtls/vtls_scache.h"
#include "vquic/cf-ngtcp2-proxy.h"
#include "capsule.h"

/* A stream window is the maximum amount we need to buffer for
 * each active transfer. We use HTTP/3 flow control and only ACK
 * when we take things out of the buffer.
 * Chunk size is large enough to take a full DATA frame */
#define PROXY_H3_STREAM_WINDOW_SIZE (128 * 1024)
#define PROXY_H3_STREAM_WINDOW_SIZE_MAX (10 * 1024 * 1024)
#define PROXY_H3_STREAM_CHUNK_SIZE (16 * 1024)

/* The pool keeps spares around and half of a full stream window
 * seems good. More does not seem to improve performance.
 * The benefit of the pool is that stream buffers do not keep
 * spares. Memory consumption goes down when streams run empty,
 * have a large upload done, etc. */
#define PROXY_H3_STREAM_POOL_SPARES \
  ((PROXY_H3_STREAM_WINDOW_SIZE / PROXY_H3_STREAM_CHUNK_SIZE) / 2)

#define PROXY_H3_STREAM_RECV_CHUNKS \
  (PROXY_H3_STREAM_WINDOW_SIZE / PROXY_H3_STREAM_CHUNK_SIZE)
#define PROXY_H3_STREAM_SEND_CHUNKS \
  (PROXY_H3_STREAM_WINDOW_SIZE / PROXY_H3_STREAM_CHUNK_SIZE)

#define PROXY_QUIC_MAX_STREAMS       (256 * 1024)
#define PROXY_QUIC_HANDSHAKE_TIMEOUT (10 * NGTCP2_SECONDS)

typedef enum {
  H3_TUNNEL_INIT,     /* init/default/no tunnel state */
  H3_TUNNEL_CONNECT,  /* CONNECT request is being sent */
  H3_TUNNEL_RESPONSE, /* CONNECT response received completely */
  H3_TUNNEL_ESTABLISHED,
  H3_TUNNEL_FAILED
} h3_tunnel_state;

struct h3_proxy_stream_ctx;

struct h3_tunnel_stream {
  struct http_resp *resp;
  char *authority;
  struct h3_proxy_stream_ctx *stream;
  int64_t stream_id;
  h3_tunnel_state state;
  BIT(has_final_response);
  BIT(closed);
};

static CURLcode h3_tunnel_stream_init(struct h3_tunnel_stream *ts,
                                      struct Curl_peer *dest)
{
  ts->state = H3_TUNNEL_INIT;
  ts->stream_id = -1;
  ts->has_final_response = FALSE;

  /* host:port with IPv6 support */
  ts->authority = curl_maprintf("%s%s%s:%u", dest->ipv6 ? "[" : "",
                                dest->hostname,
                                dest->ipv6 ? "]" : "",
                                dest->port);
  if(!ts->authority)
    return CURLE_OUT_OF_MEMORY;

  return CURLE_OK;
}

static void h3_tunnel_stream_reset(struct h3_tunnel_stream *ts)
{
  Curl_http_resp_free(ts->resp);
  ts->resp = NULL;
  ts->stream = NULL;
  ts->stream_id = -1;
  ts->has_final_response = FALSE;
  ts->closed = FALSE;
  ts->state = H3_TUNNEL_INIT;
}

static void h3_tunnel_stream_clear(struct h3_tunnel_stream *ts)
{
  Curl_http_resp_free(ts->resp);
  curlx_safefree(ts->authority);
  memset(ts, 0, sizeof(*ts));
  ts->state = H3_TUNNEL_INIT;
}

static void h3_tunnel_go_state(struct Curl_cfilter *cf,
                               struct h3_tunnel_stream *ts,
                               h3_tunnel_state new_state,
                               struct Curl_easy *data,
                               bool udp_tunnel)
{
  (void)cf;
  (void)udp_tunnel;

  if(ts->state == new_state)
    return;
  /* leaving this one */
  switch(ts->state) {
  case H3_TUNNEL_CONNECT:
    data->req.ignorebody = FALSE;
    break;
  default:
    break;
  }
  /* entering this one */
  switch(new_state) {
  case H3_TUNNEL_INIT:
    CURL_TRC_CF(data, cf, "[%" PRId64 "] new tunnel state 'init'",
                ts->stream_id);
    h3_tunnel_stream_reset(ts);
    break;

  case H3_TUNNEL_CONNECT:
    CURL_TRC_CF(data, cf, "[%" PRId64 "] new tunnel state 'connect'",
                ts->stream_id);
    ts->state = H3_TUNNEL_CONNECT;
    break;

  case H3_TUNNEL_RESPONSE:
    CURL_TRC_CF(data, cf, "[%" PRId64 "] new tunnel state 'response'",
                ts->stream_id);
    ts->state = H3_TUNNEL_RESPONSE;
    break;

  case H3_TUNNEL_ESTABLISHED:
    CURL_TRC_CF(data, cf, "[%" PRId64 "] new tunnel state 'established'",
                ts->stream_id);
    infof(data, "CONNECT%s phase completed for HTTP/3 proxy",
          udp_tunnel ? "-UDP" : "");
    data->state.authproxy.done = TRUE;
    data->state.authproxy.multipass = FALSE;
    FALLTHROUGH();
  case H3_TUNNEL_FAILED:
    if(new_state == H3_TUNNEL_FAILED)
      CURL_TRC_CF(data, cf, "[%" PRId64 "] new tunnel state 'failed'",
                  ts->stream_id);
    ts->state = new_state;
    /* If a proxy-authorization header was used for the proxy, then we should
       make sure that it is not accidentally used for the document request
       after we have connected. So let's free and clear it here. */
    curlx_safefree(data->req.hd_proxy_auth);
    break;
  }
}

struct cf_ngtcp2_proxy_ctx {
  struct cf_quic_ctx q;
  struct ssl_peer peer;
  struct curl_tls_ctx tls;
#ifdef OPENSSL_QUIC_API2
  ngtcp2_crypto_ossl_ctx *ossl_ctx;
#endif
  ngtcp2_path connected_path;
  ngtcp2_conn *qconn;
  ngtcp2_cid dcid;
  ngtcp2_cid scid;
  uint32_t version;
  ngtcp2_settings settings;
  ngtcp2_transport_params transport_params;
  ngtcp2_ccerr last_error;
  ngtcp2_crypto_conn_ref conn_ref;
  struct cf_call_data call_data;
  nghttp3_conn *h3conn;
  nghttp3_settings h3settings;
  struct curltime started_at;       /* time the current attempt started */
  struct curltime handshake_at;     /* time connect handshake finished */
  struct bufc_pool stream_bufcp;    /* chunk pool for streams */
  struct dynbuf scratch;            /* temp buffer for header construction */
  struct uint_hash streams;         /* hash data->mid to h3_proxy_stream_ctx */
  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
                                       server on session reuse */
  size_t earlydata_skip;            /* sending bytes to skip when earlydata
                                       is accepted by peer */
  CURLcode tls_vrfy_result;         /* result of TLS peer verification */
  int qlogfd;
  BIT(initialized);
  BIT(tls_handshake_complete);      /* TLS handshake is done */
  BIT(use_earlydata);               /* Using 0RTT data */
  BIT(earlydata_accepted);          /* 0RTT was accepted by server */
  BIT(shutdown_started);            /* queued shutdown packets */
};

struct cf_h3_proxy_ctx {
  struct cf_ngtcp2_proxy_ctx *ngtcp2_ctx;
  struct cf_call_data call_data;  /* fallback before backend ctx exists */
  struct bufq inbufq;             /* network receive buffer */
  struct Curl_peer *dest;         /* where to tunnel to */
  struct h3_tunnel_stream tunnel; /* our tunnel CONNECT stream */
  BIT(connected);
  BIT(udp_tunnel);
};

/**
 * All about the H3 internals of a stream
 */
struct h3_proxy_stream_ctx {
  int64_t id;                   /* HTTP/3 stream identifier */
  struct bufq sendbuf;          /* h3 request body */
  size_t sendbuf_len_in_flight; /* sendbuf amount "in flight" */
  uint64_t error3;              /* HTTP/3 stream error code */
  curl_off_t upload_left;       /* number of request bytes left to upload */
  curl_off_t tun_data_recvd;    /* number of bytes received over tunnel */
  uint64_t rx_offset;           /* current receive offset */
  uint64_t rx_offset_max;       /* allowed receive offset */
  uint64_t window_size_max;     /* max flow control window set for stream */
  int status_code;              /* HTTP status code */
  CURLcode xfer_result;         /* result from xfer_resp_write(_hd) */
  BIT(resp_hds_complete);       /* we have a complete, final response */
  BIT(closed);                  /* TRUE on stream close */
  BIT(reset);                   /* TRUE on stream reset */
  BIT(send_closed);             /* stream is local closed */
  BIT(quic_flow_blocked);       /* stream is blocked by QUIC flow control */
};

#define H3_PROXY_STREAM_CTX(ctx, data)                                 \
  ((data) ? Curl_uint32_hash_get(&(ctx)->streams, (data)->mid) : NULL)

#define H3_STREAM_ID(stream) ((stream)->id)

static void h3_proxy_stream_ctx_free(struct h3_proxy_stream_ctx *stream)
{
  Curl_bufq_free(&stream->sendbuf);
  curlx_free(stream);
}

static void h3_proxy_stream_hash_free(unsigned int id, void *stream)
{
  (void)id;
  DEBUGASSERT(stream);
  h3_proxy_stream_ctx_free((struct h3_proxy_stream_ctx *)stream);
}

static void cf_ngtcp2_proxy_ctx_init(struct cf_ngtcp2_proxy_ctx *ctx)
{
  DEBUGASSERT(!ctx->initialized);
  ctx->q.sockfd = CURL_SOCKET_BAD;
  ctx->qlogfd = -1;
  ctx->version = NGTCP2_PROTO_VER_MAX;
  Curl_bufcp_init(&ctx->stream_bufcp, PROXY_H3_STREAM_CHUNK_SIZE,
                  PROXY_H3_STREAM_POOL_SPARES);
  curlx_dyn_init(&ctx->scratch, CURL_MAX_HTTP_HEADER);
  Curl_uint32_hash_init(&ctx->streams, 63, h3_proxy_stream_hash_free);
  ctx->initialized = TRUE;
}

static void cf_ngtcp2_proxy_ctx_free(struct cf_ngtcp2_proxy_ctx *ctx)
{
  if(ctx && ctx->initialized) {
    Curl_vquic_tls_cleanup(&ctx->tls);
    vquic_ctx_free(&ctx->q);
    Curl_bufcp_free(&ctx->stream_bufcp);
    curlx_dyn_free(&ctx->scratch);
    Curl_uint32_hash_destroy(&ctx->streams);
    Curl_ssl_peer_cleanup(&ctx->peer);
  }
  curlx_free(ctx);
}

static void cf_ngtcp2_proxy_ctx_close(struct cf_ngtcp2_proxy_ctx *ctx)
{
  struct cf_call_data save = ctx->call_data;

  if(!ctx->initialized)
    return;
  if(ctx->qlogfd != -1) {
    curlx_close(ctx->qlogfd);
  }
  ctx->qlogfd = -1;
  Curl_vquic_tls_cleanup(&ctx->tls);
  Curl_ssl_peer_cleanup(&ctx->peer);
  vquic_ctx_free(&ctx->q);
  if(ctx->h3conn) {
    nghttp3_conn_del(ctx->h3conn);
    ctx->h3conn = NULL;
  }
  if(ctx->qconn) {
    ngtcp2_conn_del(ctx->qconn);
    ctx->qconn = NULL;
  }
#ifdef OPENSSL_QUIC_API2
  if(ctx->ossl_ctx) {
    ngtcp2_crypto_ossl_ctx_del(ctx->ossl_ctx);
    ctx->ossl_ctx = NULL;
  }
#endif
  ctx->call_data = save;
}

static void cf_ngtcp2_proxy_setup_keep_alive(struct Curl_cfilter *cf,
                                             struct Curl_easy *data)
{
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_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_uint32_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 %" PRIu64 "ms, "
                "set keep-alive to %" PRIu64 " ms.",
                rp->max_idle_timeout / NGTCP2_MILLISECONDS,
                keep_ns / NGTCP2_MILLISECONDS);
  }
}

struct proxy_pkt_io_ctx {
  struct Curl_cfilter *cf;
  struct Curl_easy *data;
  ngtcp2_tstamp ts;
  ngtcp2_path_storage ps;
};

static void proxy_pktx_update_time(struct proxy_pkt_io_ctx *pktx,
                                   struct Curl_cfilter *cf)
{
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
  const struct curltime *pnow = Curl_pgrs_now(pktx->data);

  vquic_ctx_update_time(&ctx->q, pnow);
  pktx->ts = ((ngtcp2_tstamp)pnow->tv_sec * NGTCP2_SECONDS) +
             ((ngtcp2_tstamp)pnow->tv_usec * NGTCP2_MICROSECONDS);
}

static void proxy_pktx_init(struct proxy_pkt_io_ctx *pktx,
                            struct Curl_cfilter *cf,
                            struct Curl_easy *data)
{
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
  const struct curltime *pnow = Curl_pgrs_now(data);

  pktx->cf = cf;
  pktx->data = data;
  ngtcp2_path_storage_zero(&pktx->ps);
  vquic_ctx_set_time(&ctx->q, pnow);
  pktx->ts = ((ngtcp2_tstamp)pnow->tv_sec * NGTCP2_SECONDS) +
             ((ngtcp2_tstamp)pnow->tv_usec * NGTCP2_MICROSECONDS);
}

static ngtcp2_conn *proxy_get_conn(ngtcp2_crypto_conn_ref *conn_ref)
{
  struct Curl_cfilter *cf = conn_ref->user_data;
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
  return ctx->qconn;
}

#ifdef DEBUG_NGTCP2
static void proxy_quic_printf(void *user_data, const char *fmt, ...)
{
  va_list ap;
  (void)user_data;
  va_start(ap, fmt);
  curl_mvfprintf(stderr, fmt, ap);
  va_end(ap);
  curl_mfprintf(stderr, "\n");
}
#endif

static void proxy_qlog_callback(void *user_data, uint32_t flags,
                                const void *data, size_t datalen)
{
  struct Curl_cfilter *cf = user_data;
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
  (void)flags;
  if(ctx->qlogfd != -1) {
    ssize_t rc = write(ctx->qlogfd, data, datalen);
    if(rc == -1) {
      /* on write error, stop further write attempts */
      curlx_close(ctx->qlogfd);
      ctx->qlogfd = -1;
    }
  }
}

static void quic_settings_proxy(struct cf_ngtcp2_proxy_ctx *ctx,
                                struct Curl_easy *data,
                                struct proxy_pkt_io_ctx *pktx)
{
  ngtcp2_settings *s = &ctx->settings;
  ngtcp2_transport_params *t = &ctx->transport_params;

  ngtcp2_settings_default(s);
  ngtcp2_transport_params_default(t);
#ifdef DEBUG_NGTCP2
  s->log_printf = proxy_quic_printf;
#else
  s->log_printf = NULL;
#endif

  s->initial_ts = pktx->ts;
  s->handshake_timeout = (data->set.connecttimeout > 0) ?
    data->set.connecttimeout * NGTCP2_MILLISECONDS :
    PROXY_QUIC_HANDSHAKE_TIMEOUT;
  s->max_window = 100 * PROXY_H3_STREAM_WINDOW_SIZE;
  s->max_stream_window = 10 * PROXY_H3_STREAM_WINDOW_SIZE;
  s->no_pmtud = FALSE;
#ifdef NGTCP2_SETTINGS_V3
  /* try ten times the ngtcp2 defaults here for problems with Caddy */
  s->glitch_ratelim_burst = 1000 * 10;
  s->glitch_ratelim_rate = 33 * 10;
#endif
  t->initial_max_data = 10 * PROXY_H3_STREAM_WINDOW_SIZE;
  t->initial_max_stream_data_bidi_local = PROXY_H3_STREAM_WINDOW_SIZE;
  t->initial_max_stream_data_bidi_remote = PROXY_H3_STREAM_WINDOW_SIZE;
  t->initial_max_stream_data_uni = PROXY_H3_STREAM_WINDOW_SIZE;
  t->initial_max_streams_bidi = PROXY_QUIC_MAX_STREAMS;
  t->initial_max_streams_uni = PROXY_QUIC_MAX_STREAMS;
  t->max_idle_timeout = 0; /* no idle timeout from our side */
  if(ctx->qlogfd != -1) {
    s->qlog_write = proxy_qlog_callback;
  }
}

static void cf_ngtcp2_proxy_conn_close(struct Curl_cfilter *cf,
                                       struct Curl_easy *data);

static bool cf_ngtcp2_proxy_err_is_fatal(int code)
{
  return (NGTCP2_ERR_FATAL >= code) ||
         (NGTCP2_ERR_DROP_CONN == code) ||
         (NGTCP2_ERR_IDLE_CLOSE == code);
}

static void cf_ngtcp2_proxy_err_set(struct Curl_cfilter *cf,
                                    struct Curl_easy *data, int code)
{
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
  if(!ctx->last_error.error_code) {
    if(NGTCP2_ERR_CRYPTO == code) {
      ngtcp2_ccerr_set_tls_alert(&ctx->last_error,
                                 ngtcp2_conn_get_tls_alert(ctx->qconn),
                                 NULL, 0);
    }
    else {
      ngtcp2_ccerr_set_liberr(&ctx->last_error, code, NULL, 0);
    }
  }
  if(cf_ngtcp2_proxy_err_is_fatal(code))
    cf_ngtcp2_proxy_conn_close(cf, data);
}

static bool cf_ngtcp2_proxy_h3_err_is_fatal(int code)
{
  return (NGHTTP3_ERR_FATAL >= code) ||
         (NGHTTP3_ERR_H3_CLOSED_CRITICAL_STREAM == code);
}

static void cf_ngtcp2_proxy_h3_err_set(struct Curl_cfilter *cf,
                                       struct Curl_easy *data, int code)
{
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
  if(!ctx->last_error.error_code) {
    ngtcp2_ccerr_set_application_error(&ctx->last_error,
      nghttp3_err_infer_quic_app_error_code(code), NULL, 0);
  }
  if(cf_ngtcp2_proxy_h3_err_is_fatal(code))
    cf_ngtcp2_proxy_conn_close(cf, data);
}

/* How to access `call_data` from a cf_h3_proxy filter */
static struct cf_call_data *cf_h3_proxy_call_data(struct Curl_cfilter *cf)
{
  struct cf_h3_proxy_ctx *ctx = cf ? cf->ctx : NULL;
  static struct cf_call_data no_ctx;

  if(!ctx)
    return &no_ctx;
  if(ctx->ngtcp2_ctx)
    return &ctx->ngtcp2_ctx->call_data;
  return &ctx->call_data;
}

#undef CF_CTX_CALL_DATA
#define CF_CTX_CALL_DATA(cf) (*cf_h3_proxy_call_data(cf))

static void cf_h3_proxy_ctx_clear(struct cf_h3_proxy_ctx *ctx)
{
  Curl_bufq_free(&ctx->inbufq);
  Curl_peer_unlink(&ctx->dest);
  h3_tunnel_stream_clear(&ctx->tunnel);
  memset(ctx, 0, sizeof(*ctx));
}

static void cf_h3_proxy_ctx_free(struct cf_h3_proxy_ctx *ctx)
{
  if(ctx) {
    cf_h3_proxy_ctx_clear(ctx);
    curlx_free(ctx);
  }
}

static CURLcode h3_proxy_data_setup(struct Curl_cfilter *cf,
                                    struct Curl_easy *data)
{
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
  struct h3_proxy_stream_ctx *stream = NULL;

  if(!data)
    return CURLE_FAILED_INIT;

  if(!ctx)
    return CURLE_FAILED_INIT;

  stream = H3_PROXY_STREAM_CTX(ctx, data);
  if(stream)
    return CURLE_OK;

  stream = curlx_calloc(1, sizeof(*stream));
  if(!stream)
    return CURLE_OUT_OF_MEMORY;

  stream->id = -1;
  stream->rx_offset = 0;
  stream->rx_offset_max = PROXY_H3_STREAM_WINDOW_SIZE;
  /* on send, we control how much we put into the buffer */
  Curl_bufq_initp(&stream->sendbuf, &ctx->stream_bufcp,
                  PROXY_H3_STREAM_SEND_CHUNKS, BUFQ_OPT_NONE);
  stream->sendbuf_len_in_flight = 0;
  stream->window_size_max = PROXY_H3_STREAM_WINDOW_SIZE;

  if(!Curl_uint32_hash_set(&ctx->streams, data->mid, stream)) {
    h3_proxy_stream_ctx_free(stream);
    return CURLE_OUT_OF_MEMORY;
  }

  if(Curl_uint32_hash_count(&ctx->streams) == 1)
    cf_ngtcp2_proxy_setup_keep_alive(cf, data);

  return CURLE_OK;
}

static int cb_h3_proxy_acked_req_body(nghttp3_conn *conn, int64_t stream_id,
                                      uint64_t datalen, void *user_data,
                                      void *stream_user_data)
{
  struct Curl_cfilter *cf = user_data;
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
  struct Curl_easy *data = stream_user_data;
  struct h3_proxy_stream_ctx *stream;
  size_t skiplen;

  if(!ctx)
    return 0;
  stream = H3_PROXY_STREAM_CTX(ctx, data);
  if(!stream)
    return 0;
  /* The server acknowledged `datalen` of bytes from our request body.
   * This is a delta. We have kept this data in `sendbuf` for
   * re-transmissions and can free it now. */
  if(datalen >= (uint64_t)stream->sendbuf_len_in_flight)
    skiplen = stream->sendbuf_len_in_flight;
  else
    skiplen = (size_t)datalen;
  Curl_bufq_skip(&stream->sendbuf, skiplen);
  stream->sendbuf_len_in_flight -= skiplen;

  /* Resume upload processing if we have more data to send */
  if(stream->sendbuf_len_in_flight < Curl_bufq_len(&stream->sendbuf)) {
    int rv = nghttp3_conn_resume_stream(conn, stream_id);
    if(rv && rv != NGHTTP3_ERR_STREAM_NOT_FOUND) {
      return NGHTTP3_ERR_CALLBACK_FAILURE;
    }
  }
  return 0;
}

static int cb_h3_proxy_stream_close(nghttp3_conn *conn, int64_t stream_id,
                                    uint64_t app_error_code, void *user_data,
                                    void *stream_user_data)
{
  struct Curl_cfilter *cf = user_data;
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
  struct Curl_easy *data = stream_user_data;
  struct h3_proxy_stream_ctx *stream;
  bool tunnel_stream = FALSE;
  (void)conn;

  if(!ctx)
    return 0;
  stream = H3_PROXY_STREAM_CTX(ctx, data);
  tunnel_stream = (stream_id == proxy_ctx->tunnel.stream_id);
  /* we might be called by nghttp3 after we already cleaned up */
  if(!stream) {
    if(tunnel_stream) {
      proxy_ctx->tunnel.stream = NULL;
      proxy_ctx->tunnel.closed = TRUE;
    }
    return 0;
  }

  stream->closed = TRUE;
  stream->error3 = app_error_code;
  if(stream->error3 != NGHTTP3_H3_NO_ERROR) {
    stream->reset = TRUE;
    stream->send_closed = TRUE;
    CURL_TRC_CF(data, cf, "[%" PRId64 "] RESET: error %" PRIu64,
                H3_STREAM_ID(stream), stream->error3);
  }
  else {
    CURL_TRC_CF(data, cf, "[%" PRId64 "] CLOSED", H3_STREAM_ID(stream));
  }
  if(tunnel_stream) {
    proxy_ctx->tunnel.stream = NULL;
    proxy_ctx->tunnel.closed = TRUE;
  }
  Curl_multi_mark_dirty(data);
  return 0;
}

static void cf_h3_proxy_upd_rx_win(struct Curl_cfilter *cf,
                                   struct Curl_easy *data,
                                   struct h3_proxy_stream_ctx *stream)
{
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
  uint64_t cur_win, wanted_win = PROXY_H3_STREAM_WINDOW_SIZE_MAX;

  /* how much does rate limiting allow us to acknowledge? */
  if(Curl_rlimit_active(&data->progress.dl.rlimit)) {
    int64_t avail;

    /* start rate limit updates only after first bytes arrived */
    if(!stream->rx_offset)
      return;

    avail = Curl_rlimit_avail(&data->progress.dl.rlimit, Curl_pgrs_now(data));
    if(avail <= 0) {
      /* nothing available, do not extend the rx offset */
      CURL_TRC_CF(data, cf, "[%" PRId64 "] dl rate limit exhausted (%" PRId64
                  " tokens)", stream->id, avail);
      return;
    }
    wanted_win = CURLMIN((uint64_t)avail, PROXY_H3_STREAM_WINDOW_SIZE_MAX);
  }

  if(stream->rx_offset_max < stream->rx_offset) {
    DEBUGASSERT(0);
    return;
  }
  cur_win = stream->rx_offset_max - stream->rx_offset;
  if(cur_win < wanted_win) {
    /* We have exhausted the credit we gave the QUIC peer for DATA.
     * We extend it with the amount we can give (rate limit) */
    uint64_t ext = wanted_win - cur_win;

    ngtcp2_conn_extend_max_stream_offset(ctx->qconn, stream->id, ext);
    ngtcp2_conn_extend_max_offset(ctx->qconn, ext);
    stream->rx_offset_max += ext;
    if(stream->rx_offset_max > stream->window_size_max) {
      stream->window_size_max = stream->rx_offset_max;
      CURL_TRC_CF(data, cf, "[%" PRId64 "] max window now -> %" PRIu64,
                  stream->id, stream->window_size_max);
    }
    CURL_TRC_CF(data, cf, "[%" PRId64 "] rx_offset_max -> %" PRIu64
                " (ext %" PRIu64 ", win %" PRIu64 ")",
                stream->id, stream->rx_offset_max, ext, wanted_win);
  }
}

static int cb_h3_proxy_recv_data(nghttp3_conn *conn, int64_t stream3_id,
                                 const uint8_t *buf, size_t buflen,
                                 void *user_data, void *stream_user_data)
{
  struct Curl_cfilter *cf = user_data;
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
  struct Curl_easy *data = stream_user_data;
  struct h3_proxy_stream_ctx *stream;
  size_t nwritten;
  CURLcode result = CURLE_OK;
  (void)conn;
  (void)stream3_id;

  if(!ctx)
    return NGHTTP3_ERR_CALLBACK_FAILURE;
  stream = H3_PROXY_STREAM_CTX(ctx, data);
  if(!stream) {
    return NGHTTP3_ERR_CALLBACK_FAILURE;
  }

  stream->tun_data_recvd += (curl_off_t)buflen;
  CURL_TRC_CF(data, cf, "[cb_h3_proxy_recv_data] "
              "[%" PRId64 "] DATA len=%zu, total=%" FMT_OFF_T,
              H3_STREAM_ID(stream), buflen, stream->tun_data_recvd);

  result = Curl_bufq_write(&proxy_ctx->inbufq, buf, buflen, &nwritten);
  if(result || (nwritten < buflen)) {
    return NGHTTP3_ERR_CALLBACK_FAILURE;
  }

  /* DATA has been moved into our local recv buffer. Update stream offsets
   * and give QUIC read credit back so long transfers over proxy tunnels
   * do not stall on stream/connection flow-control limits. */
  stream->rx_offset += buflen;
  if(stream->rx_offset_max < stream->rx_offset)
    stream->rx_offset_max = stream->rx_offset;

  CURL_TRC_CF(data, cf, "[%" PRId64 "] DATA len=%zu, rx win=%" PRIu64,
              stream->id, buflen, stream->rx_offset_max - stream->rx_offset);
  cf_h3_proxy_upd_rx_win(cf, data, stream);

  Curl_multi_mark_dirty(data);
  return 0;
}

static int cb_h3_proxy_deferred_consume(nghttp3_conn *conn, int64_t stream_id,
                                        size_t consumed, void *user_data,
                                        void *stream_user_data)
{
  struct Curl_cfilter *cf = user_data;
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
  (void)conn;
  (void)stream_user_data;

  if(!ctx)
    return 0;

  /* nghttp3 has consumed bytes on the QUIC stream and we need to
   * tell the QUIC connection to increase its flow control */
  ngtcp2_conn_extend_max_stream_offset(ctx->qconn, stream_id, consumed);
  ngtcp2_conn_extend_max_offset(ctx->qconn, consumed);

  return 0;
}

static int cb_h3_proxy_recv_header(nghttp3_conn *conn, int64_t stream_id,
                                   int32_t token, nghttp3_rcbuf *name,
                                   nghttp3_rcbuf *value, uint8_t flags,
                                   void *user_data, void *stream_user_data)
{
  struct Curl_cfilter *cf = user_data;
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
  nghttp3_vec h3name = nghttp3_rcbuf_get_buf(name);
  nghttp3_vec h3val = nghttp3_rcbuf_get_buf(value);
  struct Curl_easy *data = stream_user_data;
  struct h3_proxy_stream_ctx *stream;
  CURLcode result = CURLE_OK;
  int http_status;
  struct http_resp *resp;
  (void)conn;
  (void)stream_id;
  (void)token;
  (void)flags;

  /* stream_user_data might be NULL for control streams */
  if(!data) {
    /* Silently ignore headers on streams without user data (control, etc) */
    return 0;
  }

  if(!ctx)
    return 0;
  stream = H3_PROXY_STREAM_CTX(ctx, data);
  if(!stream) {
    CURL_TRC_CF(data, cf, "[%" PRId64 "] recv_header: stream lookup "
                "failed for data=%p mid=%u",
                stream_id, (void *)data, data ? data->mid : 0);
  }

  /* we might have cleaned up this transfer already */
  if(!stream)
    return 0;

  if(proxy_ctx->tunnel.has_final_response) {
    /* we do not do anything with trailers for tunnel streams */
    return 0;
  }

  if(token == NGHTTP3_QPACK_TOKEN__STATUS) {
    result = Curl_http_decode_status(&stream->status_code,
                                     (const char *)h3val.base, h3val.len);
    if(result)
      return NGHTTP3_ERR_CALLBACK_FAILURE;
    http_status = stream->status_code;
    result = Curl_http_resp_make(&resp, http_status, NULL);
    if(result)
      return NGHTTP3_ERR_CALLBACK_FAILURE;
    if(proxy_ctx->tunnel.resp)
      Curl_http_resp_free(proxy_ctx->tunnel.resp);
    proxy_ctx->tunnel.resp = resp;
  }
  else {
    /* store as an HTTP1-style header */
    CURL_TRC_CF(data, cf, "[%" PRId64 "] header: %.*s: %.*s", stream_id,
                (int)h3name.len, h3name.base, (int)h3val.len, h3val.base);
    result = Curl_dynhds_add(&proxy_ctx->tunnel.resp->headers,
                             (const char *)h3name.base, h3name.len,
                             (const char *)h3val.base, h3val.len);
    if(result) {
      return NGHTTP3_ERR_CALLBACK_FAILURE;
    }
  }
  return 0;
}

static int cb_h3_proxy_end_headers(nghttp3_conn *conn, int64_t stream_id,
                                   int fin, void *user_data,
                                   void *stream_user_data)
{
  struct Curl_cfilter *cf = user_data;
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
  struct Curl_easy *data = stream_user_data;
  struct h3_proxy_stream_ctx *stream;
  (void)conn;
  (void)stream_id;
  (void)fin;

  /* stream_user_data might be NULL for control streams */
  if(!data) {
    /* Silently ignore for streams without user data */
    return 0;
  }

  if(!ctx)
    return 0;
  stream = H3_PROXY_STREAM_CTX(ctx, data);
  if(!stream) {
    CURL_TRC_CF(data, cf, "[%" PRId64 "] end_headers: stream lookup "
                "failed for data=%p mid=%u",
                stream_id, (void *)data, data ? data->mid : 0);
  }

  if(!stream)
    return 0;

  CURL_TRC_CF(data, cf, "[%" PRId64 "] end_headers, status=%d", stream_id,
              stream->status_code);

  if(!proxy_ctx->tunnel.has_final_response) {
    if(stream->status_code / 100 != 1) {
      proxy_ctx->tunnel.has_final_response = TRUE;
    }
  }

  if(stream->status_code / 100 != 1) {
    stream->resp_hds_complete = TRUE;
  }

  Curl_multi_mark_dirty(data);
  return 0;
}

static int cb_h3_proxy_stop_sending(nghttp3_conn *conn, int64_t stream_id,
                                    uint64_t app_error_code, void *user_data,
                                    void *stream_user_data)
{
  struct Curl_cfilter *cf = user_data;
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
  (void)conn;

  (void)stream_user_data;

  if(ctx) {
    int rv = ngtcp2_conn_shutdown_stream_read(ctx->qconn, 0, stream_id,
                                              app_error_code);

    if(rv && rv != NGTCP2_ERR_STREAM_NOT_FOUND) {
      return NGHTTP3_ERR_CALLBACK_FAILURE;
    }
  }

  return 0;
}

static int cb_h3_proxy_reset_stream(nghttp3_conn *conn, int64_t stream_id,
                                    uint64_t app_error_code, void *user_data,
                                    void *stream_user_data)
{
  struct Curl_cfilter *cf = user_data;
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
  struct Curl_easy *data = stream_user_data;
  int rv;
  (void)conn;

  if(!ctx)
    return 0;

  rv = ngtcp2_conn_shutdown_stream_write(ctx->qconn, 0, stream_id,
                                         app_error_code);
  CURL_TRC_CF(data, cf, "[%" PRId64 "] reset -> %d", stream_id, rv);
  if(stream_id == proxy_ctx->tunnel.stream_id) {
    proxy_ctx->tunnel.stream = NULL;
    proxy_ctx->tunnel.closed = TRUE;
  }
  if(rv && rv != NGTCP2_ERR_STREAM_NOT_FOUND) {
    return NGHTTP3_ERR_CALLBACK_FAILURE;
  }

  return 0;
}

static nghttp3_ssize cb_h3_read_data_for_tunnel_stream(nghttp3_conn *conn,
                                                       int64_t stream_id,
                                                       nghttp3_vec *vec,
                                                       size_t veccnt,
                                                       uint32_t *pflags,
                                                       void *user_data,
                                                       void *stream_user_data)
{
  struct Curl_cfilter *cf = user_data;
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
  struct Curl_easy *data = stream_user_data;
  struct h3_proxy_stream_ctx *stream;
  size_t nwritten = 0;
  size_t nvecs = 0;
  const unsigned char *buf_base;
  (void)conn;
  (void)stream_id;
  (void)veccnt;

  if(!ctx)
    return NGHTTP3_ERR_CALLBACK_FAILURE;
  stream = H3_PROXY_STREAM_CTX(ctx, data);

  if(!stream)
    return NGHTTP3_ERR_CALLBACK_FAILURE;
  /* nghttp3 keeps references to the sendbuf data until it is ACKed
   * by the server (see `cb_h3_proxy_acked_req_body()` for updates).
   * `sendbuf_len_in_flight` is the amount of bytes in `sendbuf`
   * that we have already passed to nghttp3, but which have not been
   * ACKed yet.
   * Any amount beyond `sendbuf_len_in_flight` we need still to pass
   * to nghttp3. Do that now, if we can. */
  if(stream->sendbuf_len_in_flight < Curl_bufq_len(&stream->sendbuf)) {
    nvecs = 0;
    while(nvecs < veccnt) {
      if(!Curl_bufq_peek_at(&stream->sendbuf,
                           stream->sendbuf_len_in_flight,
                           &buf_base,
                           &vec[nvecs].len))
        break;
      vec[nvecs].base = (uint8_t *)(uintptr_t)buf_base;
      stream->sendbuf_len_in_flight += vec[nvecs].len;
      nwritten += vec[nvecs].len;
      ++nvecs;
    }
    DEBUGASSERT(nvecs > 0); /* we SHOULD have been be able to peek */
  }

  if(nwritten > 0 &&
     stream->upload_left != -1 &&
     (H3_STREAM_ID(stream) != proxy_ctx->tunnel.stream_id))
    stream->upload_left -= nwritten;

  /* When we stopped sending and everything in `sendbuf` is "in flight",
   * we are at the end of the request body. */
  /* We should NOT set send_closed = TRUE for tunnel stream */
  if(stream->upload_left == 0 &&
     (H3_STREAM_ID(stream) != proxy_ctx->tunnel.stream_id)) {
    *pflags = NGHTTP3_DATA_FLAG_EOF;
    stream->send_closed = TRUE;
  }

  else if(!nwritten) {
    /* Not EOF, and nothing to give, we signal WOULDBLOCK. */
    CURL_TRC_CF(data, cf, "[%" PRId64 "] read req body -> AGAIN",
                H3_STREAM_ID(stream));
    return NGHTTP3_ERR_WOULDBLOCK;
  }

  CURL_TRC_CF(data, cf, "[%" PRId64 "] read req body -> "
              "%zu vecs%s with %zu (buffered=%zu, left=%" FMT_OFF_T ")",
              H3_STREAM_ID(stream), nvecs,
              *pflags == NGHTTP3_DATA_FLAG_EOF ? " EOF" : "",
              nwritten, Curl_bufq_len(&stream->sendbuf),
              stream->upload_left);
  return (nghttp3_ssize)nvecs;
}

static nghttp3_callbacks ngh3_proxy_callbacks = {
  cb_h3_proxy_acked_req_body, /* acked_stream_data */
  cb_h3_proxy_stream_close,
  cb_h3_proxy_recv_data,
  cb_h3_proxy_deferred_consume,
  NULL, /* begin_headers */
  cb_h3_proxy_recv_header,
  cb_h3_proxy_end_headers,
  NULL, /* begin_trailers */
  cb_h3_proxy_recv_header,
  NULL, /* end_trailers */
  cb_h3_proxy_stop_sending,
  NULL, /* end_stream */
  cb_h3_proxy_reset_stream,
  NULL, /* shutdown */
  NULL, /* recv_settings (deprecated) */
#ifdef NGHTTP3_CALLBACKS_V2  /* nghttp3 v1.11.0+ */
  NULL, /* recv_origin */
  NULL, /* end_origin */
  NULL, /* rand */
#endif
#ifdef NGHTTP3_CALLBACKS_V3  /* nghttp3 v1.14.0+ */
  NULL, /* recv_settings2 */
#endif
};

#if NGTCP2_VERSION_NUM < 0x011100
struct cf_ngtcp2_proxy_sfind_ctx {
  int64_t stream_id;
  struct h3_proxy_stream_ctx *stream;
  uint32_t mid;
};

static bool cf_ngtcp2_proxy_sfind(uint32_t mid, void *value, void *user_data)
{
  struct cf_ngtcp2_proxy_sfind_ctx *fctx = user_data;
  struct h3_proxy_stream_ctx *stream = value;

  if(fctx->stream_id == H3_STREAM_ID(stream)) {
    fctx->mid = mid;
    fctx->stream = stream;
    return FALSE;
  }
  return TRUE; /* continue */
}

static struct h3_proxy_stream_ctx *cf_ngtcp2_proxy_get_stream(
  struct cf_ngtcp2_proxy_ctx *ctx, int64_t stream_id)
{
  struct cf_ngtcp2_proxy_sfind_ctx fctx;
  fctx.stream_id = stream_id;
  fctx.stream = NULL;
  Curl_uint32_hash_visit(&ctx->streams, cf_ngtcp2_proxy_sfind, &fctx);
  return fctx.stream;
}
#else
static struct h3_proxy_stream_ctx *cf_ngtcp2_proxy_get_stream(
  struct cf_ngtcp2_proxy_ctx *ctx, int64_t stream_id)
{
  struct Curl_easy *data =
    ngtcp2_conn_get_stream_user_data(ctx->qconn, stream_id);

  if(!data) {
    return NULL;
  }
  return H3_PROXY_STREAM_CTX(ctx, data);
}
#endif /* NGTCP2_VERSION_NUM < 0x011100 */

static CURLcode cf_ngtcp2_h3conn_init(struct Curl_cfilter *cf,
                                      struct Curl_easy *data)
{
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
  int64_t ctrl_stream_id, qpack_enc_stream_id, qpack_dec_stream_id;
  int rc;

  if(ngtcp2_conn_get_streams_uni_left(ctx->qconn) < 3) {
    failf(data, "QUIC connection lacks 3 uni streams to run HTTP/3");
    return CURLE_QUIC_CONNECT_ERROR;
  }

  nghttp3_settings_default(&ctx->h3settings);

  rc = nghttp3_conn_client_new(&ctx->h3conn,
                               &ngh3_proxy_callbacks,
                               &ctx->h3settings,
                               Curl_nghttp3_mem(),
                               cf);
  if(rc) {
    failf(data, "error creating nghttp3 connection instance");
    return CURLE_OUT_OF_MEMORY;
  }

  rc = ngtcp2_conn_open_uni_stream(ctx->qconn, &ctrl_stream_id, NULL);
  if(rc) {
    failf(data, "error creating HTTP/3 control stream: %s",
          ngtcp2_strerror(rc));
    return CURLE_QUIC_CONNECT_ERROR;
  }

  rc = nghttp3_conn_bind_control_stream(ctx->h3conn, ctrl_stream_id);
  if(rc) {
    failf(data, "error binding HTTP/3 control stream: %s",
          ngtcp2_strerror(rc));
    return CURLE_QUIC_CONNECT_ERROR;
  }

  rc = ngtcp2_conn_open_uni_stream(ctx->qconn, &qpack_enc_stream_id, NULL);
  if(rc) {
    failf(data, "error creating HTTP/3 qpack encoding stream: %s",
          ngtcp2_strerror(rc));
    return CURLE_QUIC_CONNECT_ERROR;
  }

  rc = ngtcp2_conn_open_uni_stream(ctx->qconn, &qpack_dec_stream_id, NULL);
  if(rc) {
    failf(data, "error creating HTTP/3 qpack decoding stream: %s",
          ngtcp2_strerror(rc));
    return CURLE_QUIC_CONNECT_ERROR;
  }

  rc = nghttp3_conn_bind_qpack_streams(ctx->h3conn, qpack_enc_stream_id,
                                       qpack_dec_stream_id);
  if(rc) {
    failf(data, "error binding HTTP/3 qpack streams: %s", ngtcp2_strerror(rc));
    return CURLE_QUIC_CONNECT_ERROR;
  }

  CURL_TRC_CF(data, cf, "HTTP/3 connection initialized");
  return CURLE_OK;
}

static int cb_ngtcp2_proxy_handshake_completed(ngtcp2_conn *tconn,
                                               void *user_data)
{
  struct Curl_cfilter *cf = user_data;
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
  struct Curl_easy *data;

  (void)tconn;
  DEBUGASSERT(ctx);
  data = CF_DATA_CURRENT(cf);
  DEBUGASSERT(data);
  if(!ctx || !data)
    return NGTCP2_ERR_CALLBACK_FAILURE;

  ctx->handshake_at = *Curl_pgrs_now(data);
  ctx->tls_handshake_complete = TRUE;
  Curl_vquic_report_handshake(&ctx->tls, cf, data);

  ctx->tls_vrfy_result = Curl_vquic_tls_verify_peer(&ctx->tls, cf,
                                                    data, &ctx->peer);
#ifdef CURLVERBOSE
  if(Curl_trc_is_verbose(data)) {
    const ngtcp2_transport_params *rp;
    rp = ngtcp2_conn_get_remote_transport_params(ctx->qconn);
    CURL_TRC_CF(data, cf, "handshake complete after %" FMT_TIMEDIFF_T
                "ms, remote transport[max_udp_payload=%" PRIu64
                ", initial_max_data=%" PRIu64 "]",
               curlx_ptimediff_ms(&ctx->handshake_at, &ctx->started_at),
               rp->max_udp_payload_size, rp->initial_max_data);
  }
#endif

  /* In case of earlydata, where we simulate being connected, update
   * the handshake time when we really did connect */
  if(ctx->use_earlydata)
    Curl_pgrsTimeWas(data, TIMER_APPCONNECT, ctx->handshake_at);
  if(ctx->use_earlydata) {
#if defined(USE_OPENSSL) && defined(HAVE_OPENSSL_EARLYDATA)
    ctx->earlydata_accepted =
      (SSL_get_early_data_status(ctx->tls.ossl.ssl) !=
       SSL_EARLY_DATA_REJECTED);
#endif
#ifdef USE_GNUTLS
    int flags = gnutls_session_get_flags(ctx->tls.gtls.session);
    ctx->earlydata_accepted = !!(flags & GNUTLS_SFLAGS_EARLY_DATA);
#endif
#ifdef USE_WOLFSSL
#ifdef WOLFSSL_EARLY_DATA
    ctx->earlydata_accepted =
      (wolfSSL_get_early_data_status(ctx->tls.wssl.ssl) !=
       WOLFSSL_EARLY_DATA_REJECTED);
#else
    DEBUGASSERT(0); /* should not come here if ED is disabled. */
    ctx->earlydata_accepted = FALSE;
#endif /* WOLFSSL_EARLY_DATA */
#endif
    CURL_TRC_CF(data, cf, "server did%s accept %zu bytes of early data",
                ctx->earlydata_accepted ? "" : " not", ctx->earlydata_skip);
    Curl_pgrsEarlyData(data, ctx->earlydata_accepted ?
                              (curl_off_t)ctx->earlydata_skip :
                             -(curl_off_t)ctx->earlydata_skip);
  }

  /* Initialize HTTP/3 connection after successful handshake */
  if(!ctx->h3conn) {
    CURLcode result = cf_ngtcp2_h3conn_init(cf, data);
    if(result) {
      CURL_TRC_CF(data, cf, "HTTP/3 initialization failed: %d", result);
      return NGTCP2_ERR_CALLBACK_FAILURE;
    }
  }

  return 0;
}

static int cb_ngtcp2_recv_stream_data(ngtcp2_conn *tconn, uint32_t flags,
                                      int64_t stream_id, uint64_t offset,
                                      const uint8_t *buf, size_t buflen,
                                      void *user_data, void *stream_user_data)
{
  struct Curl_cfilter *cf = user_data;
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
  nghttp3_ssize nconsumed;
  int fin = (flags & NGTCP2_STREAM_DATA_FLAG_FIN) ? 1 : 0;
  struct Curl_easy *data = stream_user_data;
  (void)offset;
  (void)data;

  nconsumed =
    nghttp3_conn_read_stream(ctx->h3conn, stream_id, buf, buflen, fin);
  if(!data)
    data = CF_DATA_CURRENT(cf);
  if(data)
    CURL_TRC_CF(data, cf, "[%" PRId64 "] read_stream(len=%zu) -> %zd",
                stream_id, buflen, nconsumed);
  if(nconsumed < 0) {
    struct h3_proxy_stream_ctx *stream = H3_PROXY_STREAM_CTX(ctx, data);
    if(data && stream) {
      CURL_TRC_CF(data, cf, "[%" PRId64 "] error on known stream, "
                  "reset=%d, closed=%d",
                  stream_id, stream->reset, stream->closed);
    }
    return NGTCP2_ERR_CALLBACK_FAILURE;
  }

  /* number of bytes inside buflen which consists of framing overhead
   * including QPACK HEADERS. In other words, it does not consume payload of
   * DATA frame. */
  if(nconsumed) {
    ngtcp2_conn_extend_max_stream_offset(tconn, stream_id, nconsumed);
    ngtcp2_conn_extend_max_offset(tconn, nconsumed);
  }

  return 0;
}

static int cb_ngtcp2_acked_stream_data_offset(ngtcp2_conn *tconn,
                                              int64_t stream_id,
                                              uint64_t offset,
                                              uint64_t datalen,
                                              void *user_data,
                                              void *stream_user_data)
{
  struct Curl_cfilter *cf = user_data;
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
  int rv;
  (void)stream_id;
  (void)tconn;
  (void)offset;
  (void)datalen;
  (void)stream_user_data;

  rv = nghttp3_conn_add_ack_offset(ctx->h3conn, stream_id, datalen);
  if(rv && rv != NGHTTP3_ERR_STREAM_NOT_FOUND) {
    return NGTCP2_ERR_CALLBACK_FAILURE;
  }
  return 0;
}

static int cb_ngtcp2_stream_close(ngtcp2_conn *tconn, uint32_t flags,
                                  int64_t stream_id, uint64_t app_error_code,
                                  void *user_data, void *stream_user_data)
{
  struct Curl_cfilter *cf = user_data;
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
  struct Curl_easy *data = stream_user_data;
  int rv;

  (void)tconn;
  /* stream is closed... */
  if(!data)
    data = CF_DATA_CURRENT(cf);
  if(!data)
    return NGTCP2_ERR_CALLBACK_FAILURE;

  if(!(flags & NGTCP2_STREAM_CLOSE_FLAG_APP_ERROR_CODE_SET)) {
    app_error_code = NGHTTP3_H3_NO_ERROR;
  }

  rv = nghttp3_conn_close_stream(ctx->h3conn, stream_id, app_error_code);
  CURL_TRC_CF(data, cf, "[%" PRId64 "] quic close(app_error=%"
              PRIu64 ") -> %d", stream_id, app_error_code, rv);
  if(rv && rv != NGHTTP3_ERR_STREAM_NOT_FOUND) {
    cf_ngtcp2_proxy_h3_err_set(cf, data, rv);
    return NGTCP2_ERR_CALLBACK_FAILURE;
  }
  return 0;
}

static int cb_ngtcp2_extend_max_local_streams_bidi(ngtcp2_conn *tconn,
                                                   uint64_t max_streams,
                                                   void *user_data)
{
  struct Curl_cfilter *cf = user_data;
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
  struct Curl_easy *data = CF_DATA_CURRENT(cf);

  (void)tconn;
  ctx->max_bidi_streams = max_streams;
  if(data)
    CURL_TRC_CF(data, cf, "max bidi streams now %" PRIu64 ", used %" PRIu64,
                ctx->max_bidi_streams, ctx->used_bidi_streams);
  return 0;
}

static void cb_ngtcp2_rand(uint8_t *dest, size_t destlen,
                           const ngtcp2_rand_ctx *rand_ctx)
{
  CURLcode result;
  (void)rand_ctx;

  result = Curl_rand(NULL, dest, destlen);
  if(result) {
    /* cb_rand is only used for non-cryptographic context. If Curl_rand
       failed, fill 0 and call it *random*. */
    memset(dest, 0, destlen);
  }
}

/* for ngtcp2 <v1.22.0 */
static int cb_ngtcp2_get_new_connection_id(ngtcp2_conn *tconn, ngtcp2_cid *cid,
                                           uint8_t *token, size_t cidlen,
                                           void *user_data)
{
  CURLcode result;
  (void)tconn;
  (void)user_data;

  result = Curl_rand(NULL, cid->data, cidlen);
  if(result)
    return NGTCP2_ERR_CALLBACK_FAILURE;
  cid->datalen = cidlen;

  result = Curl_rand(NULL, token, NGTCP2_STATELESS_RESET_TOKENLEN);
  if(result)
    return NGTCP2_ERR_CALLBACK_FAILURE;

  return 0;
}

#ifdef NGTCP2_CALLBACKS_V3  /* ngtcp2 v1.22.0+ */
static int cb_ngtcp2_get_new_connection_id2(ngtcp2_conn *tconn,
  ngtcp2_cid *cid, struct ngtcp2_stateless_reset_token *token,
  size_t cidlen, void *user_data)
{
  CURLcode result;
  (void)tconn;
  (void)user_data;

  result = Curl_rand(NULL, cid->data, cidlen);
  if(result)
    return NGTCP2_ERR_CALLBACK_FAILURE;
  cid->datalen = cidlen;

  result = Curl_rand(NULL, token->data, sizeof(token->data));
  if(result)
    return NGTCP2_ERR_CALLBACK_FAILURE;

  return 0;
}
#endif

static int cb_ngtcp2_stream_reset(ngtcp2_conn *tconn, int64_t stream_id,
                                  uint64_t final_size, uint64_t app_error_code,
                                  void *user_data, void *stream_user_data)
{
  struct Curl_cfilter *cf = user_data;
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
  struct Curl_easy *data = stream_user_data;
  int rv;
  (void)tconn;
  (void)final_size;
  (void)app_error_code;
  (void)data;

  rv = nghttp3_conn_shutdown_stream_read(ctx->h3conn, stream_id);
  CURL_TRC_CF(data, cf, "[%" PRId64 "] reset -> %d", stream_id, rv);
  if(stream_id == proxy_ctx->tunnel.stream_id) {
    proxy_ctx->tunnel.stream = NULL;
    proxy_ctx->tunnel.closed = TRUE;
  }
  if(rv && rv != NGHTTP3_ERR_STREAM_NOT_FOUND) {
    return NGTCP2_ERR_CALLBACK_FAILURE;
  }
  return 0;
}

static int cb_ngtcp2_extend_max_stream_data(ngtcp2_conn *tconn,
                                            int64_t stream_id,
                                            uint64_t max_data, void *user_data,
                                            void *stream_user_data)
{
  struct Curl_cfilter *cf = user_data;
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
  struct Curl_easy *s_data = stream_user_data;
  struct h3_proxy_stream_ctx *stream = NULL;
  int rv;
  (void)tconn;
  (void)max_data;

  rv = nghttp3_conn_unblock_stream(ctx->h3conn, stream_id);
  if(rv && rv != NGHTTP3_ERR_STREAM_NOT_FOUND) {
    return NGTCP2_ERR_CALLBACK_FAILURE;
  }
  stream = H3_PROXY_STREAM_CTX(ctx, s_data);
  if(stream && stream->quic_flow_blocked) {
    CURL_TRC_CF(s_data, cf, "[%" PRId64 "] unblock quic flow", stream_id);
    stream->quic_flow_blocked = FALSE;
    Curl_multi_mark_dirty(s_data);
  }
  return 0;
}

static int cb_ngtcp2_stream_stop_sending(ngtcp2_conn *tconn, int64_t stream_id,
                                         uint64_t app_error_code,
                                         void *user_data,
                                         void *stream_user_data)
{
  struct Curl_cfilter *cf = user_data;
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
  int rv;
  (void)tconn;
  (void)app_error_code;
  (void)stream_user_data;

  rv = nghttp3_conn_shutdown_stream_read(ctx->h3conn, stream_id);
  if(rv && rv != NGHTTP3_ERR_STREAM_NOT_FOUND) {
    return NGTCP2_ERR_CALLBACK_FAILURE;
  }
  return 0;
}

static int cb_ngtcp2_recv_rx_key(ngtcp2_conn *tconn,
                                 ngtcp2_encryption_level level,
                                 void *user_data)
{
  struct Curl_cfilter *cf = user_data;
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
  struct Curl_easy *data = CF_DATA_CURRENT(cf);
  (void)tconn;

  if(level != NGTCP2_ENCRYPTION_LEVEL_1RTT)
    return 0;

  DEBUGASSERT(ctx);
  DEBUGASSERT(data);
  if(ctx && data && !ctx->h3conn) {
    if(cf_ngtcp2_h3conn_init(cf, data))
      return NGTCP2_ERR_CALLBACK_FAILURE;
  }
  return 0;
}

#if defined(_MSC_VER) && defined(_DLL)
#pragma warning(push)
#pragma warning(disable:4232) /* MSVC extension, dllimport identity */
#endif

static ngtcp2_callbacks ngtcp2_proxy_callbacks = {
  ngtcp2_crypto_client_initial_cb,
  NULL, /* recv_client_initial */
  ngtcp2_crypto_recv_crypto_data_cb,
  cb_ngtcp2_proxy_handshake_completed,
  NULL, /* recv_version_negotiation */
  ngtcp2_crypto_encrypt_cb,
  ngtcp2_crypto_decrypt_cb,
  ngtcp2_crypto_hp_mask_cb,
  cb_ngtcp2_recv_stream_data,
  cb_ngtcp2_acked_stream_data_offset,
  NULL, /* stream_open */
  cb_ngtcp2_stream_close,
  NULL, /* recv_stateless_reset */
  ngtcp2_crypto_recv_retry_cb,
  cb_ngtcp2_extend_max_local_streams_bidi,
  NULL, /* extend_max_local_streams_uni */
  cb_ngtcp2_rand,
  cb_ngtcp2_get_new_connection_id, /* for ngtcp2 <v1.22.0 */
  NULL, /* remove_connection_id */
  ngtcp2_crypto_update_key_cb,
  NULL, /* path_validation */
  NULL, /* select_preferred_addr */
  cb_ngtcp2_stream_reset,
  NULL, /* extend_max_remote_streams_bidi */
  NULL, /* extend_max_remote_streams_uni */
  cb_ngtcp2_extend_max_stream_data,
  NULL, /* dcid_status */
  NULL, /* handshake_confirmed */
  NULL, /* recv_new_token */
  ngtcp2_crypto_delete_crypto_aead_ctx_cb,
  ngtcp2_crypto_delete_crypto_cipher_ctx_cb,
  NULL, /* recv_datagram */
  NULL, /* ack_datagram */
  NULL, /* lost_datagram */
  ngtcp2_crypto_get_path_challenge_data_cb,
  cb_ngtcp2_stream_stop_sending,
  NULL, /* version_negotiation */
  cb_ngtcp2_recv_rx_key, /* recv_rx_key */
  NULL, /* recv_tx_key */
  NULL, /* early_data_rejected */
#ifdef NGTCP2_CALLBACKS_V2  /* ngtcp2 v1.14.0+ */
  NULL, /* begin_path_validation */
#endif
#ifdef NGTCP2_CALLBACKS_V3  /* ngtcp2 v1.22.0+ */
  NULL, /* recv_stateless_reset2 */
  cb_ngtcp2_get_new_connection_id2, /* get_new_connection_id2 */
  NULL, /* dcid_status2 */
  ngtcp2_crypto_get_path_challenge_data2_cb, /* get_path_challenge_data2 */
#endif
};

#if defined(_MSC_VER) && defined(_DLL)
#pragma warning(pop)
#endif

static CURLcode cf_ngtcp2_recv_pkts_proxy(const unsigned char *buf,
                                          size_t buflen, size_t gso_size,
                                          struct sockaddr_storage *remote_addr,
                                          socklen_t remote_addrlen, int ecn,
                                          void *userp)
{
  struct proxy_pkt_io_ctx *pktx = userp;
  struct cf_h3_proxy_ctx *proxy_ctx = pktx->cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
  ngtcp2_pkt_info pi;
  ngtcp2_path path;
  size_t offset, pktlen;
  int rv;

  if(ecn)
    CURL_TRC_CF(pktx->data, pktx->cf, "vquic_recv(len=%zu, gso=%zu, ecn=%x)",
                buflen, gso_size, ecn);
  ngtcp2_addr_init(&path.local, (struct sockaddr *)&ctx->q.local_addr,
                   ctx->q.local_addrlen);
  ngtcp2_addr_init(&path.remote, (struct sockaddr *)remote_addr,
                   remote_addrlen);
  pi.ecn = (uint8_t)ecn;

  for(offset = 0; offset < buflen; offset += gso_size) {
    pktlen = ((offset + gso_size) <= buflen) ? gso_size : (buflen - offset);
    rv = ngtcp2_conn_read_pkt(ctx->qconn, &path, &pi,
                              buf + offset, pktlen, pktx->ts);
    if(rv) {
      CURL_TRC_CF(pktx->data, pktx->cf, "ingress, read_pkt -> %s (%d)",
                  ngtcp2_strerror(rv), rv);
      cf_ngtcp2_proxy_err_set(pktx->cf, pktx->data, rv);

      if(rv == NGTCP2_ERR_CRYPTO)
        /* this is a "TLS problem", but a failed certificate verification
           is a common reason for this */
        return CURLE_PEER_FAILED_VERIFICATION;
      return CURLE_RECV_ERROR;
    }
  }
  return CURLE_OK;
}

static CURLcode proxy_h3_progress_ingress_ngtcp2(struct Curl_cfilter *cf,
                                                 struct Curl_easy *data,
                                                 struct proxy_pkt_io_ctx *pktx)
{
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
  struct proxy_pkt_io_ctx local_pktx;
  CURLcode result = CURLE_OK;

  if(!ctx)
    return CURLE_RECV_ERROR;
  if(!data || !data->multi)
    return CURLE_RECV_ERROR;

  if(!pktx) {
    proxy_pktx_init(&local_pktx, cf, data);
    pktx = &local_pktx;
  }
  else {
    proxy_pktx_update_time(pktx, cf);
    ngtcp2_path_storage_zero(&pktx->ps);
  }

  result = Curl_vquic_tls_before_recv(&ctx->tls, cf, data);
  if(result)
    return result;

  if(ctx->q.sockfd == CURL_SOCKET_BAD)
    return CURLE_RECV_ERROR;

  return vquic_recv_packets(cf, data, &ctx->q, 1000,
                            cf_ngtcp2_recv_pkts_proxy, pktx);
}

/**
 * Read a network packet to send from ngtcp2 into `buf`.
 * Return number of bytes written or -1 with *err set.
 */
static CURLcode proxy_read_pkt_to_send(void *userp,
                                       unsigned char *buf, size_t buflen,
                                       size_t *pnread)
{
  struct proxy_pkt_io_ctx *x = userp;
  struct cf_h3_proxy_ctx *proxy_ctx = x->cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
  nghttp3_vec vec[16];
  nghttp3_ssize veccnt;
  ngtcp2_ssize ndatalen;
  uint32_t flags;
  int64_t stream_id;
  int fin;
  ssize_t n;

  *pnread = 0;
  veccnt = 0;
  stream_id = -1;
  fin = 0;

  /* ngtcp2 may want to put several frames from different streams into
   * this packet. `NGTCP2_WRITE_STREAM_FLAG_MORE` tells it to do so.
   * When `NGTCP2_ERR_WRITE_MORE` is returned, we *need* to make
   * another iteration.
   * When ngtcp2 is happy (because it has no other frame that would fit
   * or it has nothing more to send), it returns the total length
   * of the assembled packet. This may be 0 if there was nothing to send. */
  for(;;) {

    if(ctx->h3conn && ngtcp2_conn_get_max_data_left(ctx->qconn)) {
      veccnt = nghttp3_conn_writev_stream(ctx->h3conn, &stream_id, &fin, vec,
                                          CURL_ARRAYSIZE(vec));
      if(veccnt < 0) {
        failf(x->data, "nghttp3_conn_writev_stream returned error: %s",
              nghttp3_strerror((int)veccnt));
        cf_ngtcp2_proxy_h3_err_set(x->cf, x->data, (int)veccnt);
        return CURLE_SEND_ERROR;
      }
    }

    flags = NGTCP2_WRITE_STREAM_FLAG_MORE |
            (fin ? NGTCP2_WRITE_STREAM_FLAG_FIN : 0);
    n = ngtcp2_conn_writev_stream(ctx->qconn, &x->ps.path,
                                  NULL, buf, buflen,
                                  &ndatalen, flags, stream_id,
                                  (const ngtcp2_vec *)vec, veccnt, x->ts);
    if(n == 0) {
      /* nothing to send */
      return CURLE_AGAIN;
    }
    else if(n < 0) {
      switch(n) {
      case NGTCP2_ERR_STREAM_DATA_BLOCKED: {
        struct h3_proxy_stream_ctx *stream;
        DEBUGASSERT(ndatalen == -1);
        nghttp3_conn_block_stream(ctx->h3conn, stream_id);
        CURL_TRC_CF(x->data, x->cf, "[%" PRId64 "] block quic flow",
                    stream_id);
        stream = cf_ngtcp2_proxy_get_stream(ctx, stream_id);
        if(stream) /* it might be not one of our h3 streams? */
          stream->quic_flow_blocked = TRUE;
        n = 0;
        break;
      }
      case NGTCP2_ERR_STREAM_SHUT_WR:
        DEBUGASSERT(ndatalen == -1);
        nghttp3_conn_shutdown_stream_write(ctx->h3conn, stream_id);
        n = 0;
        break;
      case NGTCP2_ERR_WRITE_MORE:
        /* ngtcp2 wants to send more. update the flow of the stream whose data
         * is in the buffer and continue */
        DEBUGASSERT(ndatalen >= 0);
        n = 0;
        break;
      default:
        DEBUGASSERT(ndatalen == -1);
        failf(x->data, "ngtcp2_conn_writev_stream returned error: %s",
              ngtcp2_strerror((int)n));
        cf_ngtcp2_proxy_err_set(x->cf, x->data, (int)n);
        return CURLE_SEND_ERROR;
      }
    }

    if(ndatalen >= 0) {
      /* we add the amount of data bytes to the flow windows */
      int rv = nghttp3_conn_add_write_offset(ctx->h3conn, stream_id, ndatalen);
      if(rv) {
        failf(x->data, "nghttp3_conn_add_write_offset returned error: %s",
              nghttp3_strerror(rv));
        return CURLE_SEND_ERROR;
      }
    }

    if(n > 0) {
      /* packet assembled, leave */
      *pnread = (size_t)n;
      return CURLE_OK;
    }
  }
}

static CURLcode proxy_h3_progress_egress_ngtcp2(struct Curl_cfilter *cf,
                                                struct Curl_easy *data,
                                                struct proxy_pkt_io_ctx *pktx)
{
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
  size_t nread;
  size_t max_payload_size, path_max_payload_size;
  size_t pktcnt = 0;
  size_t gsolen = 0;  /* this disables gso until we have a clue */
  size_t send_quantum;
  CURLcode result;
  struct proxy_pkt_io_ctx local_pktx;

  if(!pktx) {
    proxy_pktx_init(&local_pktx, cf, data);
    pktx = &local_pktx;
  }
  else {
    proxy_pktx_update_time(pktx, cf);
    ngtcp2_path_storage_zero(&pktx->ps);
  }

  result = vquic_flush(cf, data, &ctx->q);
  if(result) {
    if(result == CURLE_AGAIN) {
      Curl_expire(data, 1, EXPIRE_QUIC);
      return CURLE_OK;
    }
    return result;
  }

  /* In UDP, there is a maximum theoretical packet payload length and
   * a minimum payload length that is "guaranteed" to work.
   * To detect if this minimum payload can be increased, ngtcp2 sends
   * now and then a packet payload larger than the minimum. It that
   * is ACKed by the peer, both parties know that it works and
   * the subsequent packets can use a larger one.
   * This is called PMTUD (Path Maximum Transmission Unit Discovery).
   * Since a PMTUD might be rejected right on send, we do not want it
   * be followed by other packets of lesser size. Because those would
   * also fail then. If we detect a PMTUD while buffering, we flush.
   */
  max_payload_size = ngtcp2_conn_get_max_tx_udp_payload_size(ctx->qconn);
  path_max_payload_size =
    ngtcp2_conn_get_path_max_tx_udp_payload_size(ctx->qconn);
  send_quantum = ngtcp2_conn_get_send_quantum(ctx->qconn);
  CURL_TRC_CF(data, cf, "egress, collect and send packets, quantum=%zu",
              send_quantum);
  for(;;) {
    /* add the next packet to send, if any, to our buffer */
    result = Curl_bufq_sipn(&ctx->q.sendbuf, max_payload_size,
                            proxy_read_pkt_to_send, pktx, &nread);
    if(result == CURLE_AGAIN)
      break;
    else if(result)
      return result;
    else {
      size_t buflen = Curl_bufq_len(&ctx->q.sendbuf);
      if((buflen >= send_quantum) ||
         ((buflen + gsolen) >= ctx->q.sendbuf.chunk_size))
        break;
      DEBUGASSERT(nread > 0);
      ++pktcnt;
      if(pktcnt == 1) {
        /* first packet in buffer. This is either of a known, "good"
         * payload size or it is a PMTUD. We shall see. */
        gsolen = nread;
      }
      else if(nread > gsolen ||
              (gsolen > path_max_payload_size && nread != gsolen)) {
        /* The added packet is a PMTUD *or* the one(s) before the
         * added were PMTUD and the last one is smaller.
         * Flush the buffer before the last add. */
        result = vquic_send_tail_split(cf, data, &ctx->q,
                                       gsolen, nread, nread);
        if(result) {
          if(result == CURLE_AGAIN) {
            Curl_expire(data, 1, EXPIRE_QUIC);
            return CURLE_OK;
          }
          return result;
        }
        pktcnt = 0;
      }
      else if(nread < gsolen) {
        /* Reached capacity of our buffer *or*
         * last add was shorter than the previous ones, flush */
        break;
      }
    }
  }

  if(!Curl_bufq_is_empty(&ctx->q.sendbuf)) {
    /* time to send */
    CURL_TRC_CF(data, cf, "egress, send collected %zu packets in %zu bytes",
                pktcnt, Curl_bufq_len(&ctx->q.sendbuf));
    result = vquic_send(cf, data, &ctx->q, gsolen);
    if(result) {
      if(result == CURLE_AGAIN) {
        Curl_expire(data, 1, EXPIRE_QUIC);
        return CURLE_OK;
      }
      return result;
    }
    proxy_pktx_update_time(pktx, cf);
    ngtcp2_conn_update_pkt_tx_time(ctx->qconn, pktx->ts);
  }
  return CURLE_OK;
}

static CURLcode cf_ngtcp2_proxy_shutdown(struct Curl_cfilter *cf,
                                         struct Curl_easy *data, bool *done)
{
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
  struct cf_call_data save;
  struct proxy_pkt_io_ctx pktx;
  CURLcode result = CURLE_OK;

  if(cf->shutdown || !ctx->qconn) {
    *done = TRUE;
    return CURLE_OK;
  }

  if(!cf->next) {
    Curl_bufq_reset(&ctx->q.sendbuf);
    *done = TRUE;
    return CURLE_OK;
  }

  CF_DATA_SAVE(save, cf, data);
  *done = FALSE;
  proxy_pktx_init(&pktx, cf, data);

  if(!ctx->shutdown_started) {
    char buffer[NGTCP2_MAX_UDP_PAYLOAD_SIZE];
    ngtcp2_ssize nwritten;

    if(!Curl_bufq_is_empty(&ctx->q.sendbuf)) {
      CURL_TRC_CF(data, cf, "shutdown, flushing sendbuf");
      result = proxy_h3_progress_egress_ngtcp2(cf, data, &pktx);
      if(!Curl_bufq_is_empty(&ctx->q.sendbuf)) {
        CURL_TRC_CF(data, cf, "sending shutdown packets blocked");
        result = CURLE_OK;
        goto out;
      }
      else if(result) {
        CURL_TRC_CF(data, cf, "shutdown, error %d flushing sendbuf", result);
        *done = TRUE;
        goto out;
      }
    }

    DEBUGASSERT(Curl_bufq_is_empty(&ctx->q.sendbuf));
    ctx->shutdown_started = TRUE;
    nwritten = ngtcp2_conn_write_connection_close(
      ctx->qconn, NULL, /* path */
      NULL, /* pkt_info */
      (uint8_t *)buffer, sizeof(buffer),
      &ctx->last_error, pktx.ts);
    CURL_TRC_CF(data, cf, "start shutdown(err_type=%d, err_code=%"
                PRIu64 ") -> %zd", ctx->last_error.type,
                ctx->last_error.error_code, (ssize_t)nwritten);
    /* there are cases listed in ngtcp2 documentation where this call
     * may fail. Since we are doing a connection shutdown as graceful
     * as we can, such an error is ignored here. */
    if(nwritten > 0) {
      /* Ignore amount written. sendbuf was empty and has always room for
       * NGTCP2_MAX_UDP_PAYLOAD_SIZE. It can only completely fail, in which
       * case `result` is set non zero. */
      size_t n;
      result = Curl_bufq_write(&ctx->q.sendbuf, (const unsigned char *)buffer,
                               (size_t)nwritten, &n);
      if(result) {
        CURL_TRC_CF(data, cf, "error %d adding shutdown packets to sendbuf, "
                    "aborting shutdown", result);
        goto out;
      }

      ctx->q.no_gso = TRUE;
      ctx->q.gsolen = (size_t)nwritten;
      ctx->q.split_len = 0;
    }
  }

  if(!Curl_bufq_is_empty(&ctx->q.sendbuf)) {
    CURL_TRC_CF(data, cf, "shutdown, flushing egress");
    result = vquic_flush(cf, data, &ctx->q);
    if(result == CURLE_AGAIN) {
      CURL_TRC_CF(data, cf, "sending shutdown packets blocked");
      result = CURLE_OK;
      goto out;
    }
    else if(result) {
      CURL_TRC_CF(data, cf, "shutdown, error %d flushing sendbuf", result);
      *done = TRUE;
      goto out;
    }
  }

  if(Curl_bufq_is_empty(&ctx->q.sendbuf)) {
    /* Sent everything off. ngtcp2 seems to have no support for graceful
     * shutdowns. We are done. */
    CURL_TRC_CF(data, cf, "shutdown completely sent off, done");
    *done = TRUE;
    result = CURLE_OK;
  }
out:
  CF_DATA_RESTORE(cf, save);
  return result;
}

static void cf_ngtcp2_proxy_conn_close(struct Curl_cfilter *cf,
                                       struct Curl_easy *data)
{
  bool done;
  cf_ngtcp2_proxy_shutdown(cf, data, &done);
}

static void cf_ngtcp2_proxy_close(struct Curl_cfilter *cf,
                                  struct Curl_easy *data)
{
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
  struct cf_call_data save;

  CF_DATA_SAVE(save, cf, data);
  if(ctx && ctx->qconn) {
    cf_ngtcp2_proxy_conn_close(cf, data);
    cf_ngtcp2_proxy_ctx_close(ctx);
    CURL_TRC_CF(data, cf, "close");
  }
  cf->connected = FALSE;
  CF_DATA_RESTORE(cf, save);
}

static void cf_ngtcp2_proxy_stream_close(struct Curl_cfilter *cf,
                                         struct Curl_easy *data,
                                         struct h3_proxy_stream_ctx *stream)
{
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
  DEBUGASSERT(data);
  DEBUGASSERT(stream);

  if(stream->id == proxy_ctx->tunnel.stream_id) {
    proxy_ctx->tunnel.stream = NULL;
    proxy_ctx->tunnel.closed = TRUE;
  }

  if(ctx->h3conn)
    nghttp3_conn_set_stream_user_data(ctx->h3conn, stream->id, NULL);
  if(ctx->qconn)
    ngtcp2_conn_set_stream_user_data(ctx->qconn, stream->id, NULL);

  if(!stream->closed && ctx->qconn && ctx->h3conn) {
    CURLcode result;

    stream->closed = TRUE;
    (void)ngtcp2_conn_shutdown_stream(ctx->qconn, 0, stream->id,
                                      NGHTTP3_H3_REQUEST_CANCELLED);
    result = proxy_h3_progress_egress_ngtcp2(cf, data, NULL);
    if(result)
      CURL_TRC_CF(data, cf, "[%" PRId64 "] cancel stream -> %d",
                  stream->id, result);
  }
}

/**
 * Connection maintenance like timeouts on packet ACKs etc. are done by us, not
 * the OS like for TCP. POLL events on the socket therefore are not
 * sufficient.
 * ngtcp2 tells us when it wants to be invoked again. We handle that via
 * the `Curl_expire()` mechanisms.
 */
static CURLcode check_and_set_expiry_ngtcp2(struct Curl_cfilter *cf,
                                            struct Curl_easy *data,
                                            struct proxy_pkt_io_ctx *pktx)
{
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
  struct proxy_pkt_io_ctx local_pktx;
  ngtcp2_tstamp expiry;

  if(!ctx)
    return CURLE_OK;

  if(!pktx) {
    proxy_pktx_init(&local_pktx, cf, data);
    pktx = &local_pktx;
  }
  else {
    proxy_pktx_update_time(pktx, cf);
  }

  expiry = ngtcp2_conn_get_expiry(ctx->qconn);
  if(expiry != UINT64_MAX) {
    if(expiry <= pktx->ts) {
      CURLcode result;
      int rv = ngtcp2_conn_handle_expiry(ctx->qconn, pktx->ts);
      if(rv) {
        failf(data, "ngtcp2_conn_handle_expiry returned error: %s",
              ngtcp2_strerror(rv));
        cf_ngtcp2_proxy_err_set(cf, data, rv);
        return CURLE_SEND_ERROR;
      }
      result = proxy_h3_progress_ingress_ngtcp2(cf, data, pktx);
      if(result)
        return result;
      result = proxy_h3_progress_egress_ngtcp2(cf, data, pktx);
      if(result)
        return result;
      /* ask again, things might have changed */
      expiry = ngtcp2_conn_get_expiry(ctx->qconn);
    }

    if(expiry > pktx->ts) {
      ngtcp2_duration timeout = expiry - pktx->ts;
      if(timeout % NGTCP2_MILLISECONDS) {
        timeout += NGTCP2_MILLISECONDS;
      }
      Curl_expire(data, (timediff_t)(timeout / NGTCP2_MILLISECONDS),
                  EXPIRE_QUIC);
    }
  }
  return CURLE_OK;
}

static ssize_t proxy_recv_closed_stream(struct Curl_cfilter *cf,
                                        struct Curl_easy *data,
                                        struct h3_proxy_stream_ctx *stream,
                                        CURLcode *err)
{
  ssize_t nread = -1;
  *err = CURLE_OK;

  if(stream->reset) {
    if(stream->error3 == CURL_H3_ERR_REQUEST_REJECTED) {
      infof(data, "HTTP/3 stream %" PRId64 " refused by server, try again "
            "on a new connection", stream->id);
      connclose(cf->conn, "REFUSED_STREAM");
      data->state.refused_stream = TRUE;
      *err = CURLE_RECV_ERROR;
      goto out;
    }
    else if(stream->resp_hds_complete && data->req.no_body) {
      CURL_TRC_CF(data, cf, "[%" PRId64 "] error after response headers, "
                  "but we did not want a body anyway, ignore error 0x%"
                  PRIx64 " %s", stream->id, stream->error3,
                  vquic_h3_err_str(stream->error3));
      nread = 0;
      goto out;
    }
    failf(data, "HTTP/3 stream %" PRId64 " reset by server (error 0x%" PRIx64
          " %s)", stream->id, stream->error3,
          vquic_h3_err_str(stream->error3));
    *err = data->req.bytecount ? CURLE_PARTIAL_FILE : CURLE_HTTP3;
    goto out;
  }
  else if(!stream->resp_hds_complete) {
    failf(data,
          "HTTP/3 stream %" PRId64 " was closed cleanly, but before "
          "getting all response header fields, treated as error",
          stream->id);
    *err = CURLE_HTTP3;
    goto out;
  }
  nread = 0;

out:
  return nread;
}

static struct h3_proxy_stream_ctx *h3_proxy_resolve_send_stream(
  struct cf_h3_proxy_ctx *proxy_ctx,
  struct cf_ngtcp2_proxy_ctx *ctx,
  struct Curl_easy *data)
{
  struct h3_proxy_stream_ctx *stream = H3_PROXY_STREAM_CTX(ctx, data);

  if(stream)
    return stream;

  /* send can be driven by a different easy handle during shutdown */
  if(proxy_ctx->tunnel.stream && !proxy_ctx->tunnel.closed) {
    return proxy_ctx->tunnel.stream;
  }
  return NULL;
}

static CURLcode h3_proxy_sendbuf_add(struct Curl_easy *data,
                                     struct h3_proxy_stream_ctx *stream,
                                     const uint8_t *buf, size_t len,
                                     size_t *pnwritten)
{
  CURLcode result;
  *pnwritten = 0;
  (void)data;

  result = Curl_bufq_write(&stream->sendbuf, buf, len, pnwritten);
  return result;
}

static CURLcode cf_h3_proxy_send(struct Curl_cfilter *cf,
                                 struct Curl_easy *data,
                                 const uint8_t *buf, size_t len,
                                 bool eos, size_t *pnwritten)
{
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
  struct h3_proxy_stream_ctx *stream = NULL;
  struct cf_call_data save;
  struct proxy_pkt_io_ctx pktx;
  CURLcode result = CURLE_OK;

  CF_DATA_SAVE(save, cf, data);
  DEBUGASSERT(cf->connected);
  DEBUGASSERT(ctx->qconn);
  DEBUGASSERT(ctx->h3conn);
  proxy_pktx_init(&pktx, cf, data);
  *pnwritten = 0;

  /* handshake verification failed in callback, do not send anything */
  if(ctx->tls_vrfy_result) {
    result = ctx->tls_vrfy_result;
    goto denied;
  }

  (void)eos; /* use for stream EOF and block handling */
  result = proxy_h3_progress_ingress_ngtcp2(cf, data, &pktx);
  if(result)
    goto out;

  stream = h3_proxy_resolve_send_stream(proxy_ctx, ctx, data);
  if(!stream) {
    result = CURLE_SEND_ERROR;
    goto denied;
  }

  if(proxy_ctx->tunnel.closed) {
    result = CURLE_SEND_ERROR;
    goto denied;
  }

  if(stream->closed) {
    if(stream->resp_hds_complete) {
      /* Server decided to close the stream after having sent us a final
       * response. This is valid if it is not interested in the request
       * body. This happens on 30x or 40x responses.
       * We silently discard the data sent, since this is not a transport
       * error situation. */
      CURL_TRC_CF(data, cf, "[%" PRId64 "] discarding data"
                  "on closed stream with response", stream->id);
      result = CURLE_OK;
      *pnwritten = len;
      goto out;
    }
    CURL_TRC_CF(data, cf, "[%" PRId64 "] send_body(len=%zu) "
                "-> stream closed", stream->id, len);
    result = CURLE_HTTP3;
    goto out;
  }
  else {
    result = h3_proxy_sendbuf_add(data, stream, buf, len, pnwritten);
    CURL_TRC_CF(data, cf, "[%" PRId64 "] cf_send, add to "
                "sendbuf(len=%zu) -> %d, %zu",
                stream->id, len, result, *pnwritten);
    if(result)
      goto out;
    (void)nghttp3_conn_resume_stream(ctx->h3conn, stream->id);
  }

  if(*pnwritten > 0 && !ctx->tls_handshake_complete && ctx->use_earlydata)
    ctx->earlydata_skip += *pnwritten;

  DEBUGASSERT(!result);
  result = proxy_h3_progress_egress_ngtcp2(cf, data, &pktx);

out:
  result = Curl_1st_fatal(result,
                          check_and_set_expiry_ngtcp2(cf, data, &pktx));
denied:
  CURL_TRC_CF(data, cf, "[%" PRId64 "] cf_send(len=%zu) -> %d, %zu",
              stream ? stream->id : -1, len, result, *pnwritten);
  CF_DATA_RESTORE(cf, save);
  return result;
}

/* incoming data frames on the h3 stream */
static CURLcode cf_h3_proxy_recv(struct Curl_cfilter *cf,
                                 struct Curl_easy *data,
                                 char *buf, size_t len, size_t *pnread)
{
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
  struct h3_proxy_stream_ctx *stream = H3_PROXY_STREAM_CTX(ctx, data);
  struct cf_call_data save;
  struct proxy_pkt_io_ctx pktx;
  CURLcode result = CURLE_OK;

  CF_DATA_SAVE(save, cf, data);
  DEBUGASSERT(cf->connected);
  DEBUGASSERT(ctx);
  DEBUGASSERT(ctx->qconn);
  DEBUGASSERT(ctx->h3conn);
  *pnread = 0;

  /* handshake verification failed in callback, do not recv anything */
  if(ctx->tls_vrfy_result) {
    result = ctx->tls_vrfy_result;
    goto denied;
  }

  proxy_pktx_init(&pktx, cf, data);

  if(!stream || ctx->shutdown_started) {
    result = CURLE_RECV_ERROR;
    goto out;
  }

  if(!Curl_bufq_is_empty(&proxy_ctx->inbufq)) {
    result = Curl_bufq_cread(&proxy_ctx->inbufq, buf, len, pnread);
    if(result)
      goto out;
  }

  result = proxy_h3_progress_ingress_ngtcp2(cf, data, &pktx);
  if(result)
    goto out;

  /* inbufq had nothing before, maybe after progressing ingress? */
  if(!*pnread && !Curl_bufq_is_empty(&proxy_ctx->inbufq)) {
    result = Curl_bufq_cread(&proxy_ctx->inbufq, buf, len, pnread);
    if(result) {
      CURL_TRC_CF(data, cf, "[%" PRId64 "] read inbufq(len=%zu) -> %zu, %d",
                  stream->id, len, *pnread, result);
      goto out;
    }
  }

  if(*pnread) {
    Curl_multi_mark_dirty(data);
  }
  else {
    if(stream->xfer_result) {
      CURL_TRC_CF(data, cf, "[%" PRId64 "] xfer write failed", stream->id);
      cf_ngtcp2_proxy_stream_close(cf, data, stream);
      result = stream->xfer_result;
      goto out;
    }
    else if(stream->closed) {
      ssize_t nread = proxy_recv_closed_stream(cf, data, stream, &result);
      if(nread > 0)
        *pnread = (size_t)nread;
      goto out;
    }
    result = CURLE_AGAIN;
  }

out:
  result = Curl_1st_fatal(result,
                          proxy_h3_progress_egress_ngtcp2(cf, data, &pktx));
  result = Curl_1st_fatal(result,
                          check_and_set_expiry_ngtcp2(cf, data, &pktx));
denied:
  CURL_TRC_CF(data, cf, "[%" PRId64 "] cf_recv(len=%zu) -> %d, %zu",
              stream ? stream->id : -1, len, result, *pnread);
  CF_DATA_RESTORE(cf, save);
  return result;
}

static void proxy_h3_submit(int64_t *pstream_id,
                            struct Curl_cfilter *cf,
                            struct Curl_easy *data,
                            struct httpreq *req,
                            CURLcode *err)
{
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
  struct h3_proxy_stream_ctx *stream = NULL;

  struct dynhds h2_headers;
  nghttp3_nv *nva = NULL;
  size_t nheader;

  int rc = 0;
  unsigned int i;
  nghttp3_data_reader reader;
  nghttp3_data_reader *preader = NULL;

  Curl_dynhds_init(&h2_headers, 0, DYN_HTTP_REQUEST);
  *err = Curl_http_req_to_h2(&h2_headers, req, data);
  if(*err)
    goto out;

  *err = h3_proxy_data_setup(cf, data);
  if(*err)
    goto out;

  if(!ctx) {
    *err = CURLE_FAILED_INIT;
    goto out;
  }

  stream = H3_PROXY_STREAM_CTX(ctx, data);

  DEBUGASSERT(stream);
  if(!stream) {
    *err = CURLE_FAILED_INIT;
    goto out;
  }

  nheader = Curl_dynhds_count(&h2_headers);
  nva = curlx_malloc(sizeof(nghttp3_nv) * nheader);
  if(!nva) {
    *err = CURLE_OUT_OF_MEMORY;
    goto out;
  }

  for(i = 0; i < nheader; ++i) {
    struct dynhds_entry *e = Curl_dynhds_getn(&h2_headers, i);
    nva[i].name = (unsigned char *)e->name;
    nva[i].namelen = e->namelen;
    nva[i].value = (unsigned char *)e->value;
    nva[i].valuelen = e->valuelen;
    nva[i].flags = NGHTTP3_NV_FLAG_NONE;
  }

  /* Open a bidirectional stream */
  {
    int64_t sid;
    int rv;

    DEBUGASSERT(stream->id == -1);
    rv = ngtcp2_conn_open_bidi_stream(ctx->qconn, &sid, data);
    if(rv) {
      failf(data, "cannot get bidi streams: %s", ngtcp2_strerror(rv));
      *err = CURLE_SEND_ERROR;
      goto out;
    }
    stream->id = sid;
    ++ctx->used_bidi_streams;

    /* Set stream user data in ngtcp2 connection for callbacks */
    rv = ngtcp2_conn_set_stream_user_data(ctx->qconn, sid, data);
    if(rv) {
      failf(data, "cannot set stream user data: %s", ngtcp2_strerror(rv));
      *err = CURLE_SEND_ERROR;
      goto out;
    }
    proxy_ctx->tunnel.stream = stream;
    CURL_TRC_CF(data, cf, "[%" PRId64 "] opened bidi stream", sid);
  }

  /* CONNECT-UDP request stream remains open for capsules, no fixed EOF. */
  stream->upload_left = -1;
  stream->send_closed = 0;
  reader.read_data = cb_h3_read_data_for_tunnel_stream;
  preader = &reader;

  rc = nghttp3_conn_submit_request(ctx->h3conn, H3_STREAM_ID(stream),
                                   nva, nheader, preader, data);

  if(rc) {
    switch(rc) {
    case NGHTTP3_ERR_CONN_CLOSING:
      CURL_TRC_CF(data, cf, "h3sid[%" PRId64 "] failed to send, "
                  "connection is closing", H3_STREAM_ID(stream));
      break;
    default:
      CURL_TRC_CF(data, cf, "h3sid[%" PRId64 "] failed to send -> %d (%s)",
                  H3_STREAM_ID(stream), rc, nghttp3_strerror(rc));
      break;
    }
    *err = CURLE_SEND_ERROR;
    goto out;
  }

  if(Curl_trc_is_verbose(data)) {
    CURL_TRC_CF(data, cf, "[H3-PROXY] [%" PRId64 "] OPENED stream "
                "for %s", H3_STREAM_ID(stream),
                Curl_bufref_ptr(&data->state.url));
  }

out:
  curlx_free(nva);
  Curl_dynhds_free(&h2_headers);
  if(*err == CURLE_OK) {
    *pstream_id = H3_STREAM_ID(stream);
  }
}

static bool cf_h3_proxy_is_alive(struct Curl_cfilter *cf,
                                 struct Curl_easy *data,
                                 bool *input_pending)
{
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
  bool alive = FALSE;
  const ngtcp2_transport_params *rp;
  struct cf_call_data save;

  CF_DATA_SAVE(save, cf, data);
  *input_pending = FALSE;

  if(!ctx || !ctx->qconn || ctx->shutdown_started)
    goto out;
  if(proxy_ctx->tunnel.closed)
    goto out;

  /* We do not announce a max idle timeout, but when the peer does
   * it closes the connection when it expires. */
  rp = ngtcp2_conn_get_remote_transport_params(ctx->qconn);
  if(rp && rp->max_idle_timeout) {
    timediff_t idletime_ms =
      curlx_ptimediff_ms(Curl_pgrs_now(data), &ctx->q.last_io);
    if(idletime_ms > 0) {
      uint64_t max_idle_ms =
        (uint64_t)(rp->max_idle_timeout / NGTCP2_MILLISECONDS);
      if((uint64_t)idletime_ms > max_idle_ms)
        goto out;
    }
  }

  if(!cf->next || !cf->next->cft->is_alive(cf->next, data, input_pending))
    goto out;

  alive = TRUE;
  if(*input_pending) {
    CURLcode result;
    /* This happens before we have sent off a request and the connection is
       not in use by any other transfer, there should not be any data here,
       only "protocol frames" */
    *input_pending = FALSE;
    if(!data || !data->multi) {
      alive = FALSE;
      goto out;
    }
    result = proxy_h3_progress_ingress_ngtcp2(cf, data, NULL);
    CURL_TRC_CF(data, cf, "is_alive, progress ingress -> %d", result);
    alive = result ? FALSE : TRUE;
  }

out:
  CF_DATA_RESTORE(cf, save);
  return alive;
}

static CURLcode cf_ngtcp2_proxy_query(struct Curl_cfilter *cf,
                                      struct Curl_easy *data,
                                      int query, int *pres1, void *pres2)
{
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
  struct cf_call_data save;

  if(!ctx)
    return cf->next ?
      cf->next->cft->query(cf->next, data, query, pres1, pres2) :
      CURLE_UNKNOWN_OPTION;

  switch(query) {
  case CF_QUERY_MAX_CONCURRENT: {
    DEBUGASSERT(pres1);
    CF_DATA_SAVE(save, cf, data);
    /* Set after transport params arrived and continually updated
     * by callback. QUIC counts the number over the lifetime of the
     * connection, ever increasing.
     * We count the *open* transfers plus the budget for new ones. */
    if(!ctx->qconn || ctx->shutdown_started) {
      *pres1 = 0;
    }
    else if(ctx->max_bidi_streams) {
      uint64_t avail_bidi_streams = 0;
      uint64_t max_streams = cf->conn->attached_xfers;
      if(ctx->max_bidi_streams > ctx->used_bidi_streams)
        avail_bidi_streams = ctx->max_bidi_streams - ctx->used_bidi_streams;
      max_streams += avail_bidi_streams;
      *pres1 = (max_streams > INT_MAX) ? INT_MAX : (int)max_streams;
    }
    else  /* transport params not arrived yet? take our default. */
      *pres1 = (int)Curl_multi_max_concurrent_streams(data->multi);
    CURL_TRC_CF(data, cf, "query conn[%" FMT_OFF_T "]: "
                "MAX_CONCURRENT -> %d (%u in use)",
                cf->conn->connection_id, *pres1, cf->conn->attached_xfers);
    CF_DATA_RESTORE(cf, save);
    return CURLE_OK;
  }
  case CF_QUERY_CONNECT_REPLY_MS:
    if(ctx->q.got_first_byte) {
      timediff_t ms = curlx_ptimediff_ms(&ctx->q.first_byte_at,
                                         &ctx->started_at);
      *pres1 = (ms < INT_MAX) ? (int)ms : INT_MAX;
    }
    else
      *pres1 = -1;
    return CURLE_OK;
  case CF_QUERY_TIMER_CONNECT: {
    struct curltime *when = pres2;
    if(ctx->q.got_first_byte)
      *when = ctx->q.first_byte_at;
    return CURLE_OK;
  }
  case CF_QUERY_TIMER_APPCONNECT: {
    struct curltime *when = pres2;
    if(cf->connected)
      *when = ctx->handshake_at;
    return CURLE_OK;
  }
  case CF_QUERY_HTTP_VERSION:
    *pres1 = 30;
    return CURLE_OK;
  case CF_QUERY_SSL_INFO:
  case CF_QUERY_SSL_CTX_INFO: {
    struct curl_tlssessioninfo *info = pres2;
    if(Curl_vquic_tls_get_ssl_info(&ctx->tls,
                                   (query == CF_QUERY_SSL_CTX_INFO), info))
      return CURLE_OK;
    break;
  }
  case CF_QUERY_ALPN_NEGOTIATED: {
    const char **palpn = pres2;
    DEBUGASSERT(palpn);
    *palpn = cf->connected ? "h3" : NULL;
    return CURLE_OK;
  }
  default:
    break;
  }
  return cf->next ?
    cf->next->cft->query(cf->next, data, query, pres1, pres2) :
    CURLE_UNKNOWN_OPTION;
}

static CURLcode cf_ngtcp2_proxy_adjust_pollset(struct Curl_cfilter *cf,
                                               struct Curl_easy *data,
                                               struct easy_pollset *ps)
{
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
  bool want_recv, want_send;
  CURLcode result = CURLE_OK;

  if(!ctx->qconn)
    return CURLE_OK;

  Curl_pollset_check(data, ps, ctx->q.sockfd, &want_recv, &want_send);
  if(!want_send && !Curl_bufq_is_empty(&ctx->q.sendbuf))
    want_send = TRUE;

  if(want_recv || want_send) {
    struct h3_proxy_stream_ctx *stream = H3_PROXY_STREAM_CTX(ctx, data);
    struct cf_call_data save;
    bool c_exhaust, s_exhaust;

    CF_DATA_SAVE(save, cf, data);
    c_exhaust = want_send &&
                (!ngtcp2_conn_get_cwnd_left(ctx->qconn) ||
                 !ngtcp2_conn_get_max_data_left(ctx->qconn));
    s_exhaust = want_send && stream && H3_STREAM_ID(stream) >= 0 &&
                stream->quic_flow_blocked;
    want_recv = (want_recv || c_exhaust || s_exhaust);
    want_send = (!s_exhaust && want_send) ||
                 !Curl_bufq_is_empty(&ctx->q.sendbuf);

    result = Curl_pollset_set(data, ps, ctx->q.sockfd, want_recv, want_send);
    CF_DATA_RESTORE(cf, save);
  }
  return result;
}

static CURLcode cf_h3_proxy_query(struct Curl_cfilter *cf,
                                   struct Curl_easy *data,
                                   int query, int *pres1, void *pres2)
{
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;

  if(!proxy_ctx)
    return cf->next ?
      cf->next->cft->query(cf->next, data, query, pres1, pres2) :
      CURLE_UNKNOWN_OPTION;
  return cf_ngtcp2_proxy_query(cf, data, query, pres1, pres2);
}

static CURLcode cf_h3_proxy_adjust_pollset(struct Curl_cfilter *cf,
                                           struct Curl_easy *data,
                                           struct easy_pollset *ps)
{
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;

  if(!proxy_ctx)
    return cf->next ?
      cf->next->cft->adjust_pollset(cf->next, data, ps) :
      CURLE_OK;
  return cf_ngtcp2_proxy_adjust_pollset(cf, data, ps);
}

static bool cf_h3_proxy_data_pending(struct Curl_cfilter *cf,
                                     const struct Curl_easy *data)
{
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  if(!proxy_ctx)
    return cf->next ?
      cf->next->cft->has_data_pending(cf->next, data) : FALSE;
  if(!Curl_bufq_is_empty(&proxy_ctx->inbufq))
    return TRUE;
  return cf->next ?
    cf->next->cft->has_data_pending(cf->next, data) : FALSE;
}

#ifdef USE_OPENSSL
static int proxy_quic_ossl_new_session_cb(SSL *ssl, SSL_SESSION *ssl_sessionid)
{
  ngtcp2_crypto_conn_ref *cref;
  struct Curl_cfilter *cf;
  struct cf_ngtcp2_proxy_ctx *ctx;
  struct cf_h3_proxy_ctx *proxy_ctx;
  struct Curl_easy *data;

  cref = (ngtcp2_crypto_conn_ref *)SSL_get_app_data(ssl);
  cf = cref ? cref->user_data : NULL;
  proxy_ctx = cf ? cf->ctx : NULL;
  ctx = proxy_ctx ? proxy_ctx->ngtcp2_ctx : NULL;
  data = cf ? CF_DATA_CURRENT(cf) : NULL;
  if(cf && data && ctx) {
    unsigned char *quic_tp = NULL;
    size_t quic_tp_len = 0;
#ifdef HAVE_OPENSSL_EARLYDATA
    ngtcp2_ssize tplen;
    uint8_t tpbuf[256];

    tplen = ngtcp2_conn_encode_0rtt_transport_params(ctx->qconn, tpbuf,
                                                     sizeof(tpbuf));
    if(tplen < 0)
      CURL_TRC_CF(data, cf, "error encoding 0RTT transport data: %s",
                  ngtcp2_strerror((int)tplen));
    else {
      quic_tp = (unsigned char *)tpbuf;
      quic_tp_len = (size_t)tplen;
    }
#endif
    Curl_ossl_add_session(cf, data, ctx->peer.scache_key, ssl_sessionid,
                          SSL_version(ssl), "h3", quic_tp, quic_tp_len);
  }
  return 0;
}
#endif /* USE_OPENSSL */

static CURLcode cf_ngtcp2_proxy_tls_ctx_setup(struct Curl_cfilter *cf,
                                              struct Curl_easy *data,
                                              void *user_data)
{
  struct curl_tls_ctx *ctx = user_data;

#ifdef USE_OPENSSL
#if defined(OPENSSL_IS_AWSLC) || defined(OPENSSL_IS_BORINGSSL)
  if(ngtcp2_crypto_boringssl_configure_client_context(ctx->ossl.ssl_ctx)
     != 0) {
    failf(data, "ngtcp2_crypto_boringssl_configure_client_context failed");
    return CURLE_FAILED_INIT;
  }
#elif defined(OPENSSL_QUIC_API2)
  /* nothing to do */
#else
  if(ngtcp2_crypto_quictls_configure_client_context(ctx->ossl.ssl_ctx) != 0) {
    failf(data, "ngtcp2_crypto_quictls_configure_client_context failed");
    return CURLE_FAILED_INIT;
  }
#endif /* !OPENSSL_IS_AWSLC && !OPENSSL_IS_BORINGSSL */
  if(Curl_ssl_scache_use(cf, data)) {
    SSL_CTX_set_session_cache_mode(ctx->ossl.ssl_ctx,
                                   SSL_SESS_CACHE_CLIENT |
                                   SSL_SESS_CACHE_NO_INTERNAL);
    SSL_CTX_sess_set_new_cb(ctx->ossl.ssl_ctx, proxy_quic_ossl_new_session_cb);
  }

#else
#error "ngtcp2 TLS backend not configured"
#endif /* USE_OPENSSL */

  return CURLE_OK;
}

static CURLcode cf_ngtcp2_proxy_on_session_reuse(struct Curl_cfilter *cf,
                                                 struct Curl_easy *data,
                                                 struct alpn_spec *alpns,
                                                 struct Curl_ssl_session *scs,
                                                 bool *do_early_data)
{
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
  CURLcode result = CURLE_OK;

  *do_early_data = FALSE;
#if defined(USE_OPENSSL) && defined(HAVE_OPENSSL_EARLYDATA)
  ctx->earlydata_max = scs->earlydata_max;
#endif
#ifdef USE_GNUTLS
  ctx->earlydata_max =
    gnutls_record_get_max_early_data_size(ctx->tls.gtls.session);
#endif
#ifdef USE_WOLFSSL
#ifdef WOLFSSL_EARLY_DATA
  ctx->earlydata_max = scs->earlydata_max;
#else
  ctx->earlydata_max = 0;
#endif /* WOLFSSL_EARLY_DATA */
#endif
#if defined(USE_GNUTLS) || defined(USE_WOLFSSL) || \
  (defined(USE_OPENSSL) && defined(HAVE_OPENSSL_EARLYDATA))
  if(!ctx->earlydata_max) {
    CURL_TRC_CF(data, cf, "SSL session does not allow earlydata");
  }
  else if(!Curl_alpn_contains_proto(alpns, scs->alpn)) {
    CURL_TRC_CF(data, cf, "SSL session from different ALPN, no early data");
  }
  else if(!scs->quic_tp || !scs->quic_tp_len) {
    CURL_TRC_CF(data, cf, "no 0RTT transport parameters, no early data");
  }
  else {
    int rv;
    rv = ngtcp2_conn_decode_and_set_0rtt_transport_params(
      ctx->qconn, (const uint8_t *)scs->quic_tp, scs->quic_tp_len);
    if(rv)
      CURL_TRC_CF(data, cf, "no early data, failed to set 0RTT transport "
                  "parameters: %s", ngtcp2_strerror(rv));
    else {
      infof(data, "SSL session allows %zu bytes of early data, "
            "reusing ALPN '%s'", ctx->earlydata_max, scs->alpn);
      result = cf_ngtcp2_h3conn_init(cf, data);
      if(!result) {
        ctx->use_earlydata = TRUE;
        proxy_ctx->connected = TRUE;
        *do_early_data = TRUE;
      }
    }
  }
#else /* not supported in the TLS backend */
  (void)data;
  (void)ctx;
  (void)scs;
  (void)alpns;
#endif
  return result;
}

static CURLcode cf_h3_proxy_ctx_init(struct Curl_cfilter *cf,
                                     struct Curl_easy *data)
{
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = NULL;
  int rc;
  int rv;
  CURLcode result = CURLE_OK;
  const struct Curl_sockaddr_ex *sockaddr = NULL;
  int qfd;
  static const struct alpn_spec ALPN_SPEC_H3 = { { "h3", "h3-29" }, 2 };
  struct proxy_pkt_io_ctx pktx;

  ctx = curlx_calloc(1, sizeof(struct cf_ngtcp2_proxy_ctx));
  if(!ctx) {
    result = CURLE_OUT_OF_MEMORY;
    goto out;
  }
  cf_ngtcp2_proxy_ctx_init(ctx);

  memset(&proxy_ctx->tunnel, 0, sizeof(proxy_ctx->tunnel));

  Curl_bufq_init2(&proxy_ctx->inbufq, PROXY_H3_STREAM_CHUNK_SIZE,
                  PROXY_H3_STREAM_RECV_CHUNKS, BUFQ_OPT_SOFT_LIMIT);

  result = h3_tunnel_stream_init(&proxy_ctx->tunnel, proxy_ctx->dest);
  if(result)
    goto out;

  DEBUGASSERT(ctx->initialized);
  ctx->started_at = *Curl_pgrs_now(data);

  /* Initialize connection IDs BEFORE creating the connection */
  ctx->dcid.datalen = NGTCP2_MAX_CIDLEN;
  result = Curl_rand(data, ctx->dcid.data, NGTCP2_MAX_CIDLEN);
  if(result)
    goto out;

  ctx->scid.datalen = NGTCP2_MAX_CIDLEN;
  result = Curl_rand(data, ctx->scid.data, NGTCP2_MAX_CIDLEN);
  if(result)
    goto out;

  (void)Curl_qlogdir(data, ctx->scid.data, NGTCP2_MAX_CIDLEN, &qfd);
  ctx->qlogfd = qfd; /* -1 if failure above */

  result = CURLE_QUIC_CONNECT_ERROR;
  if(!cf->next) {
    CURL_TRC_CF(data, cf, "h3_proxy_ctx_init: no lower filter");
    goto out;
  }
  ctx->q.sockfd = Curl_conn_cf_get_socket(cf->next, data);
  if(ctx->q.sockfd == CURL_SOCKET_BAD)
    goto out;
  /* Get remote address from the socket filter below */
  if(cf->next->cft->query(cf->next, data, CF_QUERY_REMOTE_ADDR, NULL,
                          CURL_UNCONST(&sockaddr)))
    goto out;
  if(!sockaddr)
    goto out;
  ctx->q.local_addrlen = sizeof(ctx->q.local_addr);
  rv = getsockname(ctx->q.sockfd, (struct sockaddr *)&ctx->q.local_addr,
                   &ctx->q.local_addrlen);
  if(rv == -1)
    goto out;

  /* Initialize vquic context BEFORE proxy_pktx_init which needs it */
  result = vquic_ctx_init(data, &ctx->q);
  if(result)
    goto out;

  /* Set ngtcp2_ctx in proxy_ctx BEFORE proxy_pktx_init which accesses it */
  proxy_ctx->ngtcp2_ctx = ctx;

  /* Now we can safely initialize pktx and settings */
  proxy_pktx_init(&pktx, cf, data);
  quic_settings_proxy(ctx, data, &pktx);

  ngtcp2_addr_init(&ctx->connected_path.local,
                   (struct sockaddr *)&ctx->q.local_addr,
                   ctx->q.local_addrlen);
  ngtcp2_addr_init(&ctx->connected_path.remote,
                   &sockaddr->curl_sa_addr, (socklen_t)sockaddr->addrlen);

  rc = ngtcp2_conn_client_new(&ctx->qconn, &ctx->dcid, &ctx->scid,
                              &ctx->connected_path,
                              NGTCP2_PROTO_VER_V1, &ngtcp2_proxy_callbacks,
                              &ctx->settings, &ctx->transport_params,
                              Curl_ngtcp2_mem(), cf);
  if(rc) {
    result = CURLE_QUIC_CONNECT_ERROR;
    goto out;
  }

  ctx->conn_ref.get_conn = proxy_get_conn;
  ctx->conn_ref.user_data = cf;

  result = Curl_vquic_tls_init(&ctx->tls, cf, data, &ctx->peer, &ALPN_SPEC_H3,
                               cf_ngtcp2_proxy_tls_ctx_setup, &ctx->tls,
                               &ctx->conn_ref,
                               cf_ngtcp2_proxy_on_session_reuse);
  if(result)
    goto out;

#if defined(USE_OPENSSL) && defined(OPENSSL_QUIC_API2)
  if(ngtcp2_crypto_ossl_ctx_new(&ctx->ossl_ctx, ctx->tls.ossl.ssl) != 0) {
    failf(data, "ngtcp2_crypto_ossl_ctx_new failed");
    result = CURLE_FAILED_INIT;
    goto out;
  }
  ngtcp2_conn_set_tls_native_handle(ctx->qconn, ctx->ossl_ctx);
  if(ngtcp2_crypto_ossl_configure_client_session(ctx->tls.ossl.ssl) != 0) {
    failf(data, "ngtcp2_crypto_ossl_configure_client_session failed");
    result = CURLE_FAILED_INIT;
    goto out;
  }
#elif defined(USE_OPENSSL)
  SSL_set_quic_use_legacy_codepoint(ctx->tls.ossl.ssl, 0);
  ngtcp2_conn_set_tls_native_handle(ctx->qconn, ctx->tls.ossl.ssl);
#else
#error "ngtcp2 TLS backend not defined"
#endif /* USE_OPENSSL */

  ngtcp2_ccerr_default(&ctx->last_error);

  proxy_ctx->connected = FALSE;

out:
  if(result) {
    if(ctx) {
      proxy_ctx->ngtcp2_ctx = NULL; /* Clear before freeing on error */
      cf_ngtcp2_proxy_ctx_free(ctx);
    }
  }
  CURL_TRC_CF(data, cf, "QUIC tls init -> %d", result);
  return result;
}

static CURLcode h3_submit_CONNECT(struct Curl_cfilter *cf,
                                  struct Curl_easy *data,
                                  struct h3_tunnel_stream *ts)
{
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  CURLcode result;
  struct httpreq *req = NULL;

  result = Curl_http_proxy_create_tunnel_request(&req, cf, data,
                                                  proxy_ctx->dest,
                                                  PROXY_HTTP_V3,
                                                  (bool)proxy_ctx->udp_tunnel);
  if(result)
    goto out;
  result = Curl_creader_set_null(data);
  if(result)
    goto out;

  proxy_h3_submit(&ts->stream_id, cf, data, req, &result);

out:
  if(req)
    Curl_http_req_free(req);
  if(result)
    failf(data, "Failed sending CONNECT to proxy");
  return result;
}

static CURLcode h3_proxy_inspect_response(struct Curl_cfilter *cf,
                                          struct Curl_easy *data,
                                          struct h3_tunnel_stream *ts)
{
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  proxy_inspect_result res;
  CURLcode result;

  result = Curl_http_proxy_inspect_tunnel_response(
      cf, data, ts->resp, (bool)proxy_ctx->udp_tunnel, &res);
  if(result)
    return result;
  switch(res) {
  case PROXY_INSPECT_OK:
    h3_tunnel_go_state(cf, ts, H3_TUNNEL_ESTABLISHED, data,
                       (bool)proxy_ctx->udp_tunnel);
    break;
  case PROXY_INSPECT_FAILED:
    h3_tunnel_go_state(cf, ts, H3_TUNNEL_FAILED, data,
                       (bool)proxy_ctx->udp_tunnel);
    result = CURLE_COULDNT_CONNECT;
    break;
  case PROXY_INSPECT_AUTH_RETRY:
    h3_tunnel_go_state(cf, ts, H3_TUNNEL_INIT, data,
                       (bool)proxy_ctx->udp_tunnel);
    break;
  }
  return result;
}

static CURLcode cf_h3_proxy_quic_connect(struct Curl_cfilter *cf,
                                         struct Curl_easy *data,
                                         bool *done)
{
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_call_data save;
  CURLcode result = CURLE_OK;
  struct proxy_pkt_io_ctx pktx;

  if(proxy_ctx->connected) {
    *done = TRUE;
    return CURLE_OK;
  }

  /* Connect the sub-chain (UDP via happy eyeballs) */
  if(cf->next && !cf->next->connected) {
    result = Curl_conn_cf_connect(cf->next, data, done);
    if(result || !*done)
      return result;
  }

  *done = FALSE;
  if(!proxy_ctx->dest) {
    Curl_peer_link(&proxy_ctx->dest,
                   Curl_conn_get_destination(cf->conn, cf->sockindex));
  }

  if(!proxy_ctx->ngtcp2_ctx) {
    result = cf_h3_proxy_ctx_init(cf, data);
    if(result)
      return result;
  }

  /* Initialize pktx AFTER ensuring ngtcp2_ctx exists */
  proxy_pktx_init(&pktx, cf, data);

  CF_DATA_SAVE(save, cf, data);

  if(!proxy_ctx->ngtcp2_ctx->qconn) {
    proxy_ctx->ngtcp2_ctx->started_at = *Curl_pgrs_now(data);
    if(proxy_ctx->connected) {
      *done = TRUE;
      goto out;
    }
    result = proxy_h3_progress_egress_ngtcp2(cf, data, &pktx);
    /* we do not expect to be able to recv anything yet */
    goto out;
  }

  result = proxy_h3_progress_ingress_ngtcp2(cf, data, &pktx);
  if(result)
    goto out;

  result = proxy_h3_progress_egress_ngtcp2(cf, data, &pktx);
  if(result)
    goto out;

  if(ngtcp2_conn_get_handshake_completed(proxy_ctx->ngtcp2_ctx->qconn)) {
    result = proxy_ctx->ngtcp2_ctx->tls_vrfy_result;
    if(!result) {
      CURL_TRC_CF(data, cf, "peer verified");
      proxy_ctx->connected = TRUE;
      *done = TRUE;
      connkeep(cf->conn, "HTTP/3 default");
    }
  }

out:
  if(proxy_ctx->ngtcp2_ctx->qconn &&
     ((result == CURLE_RECV_ERROR) || (result == CURLE_SEND_ERROR)) &&
     ngtcp2_conn_in_draining_period(proxy_ctx->ngtcp2_ctx->qconn)) {
    const ngtcp2_ccerr *cerr =
      ngtcp2_conn_get_ccerr(proxy_ctx->ngtcp2_ctx->qconn);

    result = CURLE_COULDNT_CONNECT;
    if(cerr) {
      CURL_TRC_CF(data, cf, "connect error, type=%d, code=%" PRIu64,
                  cerr->type, cerr->error_code);
      switch(cerr->type) {
      case NGTCP2_CCERR_TYPE_VERSION_NEGOTIATION:
        CURL_TRC_CF(data, cf, "error in version negotiation");
        break;
      default:
        if(cerr->error_code >= NGTCP2_CRYPTO_ERROR) {
          CURL_TRC_CF(data, cf, "crypto error, tls alert=%u",
                      (unsigned int)(cerr->error_code & 0xffU));
        }
        else if(cerr->error_code == NGTCP2_CONNECTION_REFUSED) {
          CURL_TRC_CF(data, cf, "connection refused by server");
          /* When a QUIC server instance is shutting down, it may send us a
           * CONNECTION_CLOSE with this code right away. We want
           * to keep on trying in this case. */
          result = CURLE_WEIRD_SERVER_REPLY;
        }
      }
    }
  }

#ifdef CURLVERBOSE
  if(result) {
    bool is_ipv6;
    struct ip_quadruple ip;
    if(!Curl_conn_cf_get_ip_info(cf->next, data, &is_ipv6, &ip))
      infof(data, "QUIC connect to %s port %u failed: %s",
            ip.remote_ip, ip.remote_port, curl_easy_strerror(result));
  }
#endif
  if(!result && proxy_ctx->ngtcp2_ctx->qconn) {
    result = check_and_set_expiry_ngtcp2(cf, data, &pktx);
  }
  if(result || *done)
    CURL_TRC_CF(data, cf, "connect -> %d, done=%d", result, *done);
  CF_DATA_RESTORE(cf, save);
  return result;
}

static CURLcode H3_CONNECT(struct Curl_cfilter *cf,
                           struct Curl_easy *data,
                           struct h3_tunnel_stream *ts)
{
  struct cf_h3_proxy_ctx *ctx = cf->ctx;
  CURLcode result = CURLE_OK;

  DEBUGASSERT(ts);
  DEBUGASSERT(ts->authority);

  do {
    switch(ts->state) {
    case H3_TUNNEL_INIT:
      CURL_TRC_CF(data, cf, "[0] CONNECT start for %s", ts->authority);
      result = h3_submit_CONNECT(cf, data, ts);
      if(result)
        goto out;
      h3_tunnel_go_state(cf, ts, H3_TUNNEL_CONNECT, data,
                         (bool)ctx->udp_tunnel);

      result = proxy_h3_progress_egress_ngtcp2(cf, data, NULL);
      if(result)
        goto out;
      FALLTHROUGH();

    case H3_TUNNEL_CONNECT:
      /* Non-blocking: call ingress/egress once and return.
       * The multi interface will call us again when ready. */
      result = proxy_h3_progress_ingress_ngtcp2(cf, data, NULL);
      if(result)
        goto out;
      result = proxy_h3_progress_egress_ngtcp2(cf, data, NULL);
      if(result && result != CURLE_AGAIN) {
        h3_tunnel_go_state(cf, ts, H3_TUNNEL_FAILED, data,
                           (bool)ctx->udp_tunnel);
        goto out;
      }

      if(ts->has_final_response) {
        h3_tunnel_go_state(cf, ts, H3_TUNNEL_RESPONSE, data,
                           (bool)ctx->udp_tunnel);
      }
      else {
        /* Not done yet, return and let multi interface call us again */
        result = CURLE_OK;
        goto out;
      }
      FALLTHROUGH();

    case H3_TUNNEL_RESPONSE:
      DEBUGASSERT(ts->has_final_response);
      result = h3_proxy_inspect_response(cf, data, ts);
      if(result)
        goto out;
      ctx->connected = TRUE;
      break;

    case H3_TUNNEL_ESTABLISHED:
      return CURLE_OK;

    case H3_TUNNEL_FAILED:
      return CURLE_RECV_ERROR;

    default:
      break;
    }

  } while(ts->state == H3_TUNNEL_INIT);

out:
  if((result && (result != CURLE_AGAIN)) || ctx->tunnel.closed)
    h3_tunnel_go_state(cf, ts, H3_TUNNEL_FAILED, data, (bool)ctx->udp_tunnel);
  return result;
}

static CURLcode cf_h3_proxy_connect(struct Curl_cfilter *cf,
                                    struct Curl_easy *data, bool *done)
{
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_call_data save = { 0 };
  CURLcode result = CURLE_OK;
  timediff_t check;
  struct h3_tunnel_stream *ts = &proxy_ctx->tunnel;
  bool data_saved = FALSE;

  /* Curl_cft_http_proxy --> Curl_cft_h3_proxy --> HAPPY-EYEBALLS --> UDP */
  if(cf->connected) {
    *done = TRUE;
    return CURLE_OK;
  }

  *done = FALSE;

  check = Curl_timeleft_ms(data);
  if(check <= 0) {
    failf(data, "Proxy CONNECT aborted due to timeout");
    result = CURLE_OPERATION_TIMEDOUT;
    goto out;
  }

  result = cf_h3_proxy_quic_connect(cf, data, done);
  if(*done != TRUE)
    goto out;

  CF_DATA_SAVE(save, cf, data);
  data_saved = TRUE;

  /* At this point the QUIC is connected, but the proxy isn't connected */
  *done = FALSE;

  result = H3_CONNECT(cf, data, ts);

out:
  *done = (result == CURLE_OK) && (ts->state == H3_TUNNEL_ESTABLISHED);
  if(*done) {
    cf->connected = TRUE;
    /* The real request will follow the CONNECT, reset request partially */
    Curl_req_soft_reset(&data->req, data);
    Curl_client_reset(data);
  }

  if(data_saved)
    CF_DATA_RESTORE(cf, save);
  return result;
}

static CURLcode h3_proxy_data_pause(struct Curl_cfilter *cf,
                              struct Curl_easy *data,
                              bool pause)
{
  (void)cf;
  if(!pause) {
    /* unpaused. make it run again right away */
    Curl_multi_mark_dirty(data);
  }
  return CURLE_OK;
}

static void h3_proxy_data_done(struct Curl_cfilter *cf, struct Curl_easy *data)
{
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
  struct h3_proxy_stream_ctx *stream;

  if(!ctx)
    return;

  stream = H3_PROXY_STREAM_CTX(ctx, data);
  if(stream) {
    CURL_TRC_CF(data, cf, "[%" PRId64 "] easy handle is done", stream->id);
    cf_ngtcp2_proxy_stream_close(cf, data, stream);
    Curl_uint32_hash_remove(&ctx->streams, data->mid);
    if(!Curl_uint32_hash_count(&ctx->streams))
      cf_ngtcp2_proxy_setup_keep_alive(cf, data);
  }
}

static CURLcode cf_h3_proxy_cntrl(struct Curl_cfilter *cf,
                                  struct Curl_easy *data,
                                  int event, int arg1, void *arg2)
{
  struct cf_h3_proxy_ctx *proxy_ctx = cf->ctx;
  struct cf_call_data save;
  CURLcode result = CURLE_OK;

  CF_DATA_SAVE(save, cf, data);

  (void)arg1;
  (void)arg2;
  switch(event) {
  case CF_CTRL_DATA_SETUP:
    break;
  case CF_CTRL_DATA_PAUSE:
    result = h3_proxy_data_pause(cf, data, (arg1 != 0));
    break;
  case CF_CTRL_DATA_DONE:
    h3_proxy_data_done(cf, data);
    break;
  case CF_CTRL_DATA_DONE_SEND: {
    struct cf_ngtcp2_proxy_ctx *ctx = proxy_ctx->ngtcp2_ctx;
    struct h3_proxy_stream_ctx *stream = NULL;
    if(ctx) {
      stream = H3_PROXY_STREAM_CTX(ctx, data);
      if(stream && !stream->send_closed &&
         (H3_STREAM_ID(stream) != proxy_ctx->tunnel.stream_id)) {
        stream->send_closed = TRUE;
        stream->upload_left = Curl_bufq_len(&stream->sendbuf) -
          stream->sendbuf_len_in_flight;
        (void)nghttp3_conn_resume_stream(ctx->h3conn, H3_STREAM_ID(stream));
      }
    }
    break;
  }
  case CF_CTRL_CONN_INFO_UPDATE:
    if(!cf->sockindex && cf->connected) {
      cf->conn->httpversion_seen = 30;
      Curl_conn_set_multiplex(cf->conn);
    }
    break;
  default:
    break;
  }

  CF_DATA_RESTORE(cf, save);
  return result;
}

static void cf_h3_proxy_destroy(struct Curl_cfilter *cf,
                                struct Curl_easy *data)
{
  struct cf_h3_proxy_ctx *ctx = cf->ctx;

  if(ctx) {
    /* Clean up the ngtcp2 context properly */
    if(ctx->ngtcp2_ctx) {
      CURL_TRC_CF(data, cf, "cf_ngtcp2_proxy_ctx_close()");
      cf_ngtcp2_proxy_close(cf, data);
      cf_ngtcp2_proxy_ctx_free(ctx->ngtcp2_ctx);
      ctx->ngtcp2_ctx = NULL;
    }
    cf_h3_proxy_ctx_free(ctx);
    cf->ctx = NULL;
  }
}

static CURLcode cf_h3_proxy_shutdown(struct Curl_cfilter *cf,
                                     struct Curl_easy *data, bool *done)
{
  return cf_ngtcp2_proxy_shutdown(cf, data, done);
}

struct Curl_cftype Curl_cft_h3_proxy = {
  "H3-PROXY",
  CF_TYPE_IP_CONNECT | CF_TYPE_PROXY | CF_TYPE_SSL,
  CURL_LOG_LVL_NONE,
  cf_h3_proxy_destroy,
  cf_h3_proxy_connect,
  cf_h3_proxy_shutdown,
  cf_h3_proxy_adjust_pollset,
  cf_h3_proxy_data_pending,
  cf_h3_proxy_send,
  cf_h3_proxy_recv,
  cf_h3_proxy_cntrl,
  cf_h3_proxy_is_alive,
  Curl_cf_def_conn_keep_alive,
  cf_h3_proxy_query,
};

CURLcode Curl_cf_ngtcp2_proxy_create(struct Curl_cfilter **pcf,
                                     struct Curl_easy *data,
                                     struct connectdata *conn,
                                     struct Curl_sockaddr_ex *addr,
                                     uint8_t transport_in,
                                     uint8_t transport_out)
{
  struct Curl_cfilter *cf = NULL;
  struct cf_h3_proxy_ctx *ctx;
  CURLcode result = CURLE_OUT_OF_MEMORY;

  if((transport_out != TRNSPRT_QUIC) || (!conn->http_proxy.peer))
    return CURLE_FAILED_INIT;

  ctx = curlx_calloc(1, sizeof(*ctx));
  if(!ctx) {
    result = CURLE_OUT_OF_MEMORY;
    goto out;
  }
  ctx->udp_tunnel = (transport_in == TRNSPRT_QUIC);

  result = Curl_cf_create(&cf, &Curl_cft_h3_proxy, ctx);
  if(result)
    goto out;
  cf->conn = conn;

  result = Curl_cf_udp_create(&cf->next, data, conn, addr,
                              TRNSPRT_QUIC, TRNSPRT_QUIC);
  if(result)
    goto out;
  cf->next->conn = cf->conn;
  cf->next->sockindex = cf->sockindex;

out:
  *pcf = (!result) ? cf : NULL;
  if(result) {
    if(cf)
      Curl_conn_cf_discard_chain(&cf, data);
    else if(ctx)
      cf_h3_proxy_ctx_free(ctx);
  }
  else
    CURL_TRC_CF(data, cf, "created, udp_tunnel=%d", ctx->udp_tunnel);
  return result;
}

CURLcode Curl_cf_ngtcp2_proxy_insert_after(struct Curl_cfilter *cf_at,
                                           struct Curl_easy *data,
                                           struct Curl_peer *dest,
                                           bool udp_tunnel)
{
  struct Curl_cfilter *cf = NULL;
  struct cf_h3_proxy_ctx *ctx;
  CURLcode result = CURLE_OUT_OF_MEMORY;
  (void)data;

  ctx = curlx_calloc(1, sizeof(*ctx));
  if(!ctx)
    goto out;
  Curl_peer_link(&ctx->dest, dest);
  ctx->udp_tunnel = udp_tunnel;

  result = Curl_cf_create(&cf, &Curl_cft_h3_proxy, ctx);
  if(result)
    goto out;

  /* H3-PROXY uses the UDP socket created by happy eyeballs below it.
     Curl_conn_cf_insert_after chains the existing sub-filters, i.e.
     "HAPPY-EYEBALLS -> UDP" as cf->next of H3-PROXY. */
  Curl_conn_cf_insert_after(cf_at, cf);

out:
  if(result) {
    if(cf)
      Curl_conn_cf_discard_chain(&cf, data);
    else if(ctx)
      cf_h3_proxy_ctx_free(ctx);
  }
  return result;
}

#endif
