changeset 8751:bc910a5ec737 quic

QUIC: separate files for output and ack related processing.
author Vladimir Homutov <vl@nginx.com>
date Tue, 13 Apr 2021 14:41:20 +0300
parents 41807e581de9
children e19723c40d28
files auto/modules src/event/quic/ngx_event_quic.c src/event/quic/ngx_event_quic_ack.c src/event/quic/ngx_event_quic_ack.h src/event/quic/ngx_event_quic_connection.h src/event/quic/ngx_event_quic_output.c src/event/quic/ngx_event_quic_output.h
diffstat 7 files changed, 2027 insertions(+), 1955 deletions(-) [+]
line wrap: on
line diff
--- a/auto/modules
+++ b/auto/modules
@@ -1346,14 +1346,18 @@ if [ $USE_OPENSSL$USE_OPENSSL_QUIC = YES
                      src/event/quic/ngx_event_quic_frames.h \
                      src/event/quic/ngx_event_quic_connid.h \
                      src/event/quic/ngx_event_quic_migration.h \
-                     src/event/quic/ngx_event_quic_streams.h"
+                     src/event/quic/ngx_event_quic_streams.h \
+                     src/event/quic/ngx_event_quic_ack.h \
+                     src/event/quic/ngx_event_quic_output.h"
     ngx_module_srcs="src/event/quic/ngx_event_quic.c \
                      src/event/quic/ngx_event_quic_transport.c \
                      src/event/quic/ngx_event_quic_protection.c \
                      src/event/quic/ngx_event_quic_frames.c \
                      src/event/quic/ngx_event_quic_connid.c \
                      src/event/quic/ngx_event_quic_migration.c \
-                     src/event/quic/ngx_event_quic_streams.c"
+                     src/event/quic/ngx_event_quic_streams.c \
+                     src/event/quic/ngx_event_quic_ack.c \
+                     src/event/quic/ngx_event_quic_output.c"
 
     ngx_module_libs=
     ngx_module_link=YES
--- a/src/event/quic/ngx_event_quic.c
+++ b/src/event/quic/ngx_event_quic.c
@@ -11,28 +11,12 @@
 #include <ngx_event_quic_connection.h>
 
 
-#define ngx_quic_lost_threshold(qc)                                           \
-    ngx_max(NGX_QUIC_TIME_THR * ngx_max((qc)->latest_rtt, (qc)->avg_rtt),     \
-            NGX_QUIC_TIME_GRANULARITY)
-
 /*
  * 7.4.  Cryptographic Message Buffering
  *       Implementations MUST support buffering at least 4096 bytes of data
  */
 #define NGX_QUIC_MAX_BUFFERED    65535
 
-/*
- * Endpoints MUST discard packets that are too small to be valid QUIC
- * packets.  With the set of AEAD functions defined in [QUIC-TLS],
- * packets that are smaller than 21 bytes are never valid.
- */
-#define NGX_QUIC_MIN_PKT_LEN     21
-
-#define NGX_QUIC_MIN_SR_PACKET   43 /* 5 random + 16 srt + 22 padding */
-#define NGX_QUIC_MAX_SR_PACKET   1200
-
-#define NGX_QUIC_MAX_ACK_GAP     2
-
 
 #if BORINGSSL_API_VERSION >= 10
 static int ngx_quic_set_read_secret(ngx_ssl_conn_t *ssl_conn,
@@ -50,30 +34,19 @@ static int ngx_quic_set_encryption_secre
 static int ngx_quic_add_handshake_data(ngx_ssl_conn_t *ssl_conn,
     enum ssl_encryption_level_t level, const uint8_t *data, size_t len);
 static int ngx_quic_flush_flight(ngx_ssl_conn_t *ssl_conn);
-static int ngx_quic_send_alert(ngx_ssl_conn_t *ssl_conn,
-    enum ssl_encryption_level_t level, uint8_t alert);
 
 
 static ngx_int_t ngx_quic_apply_transport_params(ngx_connection_t *c,
     ngx_quic_tp_t *ctp);
 static ngx_quic_connection_t *ngx_quic_new_connection(ngx_connection_t *c,
     ngx_quic_conf_t *conf, ngx_quic_header_t *pkt);
-static ngx_int_t ngx_quic_send_stateless_reset(ngx_connection_t *c,
-    ngx_quic_conf_t *conf, ngx_quic_header_t *pkt);
 static ngx_int_t ngx_quic_process_stateless_reset(ngx_connection_t *c,
     ngx_quic_header_t *pkt);
-static ngx_int_t ngx_quic_negotiate_version(ngx_connection_t *c,
-    ngx_quic_header_t *inpkt);
-static ngx_int_t ngx_quic_send_retry(ngx_connection_t *c,
-    ngx_quic_conf_t *conf, ngx_quic_header_t *pkt);
-static ngx_int_t ngx_quic_new_token(ngx_connection_t *c, u_char *key,
-    ngx_str_t *token, ngx_str_t *odcid, time_t expires, ngx_uint_t is_retry);
 static void ngx_quic_address_hash(ngx_connection_t *c, ngx_uint_t no_port,
     u_char buf[20]);
 static ngx_int_t ngx_quic_validate_token(ngx_connection_t *c,
     u_char *key, ngx_quic_header_t *pkt);
 static ngx_int_t ngx_quic_init_connection(ngx_connection_t *c);
-static ngx_inline size_t ngx_quic_max_udp_payload(ngx_connection_t *c);
 static void ngx_quic_input_handler(ngx_event_t *rev);
 
 static ngx_int_t ngx_quic_close_quic(ngx_connection_t *c, ngx_int_t rc);
@@ -85,60 +58,21 @@ static ngx_int_t ngx_quic_process_packet
     ngx_quic_conf_t *conf, ngx_quic_header_t *pkt);
 static ngx_int_t ngx_quic_process_payload(ngx_connection_t *c,
     ngx_quic_header_t *pkt);
-static ngx_int_t ngx_quic_send_early_cc(ngx_connection_t *c,
-    ngx_quic_header_t *inpkt, ngx_uint_t err, const char *reason);
 static void ngx_quic_discard_ctx(ngx_connection_t *c,
     enum ssl_encryption_level_t level);
 static ngx_int_t ngx_quic_check_csid(ngx_quic_connection_t *qc,
     ngx_quic_header_t *pkt);
 static ngx_int_t ngx_quic_handle_frames(ngx_connection_t *c,
     ngx_quic_header_t *pkt);
-static ngx_int_t ngx_quic_ack_packet(ngx_connection_t *c,
-    ngx_quic_header_t *pkt);
-static ngx_int_t ngx_quic_send_ack_range(ngx_connection_t *c,
-    ngx_quic_send_ctx_t *ctx, uint64_t smallest, uint64_t largest);
-static void ngx_quic_drop_ack_ranges(ngx_connection_t *c,
-    ngx_quic_send_ctx_t *ctx, uint64_t pn);
-static ngx_int_t ngx_quic_send_ack(ngx_connection_t *c,
-    ngx_quic_send_ctx_t *ctx);
-static ngx_int_t ngx_quic_send_cc(ngx_connection_t *c);
-static ngx_int_t ngx_quic_send_new_token(ngx_connection_t *c);
-
-static ngx_int_t ngx_quic_handle_ack_frame(ngx_connection_t *c,
-    ngx_quic_header_t *pkt, ngx_quic_frame_t *f);
-static ngx_int_t ngx_quic_handle_ack_frame_range(ngx_connection_t *c,
-    ngx_quic_send_ctx_t *ctx, uint64_t min, uint64_t max,
-    ngx_msec_t *send_time);
-static void ngx_quic_rtt_sample(ngx_connection_t *c, ngx_quic_ack_frame_t *ack,
-    enum ssl_encryption_level_t level, ngx_msec_t send_time);
+
 
 static ngx_int_t ngx_quic_handle_crypto_frame(ngx_connection_t *c,
     ngx_quic_header_t *pkt, ngx_quic_frame_t *frame);
 ngx_int_t ngx_quic_crypto_input(ngx_connection_t *c,
     ngx_quic_frame_t *frame, void *data);
 
-static ngx_uint_t ngx_quic_get_padding_level(ngx_connection_t *c);
-static ngx_int_t ngx_quic_generate_ack(ngx_connection_t *c,
-    ngx_quic_send_ctx_t *ctx);
-static ssize_t ngx_quic_output_packet(ngx_connection_t *c,
-    ngx_quic_send_ctx_t *ctx, u_char *data, size_t max, size_t min);
-static ssize_t ngx_quic_send(ngx_connection_t *c, u_char *buf, size_t len);
-
-static void ngx_quic_set_packet_number(ngx_quic_header_t *pkt,
-    ngx_quic_send_ctx_t *ctx);
-static void ngx_quic_pto_handler(ngx_event_t *ev);
-static void ngx_quic_lost_handler(ngx_event_t *ev);
-static ngx_int_t ngx_quic_detect_lost(ngx_connection_t *c);
-static void ngx_quic_set_lost_timer(ngx_connection_t *c);
-static void ngx_quic_resend_frames(ngx_connection_t *c,
-    ngx_quic_send_ctx_t *ctx);
 static void ngx_quic_push_handler(ngx_event_t *ev);
 
-static void ngx_quic_congestion_ack(ngx_connection_t *c,
-    ngx_quic_frame_t *frame);
-static void ngx_quic_congestion_lost(ngx_connection_t *c,
-    ngx_quic_frame_t *frame);
-
 
 static ngx_core_module_t  ngx_quic_module_ctx = {
     ngx_string("quic"),
@@ -178,7 +112,7 @@ static SSL_QUIC_METHOD quic_method = {
 
 #if (NGX_DEBUG)
 
-static void
+void
 ngx_quic_connstate_dbg(ngx_connection_t *c)
 {
     u_char                 *p, *last;
@@ -241,10 +175,6 @@ ngx_quic_connstate_dbg(ngx_connection_t 
                    "quic %*s", p - buf, buf);
 }
 
-#else
-
-#define ngx_quic_connstate_dbg(c)
-
 #endif
 
 
@@ -468,38 +398,6 @@ ngx_quic_flush_flight(ngx_ssl_conn_t *ss
 }
 
 
-static int
-ngx_quic_send_alert(ngx_ssl_conn_t *ssl_conn, enum ssl_encryption_level_t level,
-    uint8_t alert)
-{
-    ngx_connection_t       *c;
-    ngx_quic_connection_t  *qc;
-
-    c = ngx_ssl_get_connection((ngx_ssl_conn_t *) ssl_conn);
-
-    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                   "quic ngx_quic_send_alert() lvl:%d  alert:%d",
-                   (int) level, (int) alert);
-
-    qc = ngx_quic_get_connection(c);
-    if (qc == NULL) {
-        return 1;
-    }
-
-    qc->error_level = level;
-    qc->error = NGX_QUIC_ERR_CRYPTO(alert);
-    qc->error_reason = "TLS alert";
-    qc->error_app = 0;
-    qc->error_ftype = 0;
-
-    if (ngx_quic_send_cc(c) != NGX_OK) {
-        return 0;
-    }
-
-    return 1;
-}
-
-
 static ngx_int_t
 ngx_quic_apply_transport_params(ngx_connection_t *c, ngx_quic_tp_t *ctp)
 {
@@ -722,57 +620,6 @@ ngx_quic_new_connection(ngx_connection_t
 }
 
 
-static ngx_int_t
-ngx_quic_send_stateless_reset(ngx_connection_t *c, ngx_quic_conf_t *conf,
-    ngx_quic_header_t *pkt)
-{
-    u_char    *token;
-    size_t     len, max;
-    uint16_t   rndbytes;
-    u_char     buf[NGX_QUIC_MAX_SR_PACKET];
-
-    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                   "quic handle stateless reset output");
-
-    if (pkt->len <= NGX_QUIC_MIN_PKT_LEN) {
-        return NGX_DECLINED;
-    }
-
-    if (pkt->len <= NGX_QUIC_MIN_SR_PACKET) {
-        len = pkt->len - 1;
-
-    } else {
-        max = ngx_min(NGX_QUIC_MAX_SR_PACKET, pkt->len * 3);
-
-        if (RAND_bytes((u_char *) &rndbytes, sizeof(rndbytes)) != 1) {
-            return NGX_ERROR;
-        }
-
-        len = (rndbytes % (max - NGX_QUIC_MIN_SR_PACKET + 1))
-              + NGX_QUIC_MIN_SR_PACKET;
-    }
-
-    if (RAND_bytes(buf, len - NGX_QUIC_SR_TOKEN_LEN) != 1) {
-        return NGX_ERROR;
-    }
-
-    buf[0] &= ~NGX_QUIC_PKT_LONG;
-    buf[0] |= NGX_QUIC_PKT_FIXED_BIT;
-
-    token = &buf[len - NGX_QUIC_SR_TOKEN_LEN];
-
-    if (ngx_quic_new_sr_token(c, &pkt->dcid, conf->sr_token_key, token)
-        != NGX_OK)
-    {
-        return NGX_ERROR;
-    }
-
-    (void) ngx_quic_send(c, buf, len);
-
-    return NGX_DECLINED;
-}
-
-
 ngx_int_t
 ngx_quic_new_sr_token(ngx_connection_t *c, ngx_str_t *cid, u_char *secret,
     u_char *token)
@@ -843,102 +690,7 @@ ngx_quic_process_stateless_reset(ngx_con
 }
 
 
-static ngx_int_t
-ngx_quic_negotiate_version(ngx_connection_t *c, ngx_quic_header_t *inpkt)
-{
-    size_t             len;
-    ngx_quic_header_t  pkt;
-    static u_char      buf[NGX_QUIC_MAX_UDP_PAYLOAD_SIZE];
-
-    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                   "sending version negotiation packet");
-
-    pkt.log = c->log;
-    pkt.flags = NGX_QUIC_PKT_LONG | NGX_QUIC_PKT_FIXED_BIT;
-    pkt.dcid = inpkt->scid;
-    pkt.scid = inpkt->dcid;
-
-    len = ngx_quic_create_version_negotiation(&pkt, buf);
-
-#ifdef NGX_QUIC_DEBUG_PACKETS
-    ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                   "quic vnego packet to send len:%uz %*xs", len, len, buf);
-#endif
-
-    (void) ngx_quic_send(c, buf, len);
-
-    return NGX_ERROR;
-}
-
-
-static ngx_int_t
-ngx_quic_send_retry(ngx_connection_t *c, ngx_quic_conf_t *conf,
-    ngx_quic_header_t *inpkt)
-{
-    time_t             expires;
-    ssize_t            len;
-    ngx_str_t          res, token;
-    ngx_quic_header_t  pkt;
-
-    u_char             buf[NGX_QUIC_RETRY_BUFFER_SIZE];
-    u_char             dcid[NGX_QUIC_SERVER_CID_LEN];
-
-    expires = ngx_time() + NGX_QUIC_RETRY_TOKEN_LIFETIME;
-
-    if (ngx_quic_new_token(c, conf->av_token_key, &token, &inpkt->dcid,
-                           expires, 1)
-        != NGX_OK)
-    {
-        return NGX_ERROR;
-    }
-
-    ngx_memzero(&pkt, sizeof(ngx_quic_header_t));
-    pkt.flags = NGX_QUIC_PKT_FIXED_BIT | NGX_QUIC_PKT_LONG | NGX_QUIC_PKT_RETRY;
-    pkt.version = inpkt->version;
-    pkt.log = c->log;
-
-    pkt.odcid = inpkt->dcid;
-    pkt.dcid = inpkt->scid;
-
-    /* TODO: generate routable dcid */
-    if (RAND_bytes(dcid, NGX_QUIC_SERVER_CID_LEN) != 1) {
-        return NGX_ERROR;
-    }
-
-    pkt.scid.len = NGX_QUIC_SERVER_CID_LEN;
-    pkt.scid.data = dcid;
-
-    pkt.token = token;
-
-    res.data = buf;
-
-    if (ngx_quic_encrypt(&pkt, &res) != NGX_OK) {
-        return NGX_ERROR;
-    }
-
-#ifdef NGX_QUIC_DEBUG_PACKETS
-    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                   "quic packet to send len:%uz %xV", res.len, &res);
-#endif
-
-    len = ngx_quic_send(c, res.data, res.len);
-    if (len == NGX_ERROR) {
-        return NGX_ERROR;
-    }
-
-    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                   "quic retry packet sent to %xV", &pkt.dcid);
-
-    /*
-     * quic-transport 17.2.5.1:  A server MUST NOT send more than one Retry
-     * packet in response to a single UDP datagram.
-     * NGX_DONE will stop quic_input() from processing further
-     */
-    return NGX_DONE;
-}
-
-
-static ngx_int_t
+ngx_int_t
 ngx_quic_new_token(ngx_connection_t *c, u_char *key, ngx_str_t *token,
     ngx_str_t *odcid, time_t exp, ngx_uint_t is_retry)
 {
@@ -1267,21 +1019,6 @@ ngx_quic_init_connection(ngx_connection_
 }
 
 
-static ngx_inline size_t
-ngx_quic_max_udp_payload(ngx_connection_t *c)
-{
-    /* TODO: path MTU discovery */
-
-#if (NGX_HAVE_INET6)
-    if (c->sockaddr->sa_family == AF_INET6) {
-        return NGX_QUIC_MAX_UDP_PAYLOAD_OUT6;
-    }
-#endif
-
-    return NGX_QUIC_MAX_UDP_PAYLOAD_OUT;
-}
-
-
 static void
 ngx_quic_input_handler(ngx_event_t *rev)
 {
@@ -1902,83 +1639,6 @@ ngx_quic_process_payload(ngx_connection_
 }
 
 
-static ngx_int_t
-ngx_quic_send_early_cc(ngx_connection_t *c, ngx_quic_header_t *inpkt,
-    ngx_uint_t err, const char *reason)
-{
-    ssize_t            len;
-    ngx_str_t          res;
-    ngx_quic_frame_t   frame;
-    ngx_quic_header_t  pkt;
-
-    static u_char       src[NGX_QUIC_MAX_UDP_PAYLOAD_SIZE];
-    static u_char       dst[NGX_QUIC_MAX_UDP_PAYLOAD_SIZE];
-
-    ngx_memzero(&frame, sizeof(ngx_quic_frame_t));
-    ngx_memzero(&pkt, sizeof(ngx_quic_header_t));
-
-    frame.level = inpkt->level;
-    frame.type = NGX_QUIC_FT_CONNECTION_CLOSE;
-    frame.u.close.error_code = err;
-
-    frame.u.close.reason.data = (u_char *) reason;
-    frame.u.close.reason.len = ngx_strlen(reason);
-
-    len = ngx_quic_create_frame(NULL, &frame);
-    if (len > NGX_QUIC_MAX_UDP_PAYLOAD_SIZE) {
-        return NGX_ERROR;
-    }
-
-    ngx_quic_log_frame(c->log, &frame, 1);
-
-    len = ngx_quic_create_frame(src, &frame);
-    if (len == -1) {
-        return NGX_ERROR;
-    }
-
-    pkt.keys = ngx_quic_keys_new(c->pool);
-    if (pkt.keys == NULL) {
-        return NGX_ERROR;
-    }
-
-    if (ngx_quic_keys_set_initial_secret(c->pool, pkt.keys, &inpkt->dcid,
-                                         inpkt->version)
-        != NGX_OK)
-    {
-        return NGX_ERROR;
-    }
-
-    pkt.flags = NGX_QUIC_PKT_FIXED_BIT | NGX_QUIC_PKT_LONG
-                | NGX_QUIC_PKT_INITIAL;
-
-    pkt.num_len = 1;
-    /*
-     * pkt.num = 0;
-     * pkt.trunc = 0;
-     */
-
-    pkt.version = inpkt->version;
-    pkt.log = c->log;
-    pkt.level = inpkt->level;
-    pkt.dcid = inpkt->scid;
-    pkt.scid = inpkt->dcid;
-    pkt.payload.data = src;
-    pkt.payload.len = len;
-
-    res.data = dst;
-
-    if (ngx_quic_encrypt(&pkt, &res) != NGX_OK) {
-        return NGX_ERROR;
-    }
-
-    if (ngx_quic_send(c, res.data, res.len) == NGX_ERROR) {
-        return NGX_ERROR;
-    }
-
-    return NGX_OK;
-}
-
-
 static void
 ngx_quic_discard_ctx(ngx_connection_t *c, enum ssl_encryption_level_t level)
 {
@@ -2291,736 +1951,6 @@ ngx_quic_handle_frames(ngx_connection_t 
 
 
 static ngx_int_t
-ngx_quic_ack_packet(ngx_connection_t *c, ngx_quic_header_t *pkt)
-{
-    uint64_t                base, largest, smallest, gs, ge, gap, range, pn;
-    uint64_t                prev_pending;
-    ngx_uint_t              i, nr;
-    ngx_quic_send_ctx_t    *ctx;
-    ngx_quic_ack_range_t   *r;
-    ngx_quic_connection_t  *qc;
-
-    c->log->action = "preparing ack";
-
-    qc = ngx_quic_get_connection(c);
-
-    ctx = ngx_quic_get_send_ctx(qc, pkt->level);
-
-    ngx_log_debug4(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                   "quic ngx_quic_ack_packet pn:%uL largest %L fr:%uL"
-                   " nranges:%ui", pkt->pn, (int64_t) ctx->largest_range,
-                   ctx->first_range, ctx->nranges);
-
-    prev_pending = ctx->pending_ack;
-
-    if (pkt->need_ack) {
-
-        ngx_post_event(&qc->push, &ngx_posted_events);
-
-        if (ctx->send_ack == 0) {
-            ctx->ack_delay_start = ngx_current_msec;
-        }
-
-        ctx->send_ack++;
-
-        if (ctx->pending_ack == NGX_QUIC_UNSET_PN
-            || ctx->pending_ack < pkt->pn)
-        {
-            ctx->pending_ack = pkt->pn;
-        }
-    }
-
-    base = ctx->largest_range;
-    pn = pkt->pn;
-
-    if (base == NGX_QUIC_UNSET_PN) {
-        ctx->largest_range = pn;
-        ctx->largest_received = pkt->received;
-        return NGX_OK;
-    }
-
-    if (base == pn) {
-        return NGX_OK;
-    }
-
-    largest = base;
-    smallest = largest - ctx->first_range;
-
-    if (pn > base) {
-
-        if (pn - base == 1) {
-            ctx->first_range++;
-            ctx->largest_range = pn;
-            ctx->largest_received = pkt->received;
-
-            return NGX_OK;
-
-        } else {
-            /* new gap in front of current largest */
-
-            /* no place for new range, send current range as is */
-            if (ctx->nranges == NGX_QUIC_MAX_RANGES) {
-
-                if (prev_pending != NGX_QUIC_UNSET_PN) {
-                    if (ngx_quic_send_ack(c, ctx) != NGX_OK) {
-                        return NGX_ERROR;
-                    }
-                }
-
-                if (prev_pending == ctx->pending_ack || !pkt->need_ack) {
-                    ctx->pending_ack = NGX_QUIC_UNSET_PN;
-                }
-            }
-
-            gap = pn - base - 2;
-            range = ctx->first_range;
-
-            ctx->first_range = 0;
-            ctx->largest_range = pn;
-            ctx->largest_received = pkt->received;
-
-            /* packet is out of order, force send */
-            if (pkt->need_ack) {
-                ctx->send_ack = NGX_QUIC_MAX_ACK_GAP;
-            }
-
-            i = 0;
-
-            goto insert;
-        }
-    }
-
-    /*  pn < base, perform lookup in existing ranges */
-
-    /* packet is out of order */
-    if (pkt->need_ack) {
-        ctx->send_ack = NGX_QUIC_MAX_ACK_GAP;
-    }
-
-    if (pn >= smallest && pn <= largest) {
-        return NGX_OK;
-    }
-
-#if (NGX_SUPPRESS_WARN)
-    r = NULL;
-#endif
-
-    for (i = 0; i < ctx->nranges; i++) {
-        r = &ctx->ranges[i];
-
-        ge = smallest - 1;
-        gs = ge - r->gap;
-
-        if (pn >= gs && pn <= ge) {
-
-            if (gs == ge) {
-                /* gap size is exactly one packet, now filled */
-
-                /* data moves to previous range, current is removed */
-
-                if (i == 0) {
-                    ctx->first_range += r->range + 2;
-
-                } else {
-                    ctx->ranges[i - 1].range += r->range + 2;
-                }
-
-                nr = ctx->nranges - i - 1;
-                if (nr) {
-                    ngx_memmove(&ctx->ranges[i], &ctx->ranges[i + 1],
-                                sizeof(ngx_quic_ack_range_t) * nr);
-                }
-
-                ctx->nranges--;
-
-            } else if (pn == gs) {
-                /* current gap shrinks from tail (current range grows) */
-                r->gap--;
-                r->range++;
-
-            } else if (pn == ge) {
-                /* current gap shrinks from head (previous range grows) */
-                r->gap--;
-
-                if (i == 0) {
-                    ctx->first_range++;
-
-                } else {
-                    ctx->ranges[i - 1].range++;
-                }
-
-            } else {
-                /* current gap is split into two parts */
-
-                gap = ge - pn - 1;
-                range = 0;
-
-                if (ctx->nranges == NGX_QUIC_MAX_RANGES) {
-                    if (prev_pending != NGX_QUIC_UNSET_PN) {
-                        if (ngx_quic_send_ack(c, ctx) != NGX_OK) {
-                            return NGX_ERROR;
-                        }
-                    }
-
-                    if (prev_pending == ctx->pending_ack || !pkt->need_ack) {
-                        ctx->pending_ack = NGX_QUIC_UNSET_PN;
-                    }
-                }
-
-                r->gap = pn - gs - 1;
-                goto insert;
-            }
-
-            return NGX_OK;
-        }
-
-        largest = smallest - r->gap - 2;
-        smallest = largest - r->range;
-
-        if (pn >= smallest && pn <= largest) {
-            /* this packet number is already known */
-            return NGX_OK;
-        }
-
-    }
-
-    if (pn == smallest - 1) {
-        /* extend first or last range */
-
-        if (i == 0) {
-            ctx->first_range++;
-
-        } else {
-            r->range++;
-        }
-
-        return NGX_OK;
-    }
-
-    /* nothing found, add new range at the tail  */
-
-    if (ctx->nranges == NGX_QUIC_MAX_RANGES) {
-        /* packet is too old to keep it */
-
-        if (pkt->need_ack) {
-            return ngx_quic_send_ack_range(c, ctx, pn, pn);
-        }
-
-        return NGX_OK;
-    }
-
-    gap = smallest - 2 - pn;
-    range = 0;
-
-insert:
-
-    if (ctx->nranges < NGX_QUIC_MAX_RANGES) {
-        ctx->nranges++;
-    }
-
-    ngx_memmove(&ctx->ranges[i + 1], &ctx->ranges[i],
-                sizeof(ngx_quic_ack_range_t) * (ctx->nranges - i - 1));
-
-    ctx->ranges[i].gap = gap;
-    ctx->ranges[i].range = range;
-
-    return NGX_OK;
-}
-
-
-static ngx_int_t
-ngx_quic_send_ack_range(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx,
-    uint64_t smallest, uint64_t largest)
-{
-    ngx_quic_frame_t       *frame;
-    ngx_quic_connection_t  *qc;
-
-    qc = ngx_quic_get_connection(c);
-
-    frame = ngx_quic_alloc_frame(c);
-    if (frame == NULL) {
-        return NGX_ERROR;
-    }
-
-    frame->level = ctx->level;
-    frame->type = NGX_QUIC_FT_ACK;
-    frame->u.ack.largest = largest;
-    frame->u.ack.delay = 0;
-    frame->u.ack.range_count = 0;
-    frame->u.ack.first_range = largest - smallest;
-
-    ngx_quic_queue_frame(qc, frame);
-
-    return NGX_OK;
-}
-
-
-static void
-ngx_quic_drop_ack_ranges(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx,
-    uint64_t pn)
-{
-    uint64_t               base;
-    ngx_uint_t             i, smallest, largest;
-    ngx_quic_ack_range_t  *r;
-
-    ngx_log_debug4(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                   "quic ngx_quic_drop_ack_ranges pn:%uL largest:%uL"
-                   " fr:%uL nranges:%ui", pn, ctx->largest_range,
-                   ctx->first_range, ctx->nranges);
-
-    base = ctx->largest_range;
-
-    if (base == NGX_QUIC_UNSET_PN) {
-        return;
-    }
-
-    if (ctx->pending_ack != NGX_QUIC_UNSET_PN && pn >= ctx->pending_ack) {
-        ctx->pending_ack = NGX_QUIC_UNSET_PN;
-    }
-
-    largest = base;
-    smallest = largest - ctx->first_range;
-
-    if (pn >= largest) {
-        ctx->largest_range = NGX_QUIC_UNSET_PN;
-        ctx->first_range = 0;
-        ctx->nranges = 0;
-        return;
-    }
-
-    if (pn >= smallest) {
-        ctx->first_range = largest - pn - 1;
-        ctx->nranges = 0;
-        return;
-    }
-
-    for (i = 0; i < ctx->nranges; i++) {
-        r = &ctx->ranges[i];
-
-        largest = smallest - r->gap - 2;
-        smallest = largest - r->range;
-
-        if (pn >= largest) {
-            ctx->nranges = i;
-            return;
-        }
-        if (pn >= smallest) {
-            r->range = largest - pn - 1;
-            ctx->nranges = i + 1;
-            return;
-        }
-    }
-}
-
-
-static ngx_int_t
-ngx_quic_send_ack(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx)
-{
-    size_t                  len, left;
-    uint64_t                ack_delay;
-    ngx_buf_t              *b;
-    ngx_uint_t              i;
-    ngx_chain_t            *cl, **ll;
-    ngx_quic_frame_t       *frame;
-    ngx_quic_connection_t  *qc;
-
-    qc = ngx_quic_get_connection(c);
-
-    ack_delay = ngx_current_msec - ctx->largest_received;
-    ack_delay *= 1000;
-    ack_delay >>= qc->tp.ack_delay_exponent;
-
-    frame = ngx_quic_alloc_frame(c);
-    if (frame == NULL) {
-        return NGX_ERROR;
-    }
-
-    ll = &frame->data;
-    b = NULL;
-
-    for (i = 0; i < ctx->nranges; i++) {
-        len = ngx_quic_create_ack_range(NULL, ctx->ranges[i].gap,
-                                        ctx->ranges[i].range);
-
-        left = b ? b->end - b->last : 0;
-
-        if (left < len) {
-            cl = ngx_quic_alloc_buf(c);
-            if (cl == NULL) {
-                return NGX_ERROR;
-            }
-
-            *ll = cl;
-            ll = &cl->next;
-
-            b = cl->buf;
-            left = b->end - b->last;
-
-            if (left < len) {
-                return NGX_ERROR;
-            }
-        }
-
-        b->last += ngx_quic_create_ack_range(b->last, ctx->ranges[i].gap,
-                                             ctx->ranges[i].range);
-
-        frame->u.ack.ranges_length += len;
-    }
-
-    *ll = NULL;
-
-    frame->level = ctx->level;
-    frame->type = NGX_QUIC_FT_ACK;
-    frame->u.ack.largest = ctx->largest_range;
-    frame->u.ack.delay = ack_delay;
-    frame->u.ack.range_count = ctx->nranges;
-    frame->u.ack.first_range = ctx->first_range;
-
-    ngx_quic_queue_frame(qc, frame);
-
-    return NGX_OK;
-}
-
-
-static ngx_int_t
-ngx_quic_send_cc(ngx_connection_t *c)
-{
-    ngx_quic_frame_t       *frame;
-    ngx_quic_connection_t  *qc;
-
-    qc = ngx_quic_get_connection(c);
-
-    if (qc->draining) {
-        return NGX_OK;
-    }
-
-    if (qc->closing
-        && ngx_current_msec - qc->last_cc < NGX_QUIC_CC_MIN_INTERVAL)
-    {
-        /* dot not send CC too often */
-        return NGX_OK;
-    }
-
-    frame = ngx_quic_alloc_frame(c);
-    if (frame == NULL) {
-        return NGX_ERROR;
-    }
-
-    frame->level = qc->error_level;
-    frame->type = qc->error_app ? NGX_QUIC_FT_CONNECTION_CLOSE_APP
-                                : NGX_QUIC_FT_CONNECTION_CLOSE;
-    frame->u.close.error_code = qc->error;
-    frame->u.close.frame_type = qc->error_ftype;
-
-    if (qc->error_reason) {
-        frame->u.close.reason.len = ngx_strlen(qc->error_reason);
-        frame->u.close.reason.data = (u_char *) qc->error_reason;
-    }
-
-    ngx_quic_queue_frame(qc, frame);
-
-    qc->last_cc = ngx_current_msec;
-
-    return ngx_quic_output(c);
-}
-
-
-static ngx_int_t
-ngx_quic_send_new_token(ngx_connection_t *c)
-{
-    time_t                  expires;
-    ngx_str_t               token;
-    ngx_quic_frame_t       *frame;
-    ngx_quic_connection_t  *qc;
-
-    qc = ngx_quic_get_connection(c);
-
-    if (!qc->conf->retry) {
-        return NGX_OK;
-    }
-
-    expires = ngx_time() + NGX_QUIC_NEW_TOKEN_LIFETIME;
-
-    if (ngx_quic_new_token(c, qc->conf->av_token_key, &token, NULL, expires, 0)
-        != NGX_OK)
-    {
-        return NGX_ERROR;
-    }
-
-    frame = ngx_quic_alloc_frame(c);
-    if (frame == NULL) {
-        return NGX_ERROR;
-    }
-
-    frame->level = ssl_encryption_application;
-    frame->type = NGX_QUIC_FT_NEW_TOKEN;
-    frame->u.token.length = token.len;
-    frame->u.token.data = token.data;
-
-    ngx_quic_queue_frame(qc, frame);
-
-    return NGX_OK;
-}
-
-
-static ngx_int_t
-ngx_quic_handle_ack_frame(ngx_connection_t *c, ngx_quic_header_t *pkt,
-    ngx_quic_frame_t *f)
-{
-    ssize_t                 n;
-    u_char                 *pos, *end;
-    uint64_t                min, max, gap, range;
-    ngx_msec_t              send_time;
-    ngx_uint_t              i;
-    ngx_quic_send_ctx_t    *ctx;
-    ngx_quic_ack_frame_t   *ack;
-    ngx_quic_connection_t  *qc;
-
-    qc = ngx_quic_get_connection(c);
-
-    ctx = ngx_quic_get_send_ctx(qc, pkt->level);
-
-    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                   "quic ngx_quic_handle_ack_frame level:%d", pkt->level);
-
-    ack = &f->u.ack;
-
-    /*
-     *  If any computed packet number is negative, an endpoint MUST
-     *  generate a connection error of type FRAME_ENCODING_ERROR.
-     *  (19.3.1)
-     */
-
-    if (ack->first_range > ack->largest) {
-        qc->error = NGX_QUIC_ERR_FRAME_ENCODING_ERROR;
-        ngx_log_error(NGX_LOG_INFO, c->log, 0,
-                      "quic invalid first range in ack frame");
-        return NGX_ERROR;
-    }
-
-    min = ack->largest - ack->first_range;
-    max = ack->largest;
-
-    if (ngx_quic_handle_ack_frame_range(c, ctx, min, max, &send_time)
-        != NGX_OK)
-    {
-        return NGX_ERROR;
-    }
-
-    /* 13.2.3.  Receiver Tracking of ACK Frames */
-    if (ctx->largest_ack < max || ctx->largest_ack == NGX_QUIC_UNSET_PN) {
-        ctx->largest_ack = max;
-        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                       "quic updated largest received ack:%uL", max);
-
-        /*
-         *  An endpoint generates an RTT sample on receiving an
-         *  ACK frame that meets the following two conditions:
-         *
-         *  - the largest acknowledged packet number is newly acknowledged
-         *  - at least one of the newly acknowledged packets was ack-eliciting.
-         */
-
-        if (send_time != NGX_TIMER_INFINITE) {
-            ngx_quic_rtt_sample(c, ack, pkt->level, send_time);
-        }
-    }
-
-    if (f->data) {
-        pos = f->data->buf->pos;
-        end = f->data->buf->last;
-
-    } else {
-        pos = NULL;
-        end = NULL;
-    }
-
-    for (i = 0; i < ack->range_count; i++) {
-
-        n = ngx_quic_parse_ack_range(pkt->log, pos, end, &gap, &range);
-        if (n == NGX_ERROR) {
-            return NGX_ERROR;
-        }
-        pos += n;
-
-        if (gap + 2 > min) {
-            qc->error = NGX_QUIC_ERR_FRAME_ENCODING_ERROR;
-            ngx_log_error(NGX_LOG_INFO, c->log, 0,
-                          "quic invalid range:%ui in ack frame", i);
-            return NGX_ERROR;
-        }
-
-        max = min - gap - 2;
-
-        if (range > max) {
-            qc->error = NGX_QUIC_ERR_FRAME_ENCODING_ERROR;
-            ngx_log_error(NGX_LOG_INFO, c->log, 0,
-                          "quic invalid range:%ui in ack frame", i);
-            return NGX_ERROR;
-        }
-
-        min = max - range;
-
-        if (ngx_quic_handle_ack_frame_range(c, ctx, min, max, &send_time)
-            != NGX_OK)
-        {
-            return NGX_ERROR;
-        }
-    }
-
-    return ngx_quic_detect_lost(c);
-}
-
-
-static ngx_int_t
-ngx_quic_handle_ack_frame_range(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx,
-    uint64_t min, uint64_t max, ngx_msec_t *send_time)
-{
-    ngx_uint_t              found;
-    ngx_queue_t            *q;
-    ngx_quic_frame_t       *f;
-    ngx_quic_connection_t  *qc;
-
-    qc = ngx_quic_get_connection(c);
-
-    *send_time = NGX_TIMER_INFINITE;
-    found = 0;
-
-    q = ngx_queue_last(&ctx->sent);
-
-    while (q != ngx_queue_sentinel(&ctx->sent)) {
-
-        f = ngx_queue_data(q, ngx_quic_frame_t, queue);
-        q = ngx_queue_prev(q);
-
-        if (f->pnum >= min && f->pnum <= max) {
-            ngx_quic_congestion_ack(c, f);
-
-            switch (f->type) {
-            case NGX_QUIC_FT_ACK:
-            case NGX_QUIC_FT_ACK_ECN:
-                ngx_quic_drop_ack_ranges(c, ctx, f->u.ack.largest);
-                break;
-
-            case NGX_QUIC_FT_STREAM0:
-            case NGX_QUIC_FT_STREAM1:
-            case NGX_QUIC_FT_STREAM2:
-            case NGX_QUIC_FT_STREAM3:
-            case NGX_QUIC_FT_STREAM4:
-            case NGX_QUIC_FT_STREAM5:
-            case NGX_QUIC_FT_STREAM6:
-            case NGX_QUIC_FT_STREAM7:
-                ngx_quic_handle_stream_ack(c, f);
-                break;
-            }
-
-            if (f->pnum == max) {
-                *send_time = f->last;
-            }
-
-            ngx_queue_remove(&f->queue);
-            ngx_quic_free_frame(c, f);
-            found = 1;
-        }
-    }
-
-    if (!found) {
-
-        if (max < ctx->pnum) {
-            /* duplicate ACK or ACK for non-ack-eliciting frame */
-            return NGX_OK;
-        }
-
-        ngx_log_error(NGX_LOG_INFO, c->log, 0,
-                      "quic ACK for the packet not sent");
-
-        qc->error = NGX_QUIC_ERR_PROTOCOL_VIOLATION;
-        qc->error_ftype = NGX_QUIC_FT_ACK;
-        qc->error_reason = "unknown packet number";
-
-        return NGX_ERROR;
-    }
-
-    if (!qc->push.timer_set) {
-        ngx_post_event(&qc->push, &ngx_posted_events);
-    }
-
-    qc->pto_count = 0;
-
-    return NGX_OK;
-}
-
-
-static void
-ngx_quic_rtt_sample(ngx_connection_t *c, ngx_quic_ack_frame_t *ack,
-    enum ssl_encryption_level_t level, ngx_msec_t send_time)
-{
-    ngx_msec_t              latest_rtt, ack_delay, adjusted_rtt, rttvar_sample;
-    ngx_quic_connection_t  *qc;
-
-    qc = ngx_quic_get_connection(c);
-
-    latest_rtt = ngx_current_msec - send_time;
-    qc->latest_rtt = latest_rtt;
-
-    if (qc->min_rtt == NGX_TIMER_INFINITE) {
-        qc->min_rtt = latest_rtt;
-        qc->avg_rtt = latest_rtt;
-        qc->rttvar = latest_rtt / 2;
-
-    } else {
-        qc->min_rtt = ngx_min(qc->min_rtt, latest_rtt);
-
-        ack_delay = ack->delay * (1 << qc->ctp.ack_delay_exponent) / 1000;
-
-        if (c->ssl->handshaked) {
-            ack_delay = ngx_min(ack_delay, qc->ctp.max_ack_delay);
-        }
-
-        adjusted_rtt = latest_rtt;
-
-        if (qc->min_rtt + ack_delay < latest_rtt) {
-            adjusted_rtt -= ack_delay;
-        }
-
-        qc->avg_rtt = 0.875 * qc->avg_rtt + 0.125 * adjusted_rtt;
-        rttvar_sample = ngx_abs((ngx_msec_int_t) (qc->avg_rtt - adjusted_rtt));
-        qc->rttvar = 0.75 * qc->rttvar + 0.25 * rttvar_sample;
-    }
-
-    ngx_log_debug4(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                   "quic rtt sample latest:%M min:%M avg:%M var:%M",
-                   latest_rtt, qc->min_rtt, qc->avg_rtt, qc->rttvar);
-}
-
-
-ngx_msec_t
-ngx_quic_pto(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx)
-{
-    ngx_msec_t              duration;
-    ngx_quic_connection_t  *qc;
-
-    qc = ngx_quic_get_connection(c);
-
-    /* PTO calculation: quic-recovery, Appendix 8 */
-    duration = qc->avg_rtt;
-
-    duration += ngx_max(4 * qc->rttvar, NGX_QUIC_TIME_GRANULARITY);
-    duration <<= qc->pto_count;
-
-    if (qc->congestion.in_flight == 0) { /* no in-flight packets */
-        return duration;
-    }
-
-    if (ctx->level == ssl_encryption_application && c->ssl->handshaked) {
-        duration += qc->ctp.max_ack_delay << qc->pto_count;
-    }
-
-    return duration;
-}
-
-
-static ngx_int_t
 ngx_quic_handle_crypto_frame(ngx_connection_t *c, ngx_quic_header_t *pkt,
     ngx_quic_frame_t *frame)
 {
@@ -3170,507 +2100,6 @@ ngx_quic_crypto_input(ngx_connection_t *
 }
 
 
-ngx_int_t
-ngx_quic_output(ngx_connection_t *c)
-{
-    off_t                   max;
-    size_t                  len, min, in_flight;
-    ssize_t                 n;
-    u_char                 *p;
-    ngx_uint_t              i, pad;
-    ngx_quic_send_ctx_t    *ctx;
-    ngx_quic_congestion_t  *cg;
-    ngx_quic_connection_t  *qc;
-    static u_char           dst[NGX_QUIC_MAX_UDP_PAYLOAD_SIZE];
-
-    c->log->action = "sending frames";
-
-    qc = ngx_quic_get_connection(c);
-    cg = &qc->congestion;
-
-    in_flight = cg->in_flight;
-
-    for ( ;; ) {
-        p = dst;
-
-        len = ngx_min(qc->ctp.max_udp_payload_size,
-                      NGX_QUIC_MAX_UDP_PAYLOAD_SIZE);
-
-        if (!qc->validated) {
-            max = qc->received * 3;
-            max = (c->sent >= max) ? 0 : max - c->sent;
-            len = ngx_min(len, (size_t) max);
-        }
-
-        pad = ngx_quic_get_padding_level(c);
-
-        for (i = 0; i < NGX_QUIC_SEND_CTX_LAST; i++) {
-
-            ctx = &qc->send_ctx[i];
-
-            if (ngx_quic_generate_ack(c, ctx) != NGX_OK) {
-                return NGX_ERROR;
-            }
-
-            min = (i == pad && p - dst < NGX_QUIC_MIN_INITIAL_SIZE)
-                  ? NGX_QUIC_MIN_INITIAL_SIZE - (p - dst) : 0;
-
-            n = ngx_quic_output_packet(c, ctx, p, len, min);
-            if (n == NGX_ERROR) {
-                return NGX_ERROR;
-            }
-
-            p += n;
-            len -= n;
-        }
-
-        len = p - dst;
-        if (len == 0) {
-            break;
-        }
-
-        n = ngx_quic_send(c, dst, len);
-        if (n == NGX_ERROR) {
-            return NGX_ERROR;
-        }
-    }
-
-    if (in_flight != cg->in_flight && !qc->send_timer_set && !qc->closing) {
-        qc->send_timer_set = 1;
-        ngx_add_timer(c->read, qc->tp.max_idle_timeout);
-    }
-
-    ngx_quic_set_lost_timer(c);
-
-    return NGX_OK;
-}
-
-
-static ngx_uint_t
-ngx_quic_get_padding_level(ngx_connection_t *c)
-{
-    ngx_queue_t            *q;
-    ngx_quic_frame_t       *f;
-    ngx_quic_send_ctx_t    *ctx;
-    ngx_quic_connection_t  *qc;
-
-    /*
-     * 14.1.  Initial Datagram Size
-     *
-     * Similarly, a server MUST expand the payload of all UDP datagrams
-     * carrying ack-eliciting Initial packets to at least the smallest
-     * allowed maximum datagram size of 1200 bytes
-     */
-
-    qc = ngx_quic_get_connection(c);
-    ctx = ngx_quic_get_send_ctx(qc, ssl_encryption_initial);
-
-    for (q = ngx_queue_head(&ctx->frames);
-         q != ngx_queue_sentinel(&ctx->frames);
-         q = ngx_queue_next(q))
-    {
-        f = ngx_queue_data(q, ngx_quic_frame_t, queue);
-
-        if (f->need_ack) {
-            ctx = ngx_quic_get_send_ctx(qc, ssl_encryption_handshake);
-
-            if (ngx_queue_empty(&ctx->frames)) {
-                return 0;
-            }
-
-            return 1;
-        }
-    }
-
-    return NGX_QUIC_SEND_CTX_LAST;
-}
-
-
-static ngx_int_t
-ngx_quic_generate_ack(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx)
-{
-    ngx_msec_t              delay;
-    ngx_quic_connection_t  *qc;
-
-    if (!ctx->send_ack) {
-        return NGX_OK;
-    }
-
-    if (ctx->level == ssl_encryption_application)  {
-
-        delay = ngx_current_msec - ctx->ack_delay_start;
-        qc = ngx_quic_get_connection(c);
-
-        if (ctx->send_ack < NGX_QUIC_MAX_ACK_GAP
-            && delay < qc->tp.max_ack_delay)
-        {
-            if (!qc->push.timer_set && !qc->closing) {
-                ngx_add_timer(&qc->push,
-                              qc->tp.max_ack_delay - delay);
-            }
-
-            return NGX_OK;
-        }
-    }
-
-    if (ngx_quic_send_ack(c, ctx) != NGX_OK) {
-        return NGX_ERROR;
-    }
-
-    ctx->send_ack = 0;
-
-    return NGX_OK;
-}
-
-
-static ssize_t
-ngx_quic_output_packet(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx,
-    u_char *data, size_t max, size_t min)
-{
-    size_t                  len, hlen, pad_len;
-    u_char                 *p;
-    ssize_t                 flen;
-    ngx_str_t               out, res;
-    ngx_int_t               rc;
-    ngx_uint_t              nframes;
-    ngx_msec_t              now;
-    ngx_queue_t            *q;
-    ngx_quic_frame_t       *f;
-    ngx_quic_header_t       pkt;
-    ngx_quic_congestion_t  *cg;
-    ngx_quic_connection_t  *qc;
-    static u_char           src[NGX_QUIC_MAX_UDP_PAYLOAD_SIZE];
-
-    if (ngx_queue_empty(&ctx->frames)) {
-        return 0;
-    }
-
-    ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                   "quic output %s packet max:%uz min:%uz",
-                   ngx_quic_level_name(ctx->level), max, min);
-
-    qc = ngx_quic_get_connection(c);
-    cg = &qc->congestion;
-
-    hlen = (ctx->level == ssl_encryption_application)
-           ? NGX_QUIC_MAX_SHORT_HEADER
-           : NGX_QUIC_MAX_LONG_HEADER;
-
-    hlen += EVP_GCM_TLS_TAG_LEN;
-    hlen -= NGX_QUIC_MAX_CID_LEN - qc->scid.len;
-
-    ngx_memzero(&pkt, sizeof(ngx_quic_header_t));
-
-    now = ngx_current_msec;
-    nframes = 0;
-    p = src;
-    len = 0;
-
-    for (q = ngx_queue_head(&ctx->frames);
-         q != ngx_queue_sentinel(&ctx->frames);
-         q = ngx_queue_next(q))
-    {
-        f = ngx_queue_data(q, ngx_quic_frame_t, queue);
-
-        if (!pkt.need_ack && f->need_ack && max > cg->window) {
-            max = cg->window;
-        }
-
-        if (hlen + len >= max) {
-            break;
-        }
-
-        if (hlen + len + f->len > max) {
-            rc = ngx_quic_split_frame(c, f, max - hlen - len);
-
-            if (rc == NGX_ERROR) {
-                return NGX_ERROR;
-            }
-
-            if (rc == NGX_DECLINED) {
-                break;
-            }
-        }
-
-        if (f->need_ack) {
-            pkt.need_ack = 1;
-        }
-
-        ngx_quic_log_frame(c->log, f, 1);
-
-        flen = ngx_quic_create_frame(p, f);
-        if (flen == -1) {
-            return NGX_ERROR;
-        }
-
-        len += flen;
-        p += flen;
-
-        f->pnum = ctx->pnum;
-        f->first = now;
-        f->last = now;
-        f->plen = 0;
-
-        nframes++;
-
-        if (f->flush) {
-            break;
-        }
-    }
-
-    if (nframes == 0) {
-        return 0;
-    }
-
-    out.data = src;
-    out.len = len;
-
-    pkt.keys = qc->keys;
-    pkt.flags = NGX_QUIC_PKT_FIXED_BIT;
-
-    if (ctx->level == ssl_encryption_initial) {
-        pkt.flags |= NGX_QUIC_PKT_LONG | NGX_QUIC_PKT_INITIAL;
-
-    } else if (ctx->level == ssl_encryption_handshake) {
-        pkt.flags |= NGX_QUIC_PKT_LONG | NGX_QUIC_PKT_HANDSHAKE;
-
-    } else {
-        if (qc->key_phase) {
-            pkt.flags |= NGX_QUIC_PKT_KPHASE;
-        }
-    }
-
-    ngx_quic_set_packet_number(&pkt, ctx);
-
-    pkt.version = qc->version;
-    pkt.log = c->log;
-    pkt.level = ctx->level;
-    pkt.dcid = qc->scid;
-    pkt.scid = qc->dcid;
-
-    pad_len = 4;
-
-    if (min) {
-        hlen = EVP_GCM_TLS_TAG_LEN
-               + ngx_quic_create_header(&pkt, NULL, out.len, NULL);
-
-        if (min > hlen + pad_len) {
-            pad_len = min - hlen;
-        }
-    }
-
-    if (out.len < pad_len) {
-        ngx_memset(p, NGX_QUIC_FT_PADDING, pad_len - out.len);
-        out.len = pad_len;
-    }
-
-    pkt.payload = out;
-
-    res.data = data;
-
-    ngx_log_debug6(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                   "quic packet tx %s bytes:%ui"
-                   " need_ack:%d number:%L encoded nl:%d trunc:0x%xD",
-                   ngx_quic_level_name(ctx->level), out.len, pkt.need_ack,
-                   pkt.number, pkt.num_len, pkt.trunc);
-
-    if (ngx_quic_encrypt(&pkt, &res) != NGX_OK) {
-        return NGX_ERROR;
-    }
-
-    ctx->pnum++;
-
-    if (pkt.need_ack) {
-        /* move frames into the sent queue to wait for ack */
-
-        if (!qc->closing) {
-            q = ngx_queue_head(&ctx->frames);
-            f = ngx_queue_data(q, ngx_quic_frame_t, queue);
-            f->plen = res.len;
-
-            do {
-                q = ngx_queue_head(&ctx->frames);
-                ngx_queue_remove(q);
-                ngx_queue_insert_tail(&ctx->sent, q);
-            } while (--nframes);
-        }
-
-        cg->in_flight += res.len;
-
-        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                       "quic congestion send if:%uz", cg->in_flight);
-    }
-
-    while (nframes--) {
-        q = ngx_queue_head(&ctx->frames);
-        f = ngx_queue_data(q, ngx_quic_frame_t, queue);
-
-        ngx_queue_remove(q);
-        ngx_quic_free_frame(c, f);
-    }
-
-    return res.len;
-}
-
-
-static ssize_t
-ngx_quic_send(ngx_connection_t *c, u_char *buf, size_t len)
-{
-    ngx_buf_t    b;
-    ngx_chain_t  cl, *res;
-
-    ngx_memzero(&b, sizeof(ngx_buf_t));
-
-    b.pos = b.start = buf;
-    b.last = b.end = buf + len;
-    b.last_buf = 1;
-    b.temporary = 1;
-
-    cl.buf = &b;
-    cl.next= NULL;
-
-    res = c->send_chain(c, &cl, 0);
-    if (res == NGX_CHAIN_ERROR) {
-        return NGX_ERROR;
-    }
-
-    return len;
-}
-
-
-static void
-ngx_quic_set_packet_number(ngx_quic_header_t *pkt, ngx_quic_send_ctx_t *ctx)
-{
-    uint64_t  delta;
-
-    delta = ctx->pnum - ctx->largest_ack;
-    pkt->number = ctx->pnum;
-
-    if (delta <= 0x7F) {
-        pkt->num_len = 1;
-        pkt->trunc = ctx->pnum & 0xff;
-
-    } else if (delta <= 0x7FFF) {
-        pkt->num_len = 2;
-        pkt->flags |= 0x1;
-        pkt->trunc = ctx->pnum & 0xffff;
-
-    } else if (delta <= 0x7FFFFF) {
-        pkt->num_len = 3;
-        pkt->flags |= 0x2;
-        pkt->trunc = ctx->pnum & 0xffffff;
-
-    } else {
-        pkt->num_len = 4;
-        pkt->flags |= 0x3;
-        pkt->trunc = ctx->pnum & 0xffffffff;
-    }
-}
-
-
-static void
-ngx_quic_pto_handler(ngx_event_t *ev)
-{
-    ngx_uint_t              i;
-    ngx_msec_t              now;
-    ngx_queue_t            *q, *next;
-    ngx_connection_t       *c;
-    ngx_quic_frame_t       *f;
-    ngx_quic_send_ctx_t    *ctx;
-    ngx_quic_connection_t  *qc;
-
-    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, ev->log, 0, "quic pto timer");
-
-    c = ev->data;
-    qc = ngx_quic_get_connection(c);
-    now = ngx_current_msec;
-
-    for (i = 0; i < NGX_QUIC_SEND_CTX_LAST; i++) {
-
-        ctx = &qc->send_ctx[i];
-
-        if (ngx_queue_empty(&ctx->sent)) {
-            continue;
-        }
-
-        q = ngx_queue_head(&ctx->sent);
-        f = ngx_queue_data(q, ngx_quic_frame_t, queue);
-
-        if (f->pnum <= ctx->largest_ack
-            && ctx->largest_ack != NGX_QUIC_UNSET_PN)
-        {
-            continue;
-        }
-
-        if ((ngx_msec_int_t) (f->last + ngx_quic_pto(c, ctx) - now) > 0) {
-            continue;
-        }
-
-        ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                       "quic pto %s pto_count:%ui",
-                       ngx_quic_level_name(ctx->level), qc->pto_count);
-
-        for (q = ngx_queue_head(&ctx->frames);
-             q != ngx_queue_sentinel(&ctx->frames);
-             /* void */)
-        {
-            next = ngx_queue_next(q);
-            f = ngx_queue_data(q, ngx_quic_frame_t, queue);
-
-            if (f->type == NGX_QUIC_FT_PING) {
-                ngx_queue_remove(q);
-                ngx_quic_free_frame(c, f);
-            }
-
-            q = next;
-        }
-
-        for (q = ngx_queue_head(&ctx->sent);
-             q != ngx_queue_sentinel(&ctx->sent);
-             /* void */)
-        {
-            next = ngx_queue_next(q);
-            f = ngx_queue_data(q, ngx_quic_frame_t, queue);
-
-            if (f->type == NGX_QUIC_FT_PING) {
-                ngx_quic_congestion_lost(c, f);
-                ngx_queue_remove(q);
-                ngx_quic_free_frame(c, f);
-            }
-
-            q = next;
-        }
-
-        /* enforce 2 udp datagrams */
-
-        f = ngx_quic_alloc_frame(c);
-        if (f == NULL) {
-            break;
-        }
-
-        f->level = ctx->level;
-        f->type = NGX_QUIC_FT_PING;
-        f->flush = 1;
-
-        ngx_quic_queue_frame(qc, f);
-
-        f = ngx_quic_alloc_frame(c);
-        if (f == NULL) {
-            break;
-        }
-
-        f->level = ctx->level;
-        f->type = NGX_QUIC_FT_PING;
-
-        ngx_quic_queue_frame(qc, f);
-    }
-
-    qc->pto_count++;
-
-    ngx_quic_connstate_dbg(c);
-}
-
-
 static void
 ngx_quic_push_handler(ngx_event_t *ev)
 {
@@ -3689,265 +2118,6 @@ ngx_quic_push_handler(ngx_event_t *ev)
 }
 
 
-static
-void ngx_quic_lost_handler(ngx_event_t *ev)
-{
-    ngx_connection_t  *c;
-
-    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, ev->log, 0, "quic lost timer");
-
-    c = ev->data;
-
-    if (ngx_quic_detect_lost(c) != NGX_OK) {
-        ngx_quic_close_connection(c, NGX_ERROR);
-    }
-
-    ngx_quic_connstate_dbg(c);
-}
-
-
-static ngx_int_t
-ngx_quic_detect_lost(ngx_connection_t *c)
-{
-    ngx_uint_t              i;
-    ngx_msec_t              now, wait, thr;
-    ngx_queue_t            *q;
-    ngx_quic_frame_t       *start;
-    ngx_quic_send_ctx_t    *ctx;
-    ngx_quic_connection_t  *qc;
-
-    qc = ngx_quic_get_connection(c);
-    now = ngx_current_msec;
-    thr = ngx_quic_lost_threshold(qc);
-
-    for (i = 0; i < NGX_QUIC_SEND_CTX_LAST; i++) {
-
-        ctx = &qc->send_ctx[i];
-
-        if (ctx->largest_ack == NGX_QUIC_UNSET_PN) {
-            continue;
-        }
-
-        while (!ngx_queue_empty(&ctx->sent)) {
-
-            q = ngx_queue_head(&ctx->sent);
-            start = ngx_queue_data(q, ngx_quic_frame_t, queue);
-
-            if (start->pnum > ctx->largest_ack) {
-                break;
-            }
-
-            wait = start->last + thr - now;
-
-            ngx_log_debug4(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                           "quic detect_lost pnum:%uL thr:%M wait:%i level:%d",
-                           start->pnum, thr, (ngx_int_t) wait, start->level);
-
-            if ((ngx_msec_int_t) wait > 0
-                && ctx->largest_ack - start->pnum < NGX_QUIC_PKT_THR)
-            {
-                break;
-            }
-
-            ngx_quic_resend_frames(c, ctx);
-        }
-    }
-
-    ngx_quic_set_lost_timer(c);
-
-    return NGX_OK;
-}
-
-
-static void
-ngx_quic_set_lost_timer(ngx_connection_t *c)
-{
-    ngx_uint_t              i;
-    ngx_msec_t              now;
-    ngx_queue_t            *q;
-    ngx_msec_int_t          lost, pto, w;
-    ngx_quic_frame_t       *f;
-    ngx_quic_send_ctx_t    *ctx;
-    ngx_quic_connection_t  *qc;
-
-    qc = ngx_quic_get_connection(c);
-    now = ngx_current_msec;
-
-    lost = -1;
-    pto = -1;
-
-    for (i = 0; i < NGX_QUIC_SEND_CTX_LAST; i++) {
-        ctx = &qc->send_ctx[i];
-
-        if (ngx_queue_empty(&ctx->sent)) {
-            continue;
-        }
-
-        if (ctx->largest_ack != NGX_QUIC_UNSET_PN) {
-            q = ngx_queue_head(&ctx->sent);
-            f = ngx_queue_data(q, ngx_quic_frame_t, queue);
-            w = (ngx_msec_int_t) (f->last + ngx_quic_lost_threshold(qc) - now);
-
-            if (f->pnum <= ctx->largest_ack) {
-                if (w < 0 || ctx->largest_ack - f->pnum >= NGX_QUIC_PKT_THR) {
-                    w = 0;
-                }
-
-                if (lost == -1 || w < lost) {
-                    lost = w;
-                }
-            }
-        }
-
-        q = ngx_queue_last(&ctx->sent);
-        f = ngx_queue_data(q, ngx_quic_frame_t, queue);
-        w = (ngx_msec_int_t) (f->last + ngx_quic_pto(c, ctx) - now);
-
-        if (w < 0) {
-            w = 0;
-        }
-
-        if (pto == -1 || w < pto) {
-            pto = w;
-        }
-    }
-
-    if (qc->pto.timer_set) {
-        ngx_del_timer(&qc->pto);
-    }
-
-    if (lost != -1) {
-        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                       "quic lost timer lost:%M", lost);
-
-        qc->pto.handler = ngx_quic_lost_handler;
-        ngx_add_timer(&qc->pto, lost);
-        return;
-    }
-
-    if (pto != -1) {
-        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                       "quic lost timer pto:%M", pto);
-
-        qc->pto.handler = ngx_quic_pto_handler;
-        ngx_add_timer(&qc->pto, pto);
-        return;
-    }
-
-    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, "quic lost timer unset");
-}
-
-
-static void
-ngx_quic_resend_frames(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx)
-{
-    size_t                  n;
-    ngx_buf_t              *b;
-    ngx_queue_t            *q;
-    ngx_quic_frame_t       *f, *start;
-    ngx_quic_stream_t      *sn;
-    ngx_quic_connection_t  *qc;
-
-    qc = ngx_quic_get_connection(c);
-    q = ngx_queue_head(&ctx->sent);
-    start = ngx_queue_data(q, ngx_quic_frame_t, queue);
-
-    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                   "quic resend packet pnum:%uL", start->pnum);
-
-    ngx_quic_congestion_lost(c, start);
-
-    do {
-        f = ngx_queue_data(q, ngx_quic_frame_t, queue);
-
-        if (f->pnum != start->pnum) {
-            break;
-        }
-
-        q = ngx_queue_next(q);
-
-        ngx_queue_remove(&f->queue);
-
-        switch (f->type) {
-        case NGX_QUIC_FT_ACK:
-        case NGX_QUIC_FT_ACK_ECN:
-            if (ctx->level == ssl_encryption_application) {
-                /* force generation of most recent acknowledgment */
-                ctx->send_ack = NGX_QUIC_MAX_ACK_GAP;
-            }
-
-            ngx_quic_free_frame(c, f);
-            break;
-
-        case NGX_QUIC_FT_PING:
-        case NGX_QUIC_FT_PATH_RESPONSE:
-        case NGX_QUIC_FT_CONNECTION_CLOSE:
-            ngx_quic_free_frame(c, f);
-            break;
-
-        case NGX_QUIC_FT_MAX_DATA:
-            f->u.max_data.max_data = qc->streams.recv_max_data;
-            ngx_quic_queue_frame(qc, f);
-            break;
-
-        case NGX_QUIC_FT_MAX_STREAMS:
-        case NGX_QUIC_FT_MAX_STREAMS2:
-            f->u.max_streams.limit = f->u.max_streams.bidi
-                                     ? qc->streams.client_max_streams_bidi
-                                     : qc->streams.client_max_streams_uni;
-            ngx_quic_queue_frame(qc, f);
-            break;
-
-        case NGX_QUIC_FT_MAX_STREAM_DATA:
-            sn = ngx_quic_find_stream(&qc->streams.tree,
-                                      f->u.max_stream_data.id);
-            if (sn == NULL) {
-                ngx_quic_free_frame(c, f);
-                break;
-            }
-
-            b = sn->b;
-            n = sn->fs.received + (b->pos - b->start) + (b->end - b->last);
-
-            if (f->u.max_stream_data.limit < n) {
-                f->u.max_stream_data.limit = n;
-            }
-
-            ngx_quic_queue_frame(qc, f);
-            break;
-
-        case NGX_QUIC_FT_STREAM0:
-        case NGX_QUIC_FT_STREAM1:
-        case NGX_QUIC_FT_STREAM2:
-        case NGX_QUIC_FT_STREAM3:
-        case NGX_QUIC_FT_STREAM4:
-        case NGX_QUIC_FT_STREAM5:
-        case NGX_QUIC_FT_STREAM6:
-        case NGX_QUIC_FT_STREAM7:
-            sn = ngx_quic_find_stream(&qc->streams.tree, f->u.stream.stream_id);
-
-            if (sn && sn->c->write->error) {
-                /* RESET_STREAM was sent */
-                ngx_quic_free_frame(c, f);
-                break;
-            }
-
-            /* fall through */
-
-        default:
-            ngx_queue_insert_tail(&ctx->frames, &f->queue);
-        }
-
-    } while (q != ngx_queue_sentinel(&ctx->sent));
-
-    if (qc->closing) {
-        return;
-    }
-
-    ngx_post_event(&qc->push, &ngx_posted_events);
-}
-
-
 void
 ngx_quic_shutdown_quic(ngx_connection_t *c)
 {
@@ -3981,100 +2151,6 @@ ngx_quic_shutdown_quic(ngx_connection_t 
 }
 
 
-
-static void
-ngx_quic_congestion_ack(ngx_connection_t *c, ngx_quic_frame_t *f)
-{
-    ngx_msec_t              timer;
-    ngx_quic_congestion_t  *cg;
-    ngx_quic_connection_t  *qc;
-
-    if (f->plen == 0) {
-        return;
-    }
-
-    qc = ngx_quic_get_connection(c);
-    cg = &qc->congestion;
-
-    cg->in_flight -= f->plen;
-
-    timer = f->last - cg->recovery_start;
-
-    if ((ngx_msec_int_t) timer <= 0) {
-        ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                       "quic congestion ack recovery win:%uz ss:%z if:%uz",
-                       cg->window, cg->ssthresh, cg->in_flight);
-
-        return;
-    }
-
-    if (cg->window < cg->ssthresh) {
-        cg->window += f->plen;
-
-        ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                       "quic congestion slow start win:%uz ss:%z if:%uz",
-                       cg->window, cg->ssthresh, cg->in_flight);
-
-    } else {
-        cg->window += qc->tp.max_udp_payload_size * f->plen / cg->window;
-
-        ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                       "quic congestion avoidance win:%uz ss:%z if:%uz",
-                       cg->window, cg->ssthresh, cg->in_flight);
-    }
-
-    /* prevent recovery_start from wrapping */
-
-    timer = cg->recovery_start - ngx_current_msec + qc->tp.max_idle_timeout * 2;
-
-    if ((ngx_msec_int_t) timer < 0) {
-        cg->recovery_start = ngx_current_msec - qc->tp.max_idle_timeout * 2;
-    }
-}
-
-
-static void
-ngx_quic_congestion_lost(ngx_connection_t *c, ngx_quic_frame_t *f)
-{
-    ngx_msec_t              timer;
-    ngx_quic_congestion_t  *cg;
-    ngx_quic_connection_t  *qc;
-
-    if (f->plen == 0) {
-        return;
-    }
-
-    qc = ngx_quic_get_connection(c);
-    cg = &qc->congestion;
-
-    cg->in_flight -= f->plen;
-    f->plen = 0;
-
-    timer = f->last - cg->recovery_start;
-
-    if ((ngx_msec_int_t) timer <= 0) {
-        ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                       "quic congestion lost recovery win:%uz ss:%z if:%uz",
-                       cg->window, cg->ssthresh, cg->in_flight);
-
-        return;
-    }
-
-    cg->recovery_start = ngx_current_msec;
-    cg->window /= 2;
-
-    if (cg->window < qc->tp.max_udp_payload_size * 2) {
-        cg->window = qc->tp.max_udp_payload_size * 2;
-    }
-
-    cg->ssthresh = cg->window;
-
-    ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                   "quic congestion lost win:%uz ss:%z if:%uz",
-                   cg->window, cg->ssthresh, cg->in_flight);
-}
-
-
 uint32_t
 ngx_quic_version(ngx_connection_t *c)
 {
new file mode 100644
--- /dev/null
+++ b/src/event/quic/ngx_event_quic_ack.c
@@ -0,0 +1,1081 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_event.h>
+#include <ngx_event_quic_connection.h>
+
+
+#define NGX_QUIC_MAX_ACK_GAP                 2
+
+/* quic-recovery, section 6.1.1, Packet Threshold */
+#define NGX_QUIC_PKT_THR                     3 /* packets */
+/* quic-recovery, section 6.1.2, Time Threshold */
+#define NGX_QUIC_TIME_THR                    1.125
+#define NGX_QUIC_TIME_GRANULARITY            1 /* ms */
+
+#define ngx_quic_lost_threshold(qc)                                           \
+    ngx_max(NGX_QUIC_TIME_THR * ngx_max((qc)->latest_rtt, (qc)->avg_rtt),     \
+            NGX_QUIC_TIME_GRANULARITY)
+
+
+static void ngx_quic_rtt_sample(ngx_connection_t *c, ngx_quic_ack_frame_t *ack,
+    enum ssl_encryption_level_t level, ngx_msec_t send_time);
+static ngx_int_t ngx_quic_handle_ack_frame_range(ngx_connection_t *c,
+    ngx_quic_send_ctx_t *ctx, uint64_t min, uint64_t max,
+    ngx_msec_t *send_time);
+static void ngx_quic_drop_ack_ranges(ngx_connection_t *c,
+    ngx_quic_send_ctx_t *ctx, uint64_t pn);
+static ngx_int_t ngx_quic_detect_lost(ngx_connection_t *c);
+static void ngx_quic_congestion_lost(ngx_connection_t *c,
+    ngx_quic_frame_t *frame);
+static void ngx_quic_lost_handler(ngx_event_t *ev);
+
+
+ngx_int_t
+ngx_quic_handle_ack_frame(ngx_connection_t *c, ngx_quic_header_t *pkt,
+    ngx_quic_frame_t *f)
+{
+    ssize_t                 n;
+    u_char                 *pos, *end;
+    uint64_t                min, max, gap, range;
+    ngx_msec_t              send_time;
+    ngx_uint_t              i;
+    ngx_quic_send_ctx_t    *ctx;
+    ngx_quic_ack_frame_t   *ack;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    ctx = ngx_quic_get_send_ctx(qc, pkt->level);
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic ngx_quic_handle_ack_frame level:%d", pkt->level);
+
+    ack = &f->u.ack;
+
+    /*
+     *  If any computed packet number is negative, an endpoint MUST
+     *  generate a connection error of type FRAME_ENCODING_ERROR.
+     *  (19.3.1)
+     */
+
+    if (ack->first_range > ack->largest) {
+        qc->error = NGX_QUIC_ERR_FRAME_ENCODING_ERROR;
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "quic invalid first range in ack frame");
+        return NGX_ERROR;
+    }
+
+    min = ack->largest - ack->first_range;
+    max = ack->largest;
+
+    if (ngx_quic_handle_ack_frame_range(c, ctx, min, max, &send_time)
+        != NGX_OK)
+    {
+        return NGX_ERROR;
+    }
+
+    /* 13.2.3.  Receiver Tracking of ACK Frames */
+    if (ctx->largest_ack < max || ctx->largest_ack == NGX_QUIC_UNSET_PN) {
+        ctx->largest_ack = max;
+        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic updated largest received ack:%uL", max);
+
+        /*
+         *  An endpoint generates an RTT sample on receiving an
+         *  ACK frame that meets the following two conditions:
+         *
+         *  - the largest acknowledged packet number is newly acknowledged
+         *  - at least one of the newly acknowledged packets was ack-eliciting.
+         */
+
+        if (send_time != NGX_TIMER_INFINITE) {
+            ngx_quic_rtt_sample(c, ack, pkt->level, send_time);
+        }
+    }
+
+    if (f->data) {
+        pos = f->data->buf->pos;
+        end = f->data->buf->last;
+
+    } else {
+        pos = NULL;
+        end = NULL;
+    }
+
+    for (i = 0; i < ack->range_count; i++) {
+
+        n = ngx_quic_parse_ack_range(pkt->log, pos, end, &gap, &range);
+        if (n == NGX_ERROR) {
+            return NGX_ERROR;
+        }
+        pos += n;
+
+        if (gap + 2 > min) {
+            qc->error = NGX_QUIC_ERR_FRAME_ENCODING_ERROR;
+            ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                          "quic invalid range:%ui in ack frame", i);
+            return NGX_ERROR;
+        }
+
+        max = min - gap - 2;
+
+        if (range > max) {
+            qc->error = NGX_QUIC_ERR_FRAME_ENCODING_ERROR;
+            ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                          "quic invalid range:%ui in ack frame", i);
+            return NGX_ERROR;
+        }
+
+        min = max - range;
+
+        if (ngx_quic_handle_ack_frame_range(c, ctx, min, max, &send_time)
+            != NGX_OK)
+        {
+            return NGX_ERROR;
+        }
+    }
+
+    return ngx_quic_detect_lost(c);
+}
+
+
+static void
+ngx_quic_rtt_sample(ngx_connection_t *c, ngx_quic_ack_frame_t *ack,
+    enum ssl_encryption_level_t level, ngx_msec_t send_time)
+{
+    ngx_msec_t              latest_rtt, ack_delay, adjusted_rtt, rttvar_sample;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    latest_rtt = ngx_current_msec - send_time;
+    qc->latest_rtt = latest_rtt;
+
+    if (qc->min_rtt == NGX_TIMER_INFINITE) {
+        qc->min_rtt = latest_rtt;
+        qc->avg_rtt = latest_rtt;
+        qc->rttvar = latest_rtt / 2;
+
+    } else {
+        qc->min_rtt = ngx_min(qc->min_rtt, latest_rtt);
+
+        ack_delay = ack->delay * (1 << qc->ctp.ack_delay_exponent) / 1000;
+
+        if (c->ssl->handshaked) {
+            ack_delay = ngx_min(ack_delay, qc->ctp.max_ack_delay);
+        }
+
+        adjusted_rtt = latest_rtt;
+
+        if (qc->min_rtt + ack_delay < latest_rtt) {
+            adjusted_rtt -= ack_delay;
+        }
+
+        qc->avg_rtt = 0.875 * qc->avg_rtt + 0.125 * adjusted_rtt;
+        rttvar_sample = ngx_abs((ngx_msec_int_t) (qc->avg_rtt - adjusted_rtt));
+        qc->rttvar = 0.75 * qc->rttvar + 0.25 * rttvar_sample;
+    }
+
+    ngx_log_debug4(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic rtt sample latest:%M min:%M avg:%M var:%M",
+                   latest_rtt, qc->min_rtt, qc->avg_rtt, qc->rttvar);
+}
+
+
+static ngx_int_t
+ngx_quic_handle_ack_frame_range(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx,
+    uint64_t min, uint64_t max, ngx_msec_t *send_time)
+{
+    ngx_uint_t              found;
+    ngx_queue_t            *q;
+    ngx_quic_frame_t       *f;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    *send_time = NGX_TIMER_INFINITE;
+    found = 0;
+
+    q = ngx_queue_last(&ctx->sent);
+
+    while (q != ngx_queue_sentinel(&ctx->sent)) {
+
+        f = ngx_queue_data(q, ngx_quic_frame_t, queue);
+        q = ngx_queue_prev(q);
+
+        if (f->pnum >= min && f->pnum <= max) {
+            ngx_quic_congestion_ack(c, f);
+
+            switch (f->type) {
+            case NGX_QUIC_FT_ACK:
+            case NGX_QUIC_FT_ACK_ECN:
+                ngx_quic_drop_ack_ranges(c, ctx, f->u.ack.largest);
+                break;
+
+            case NGX_QUIC_FT_STREAM0:
+            case NGX_QUIC_FT_STREAM1:
+            case NGX_QUIC_FT_STREAM2:
+            case NGX_QUIC_FT_STREAM3:
+            case NGX_QUIC_FT_STREAM4:
+            case NGX_QUIC_FT_STREAM5:
+            case NGX_QUIC_FT_STREAM6:
+            case NGX_QUIC_FT_STREAM7:
+                ngx_quic_handle_stream_ack(c, f);
+                break;
+            }
+
+            if (f->pnum == max) {
+                *send_time = f->last;
+            }
+
+            ngx_queue_remove(&f->queue);
+            ngx_quic_free_frame(c, f);
+            found = 1;
+        }
+    }
+
+    if (!found) {
+
+        if (max < ctx->pnum) {
+            /* duplicate ACK or ACK for non-ack-eliciting frame */
+            return NGX_OK;
+        }
+
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "quic ACK for the packet not sent");
+
+        qc->error = NGX_QUIC_ERR_PROTOCOL_VIOLATION;
+        qc->error_ftype = NGX_QUIC_FT_ACK;
+        qc->error_reason = "unknown packet number";
+
+        return NGX_ERROR;
+    }
+
+    if (!qc->push.timer_set) {
+        ngx_post_event(&qc->push, &ngx_posted_events);
+    }
+
+    qc->pto_count = 0;
+
+    return NGX_OK;
+}
+
+
+void
+ngx_quic_congestion_ack(ngx_connection_t *c, ngx_quic_frame_t *f)
+{
+    ngx_msec_t              timer;
+    ngx_quic_congestion_t  *cg;
+    ngx_quic_connection_t  *qc;
+
+    if (f->plen == 0) {
+        return;
+    }
+
+    qc = ngx_quic_get_connection(c);
+    cg = &qc->congestion;
+
+    cg->in_flight -= f->plen;
+
+    timer = f->last - cg->recovery_start;
+
+    if ((ngx_msec_int_t) timer <= 0) {
+        ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic congestion ack recovery win:%uz ss:%z if:%uz",
+                       cg->window, cg->ssthresh, cg->in_flight);
+
+        return;
+    }
+
+    if (cg->window < cg->ssthresh) {
+        cg->window += f->plen;
+
+        ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic congestion slow start win:%uz ss:%z if:%uz",
+                       cg->window, cg->ssthresh, cg->in_flight);
+
+    } else {
+        cg->window += qc->tp.max_udp_payload_size * f->plen / cg->window;
+
+        ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic congestion avoidance win:%uz ss:%z if:%uz",
+                       cg->window, cg->ssthresh, cg->in_flight);
+    }
+
+    /* prevent recovery_start from wrapping */
+
+    timer = cg->recovery_start - ngx_current_msec + qc->tp.max_idle_timeout * 2;
+
+    if ((ngx_msec_int_t) timer < 0) {
+        cg->recovery_start = ngx_current_msec - qc->tp.max_idle_timeout * 2;
+    }
+}
+
+
+static void
+ngx_quic_drop_ack_ranges(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx,
+    uint64_t pn)
+{
+    uint64_t               base;
+    ngx_uint_t             i, smallest, largest;
+    ngx_quic_ack_range_t  *r;
+
+    ngx_log_debug4(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic ngx_quic_drop_ack_ranges pn:%uL largest:%uL"
+                   " fr:%uL nranges:%ui", pn, ctx->largest_range,
+                   ctx->first_range, ctx->nranges);
+
+    base = ctx->largest_range;
+
+    if (base == NGX_QUIC_UNSET_PN) {
+        return;
+    }
+
+    if (ctx->pending_ack != NGX_QUIC_UNSET_PN && pn >= ctx->pending_ack) {
+        ctx->pending_ack = NGX_QUIC_UNSET_PN;
+    }
+
+    largest = base;
+    smallest = largest - ctx->first_range;
+
+    if (pn >= largest) {
+        ctx->largest_range = NGX_QUIC_UNSET_PN;
+        ctx->first_range = 0;
+        ctx->nranges = 0;
+        return;
+    }
+
+    if (pn >= smallest) {
+        ctx->first_range = largest - pn - 1;
+        ctx->nranges = 0;
+        return;
+    }
+
+    for (i = 0; i < ctx->nranges; i++) {
+        r = &ctx->ranges[i];
+
+        largest = smallest - r->gap - 2;
+        smallest = largest - r->range;
+
+        if (pn >= largest) {
+            ctx->nranges = i;
+            return;
+        }
+        if (pn >= smallest) {
+            r->range = largest - pn - 1;
+            ctx->nranges = i + 1;
+            return;
+        }
+    }
+}
+
+
+static ngx_int_t
+ngx_quic_detect_lost(ngx_connection_t *c)
+{
+    ngx_uint_t              i;
+    ngx_msec_t              now, wait, thr;
+    ngx_queue_t            *q;
+    ngx_quic_frame_t       *start;
+    ngx_quic_send_ctx_t    *ctx;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+    now = ngx_current_msec;
+    thr = ngx_quic_lost_threshold(qc);
+
+    for (i = 0; i < NGX_QUIC_SEND_CTX_LAST; i++) {
+
+        ctx = &qc->send_ctx[i];
+
+        if (ctx->largest_ack == NGX_QUIC_UNSET_PN) {
+            continue;
+        }
+
+        while (!ngx_queue_empty(&ctx->sent)) {
+
+            q = ngx_queue_head(&ctx->sent);
+            start = ngx_queue_data(q, ngx_quic_frame_t, queue);
+
+            if (start->pnum > ctx->largest_ack) {
+                break;
+            }
+
+            wait = start->last + thr - now;
+
+            ngx_log_debug4(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                           "quic detect_lost pnum:%uL thr:%M wait:%i level:%d",
+                           start->pnum, thr, (ngx_int_t) wait, start->level);
+
+            if ((ngx_msec_int_t) wait > 0
+                && ctx->largest_ack - start->pnum < NGX_QUIC_PKT_THR)
+            {
+                break;
+            }
+
+            ngx_quic_resend_frames(c, ctx);
+        }
+    }
+
+    ngx_quic_set_lost_timer(c);
+
+    return NGX_OK;
+}
+
+
+void
+ngx_quic_resend_frames(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx)
+{
+    size_t                  n;
+    ngx_buf_t              *b;
+    ngx_queue_t            *q;
+    ngx_quic_frame_t       *f, *start;
+    ngx_quic_stream_t      *sn;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+    q = ngx_queue_head(&ctx->sent);
+    start = ngx_queue_data(q, ngx_quic_frame_t, queue);
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic resend packet pnum:%uL", start->pnum);
+
+    ngx_quic_congestion_lost(c, start);
+
+    do {
+        f = ngx_queue_data(q, ngx_quic_frame_t, queue);
+
+        if (f->pnum != start->pnum) {
+            break;
+        }
+
+        q = ngx_queue_next(q);
+
+        ngx_queue_remove(&f->queue);
+
+        switch (f->type) {
+        case NGX_QUIC_FT_ACK:
+        case NGX_QUIC_FT_ACK_ECN:
+            if (ctx->level == ssl_encryption_application) {
+                /* force generation of most recent acknowledgment */
+                ctx->send_ack = NGX_QUIC_MAX_ACK_GAP;
+            }
+
+            ngx_quic_free_frame(c, f);
+            break;
+
+        case NGX_QUIC_FT_PING:
+        case NGX_QUIC_FT_PATH_RESPONSE:
+        case NGX_QUIC_FT_CONNECTION_CLOSE:
+            ngx_quic_free_frame(c, f);
+            break;
+
+        case NGX_QUIC_FT_MAX_DATA:
+            f->u.max_data.max_data = qc->streams.recv_max_data;
+            ngx_quic_queue_frame(qc, f);
+            break;
+
+        case NGX_QUIC_FT_MAX_STREAMS:
+        case NGX_QUIC_FT_MAX_STREAMS2:
+            f->u.max_streams.limit = f->u.max_streams.bidi
+                                     ? qc->streams.client_max_streams_bidi
+                                     : qc->streams.client_max_streams_uni;
+            ngx_quic_queue_frame(qc, f);
+            break;
+
+        case NGX_QUIC_FT_MAX_STREAM_DATA:
+            sn = ngx_quic_find_stream(&qc->streams.tree,
+                                      f->u.max_stream_data.id);
+            if (sn == NULL) {
+                ngx_quic_free_frame(c, f);
+                break;
+            }
+
+            b = sn->b;
+            n = sn->fs.received + (b->pos - b->start) + (b->end - b->last);
+
+            if (f->u.max_stream_data.limit < n) {
+                f->u.max_stream_data.limit = n;
+            }
+
+            ngx_quic_queue_frame(qc, f);
+            break;
+
+        case NGX_QUIC_FT_STREAM0:
+        case NGX_QUIC_FT_STREAM1:
+        case NGX_QUIC_FT_STREAM2:
+        case NGX_QUIC_FT_STREAM3:
+        case NGX_QUIC_FT_STREAM4:
+        case NGX_QUIC_FT_STREAM5:
+        case NGX_QUIC_FT_STREAM6:
+        case NGX_QUIC_FT_STREAM7:
+            sn = ngx_quic_find_stream(&qc->streams.tree, f->u.stream.stream_id);
+
+            if (sn && sn->c->write->error) {
+                /* RESET_STREAM was sent */
+                ngx_quic_free_frame(c, f);
+                break;
+            }
+
+            /* fall through */
+
+        default:
+            ngx_queue_insert_tail(&ctx->frames, &f->queue);
+        }
+
+    } while (q != ngx_queue_sentinel(&ctx->sent));
+
+    if (qc->closing) {
+        return;
+    }
+
+    ngx_post_event(&qc->push, &ngx_posted_events);
+}
+
+
+static void
+ngx_quic_congestion_lost(ngx_connection_t *c, ngx_quic_frame_t *f)
+{
+    ngx_msec_t              timer;
+    ngx_quic_congestion_t  *cg;
+    ngx_quic_connection_t  *qc;
+
+    if (f->plen == 0) {
+        return;
+    }
+
+    qc = ngx_quic_get_connection(c);
+    cg = &qc->congestion;
+
+    cg->in_flight -= f->plen;
+    f->plen = 0;
+
+    timer = f->last - cg->recovery_start;
+
+    if ((ngx_msec_int_t) timer <= 0) {
+        ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic congestion lost recovery win:%uz ss:%z if:%uz",
+                       cg->window, cg->ssthresh, cg->in_flight);
+
+        return;
+    }
+
+    cg->recovery_start = ngx_current_msec;
+    cg->window /= 2;
+
+    if (cg->window < qc->tp.max_udp_payload_size * 2) {
+        cg->window = qc->tp.max_udp_payload_size * 2;
+    }
+
+    cg->ssthresh = cg->window;
+
+    ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic congestion lost win:%uz ss:%z if:%uz",
+                   cg->window, cg->ssthresh, cg->in_flight);
+}
+
+
+void
+ngx_quic_set_lost_timer(ngx_connection_t *c)
+{
+    ngx_uint_t              i;
+    ngx_msec_t              now;
+    ngx_queue_t            *q;
+    ngx_msec_int_t          lost, pto, w;
+    ngx_quic_frame_t       *f;
+    ngx_quic_send_ctx_t    *ctx;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+    now = ngx_current_msec;
+
+    lost = -1;
+    pto = -1;
+
+    for (i = 0; i < NGX_QUIC_SEND_CTX_LAST; i++) {
+        ctx = &qc->send_ctx[i];
+
+        if (ngx_queue_empty(&ctx->sent)) {
+            continue;
+        }
+
+        if (ctx->largest_ack != NGX_QUIC_UNSET_PN) {
+            q = ngx_queue_head(&ctx->sent);
+            f = ngx_queue_data(q, ngx_quic_frame_t, queue);
+            w = (ngx_msec_int_t) (f->last + ngx_quic_lost_threshold(qc) - now);
+
+            if (f->pnum <= ctx->largest_ack) {
+                if (w < 0 || ctx->largest_ack - f->pnum >= NGX_QUIC_PKT_THR) {
+                    w = 0;
+                }
+
+                if (lost == -1 || w < lost) {
+                    lost = w;
+                }
+            }
+        }
+
+        q = ngx_queue_last(&ctx->sent);
+        f = ngx_queue_data(q, ngx_quic_frame_t, queue);
+        w = (ngx_msec_int_t) (f->last + ngx_quic_pto(c, ctx) - now);
+
+        if (w < 0) {
+            w = 0;
+        }
+
+        if (pto == -1 || w < pto) {
+            pto = w;
+        }
+    }
+
+    if (qc->pto.timer_set) {
+        ngx_del_timer(&qc->pto);
+    }
+
+    if (lost != -1) {
+        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic lost timer lost:%M", lost);
+
+        qc->pto.handler = ngx_quic_lost_handler;
+        ngx_add_timer(&qc->pto, lost);
+        return;
+    }
+
+    if (pto != -1) {
+        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic lost timer pto:%M", pto);
+
+        qc->pto.handler = ngx_quic_pto_handler;
+        ngx_add_timer(&qc->pto, pto);
+        return;
+    }
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, "quic lost timer unset");
+}
+
+
+ngx_msec_t
+ngx_quic_pto(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx)
+{
+    ngx_msec_t              duration;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    /* PTO calculation: quic-recovery, Appendix 8 */
+    duration = qc->avg_rtt;
+
+    duration += ngx_max(4 * qc->rttvar, NGX_QUIC_TIME_GRANULARITY);
+    duration <<= qc->pto_count;
+
+    if (qc->congestion.in_flight == 0) { /* no in-flight packets */
+        return duration;
+    }
+
+    if (ctx->level == ssl_encryption_application && c->ssl->handshaked) {
+        duration += qc->ctp.max_ack_delay << qc->pto_count;
+    }
+
+    return duration;
+}
+
+
+static
+void ngx_quic_lost_handler(ngx_event_t *ev)
+{
+    ngx_connection_t  *c;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, ev->log, 0, "quic lost timer");
+
+    c = ev->data;
+
+    if (ngx_quic_detect_lost(c) != NGX_OK) {
+        ngx_quic_close_connection(c, NGX_ERROR);
+    }
+
+    ngx_quic_connstate_dbg(c);
+}
+
+
+void
+ngx_quic_pto_handler(ngx_event_t *ev)
+{
+    ngx_uint_t              i;
+    ngx_msec_t              now;
+    ngx_queue_t            *q, *next;
+    ngx_connection_t       *c;
+    ngx_quic_frame_t       *f;
+    ngx_quic_send_ctx_t    *ctx;
+    ngx_quic_connection_t  *qc;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, ev->log, 0, "quic pto timer");
+
+    c = ev->data;
+    qc = ngx_quic_get_connection(c);
+    now = ngx_current_msec;
+
+    for (i = 0; i < NGX_QUIC_SEND_CTX_LAST; i++) {
+
+        ctx = &qc->send_ctx[i];
+
+        if (ngx_queue_empty(&ctx->sent)) {
+            continue;
+        }
+
+        q = ngx_queue_head(&ctx->sent);
+        f = ngx_queue_data(q, ngx_quic_frame_t, queue);
+
+        if (f->pnum <= ctx->largest_ack
+            && ctx->largest_ack != NGX_QUIC_UNSET_PN)
+        {
+            continue;
+        }
+
+        if ((ngx_msec_int_t) (f->last + ngx_quic_pto(c, ctx) - now) > 0) {
+            continue;
+        }
+
+        ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic pto %s pto_count:%ui",
+                       ngx_quic_level_name(ctx->level), qc->pto_count);
+
+        for (q = ngx_queue_head(&ctx->frames);
+             q != ngx_queue_sentinel(&ctx->frames);
+             /* void */)
+        {
+            next = ngx_queue_next(q);
+            f = ngx_queue_data(q, ngx_quic_frame_t, queue);
+
+            if (f->type == NGX_QUIC_FT_PING) {
+                ngx_queue_remove(q);
+                ngx_quic_free_frame(c, f);
+            }
+
+            q = next;
+        }
+
+        for (q = ngx_queue_head(&ctx->sent);
+             q != ngx_queue_sentinel(&ctx->sent);
+             /* void */)
+        {
+            next = ngx_queue_next(q);
+            f = ngx_queue_data(q, ngx_quic_frame_t, queue);
+
+            if (f->type == NGX_QUIC_FT_PING) {
+                ngx_quic_congestion_lost(c, f);
+                ngx_queue_remove(q);
+                ngx_quic_free_frame(c, f);
+            }
+
+            q = next;
+        }
+
+        /* enforce 2 udp datagrams */
+
+        f = ngx_quic_alloc_frame(c);
+        if (f == NULL) {
+            break;
+        }
+
+        f->level = ctx->level;
+        f->type = NGX_QUIC_FT_PING;
+        f->flush = 1;
+
+        ngx_quic_queue_frame(qc, f);
+
+        f = ngx_quic_alloc_frame(c);
+        if (f == NULL) {
+            break;
+        }
+
+        f->level = ctx->level;
+        f->type = NGX_QUIC_FT_PING;
+
+        ngx_quic_queue_frame(qc, f);
+    }
+
+    qc->pto_count++;
+
+    ngx_quic_connstate_dbg(c);
+}
+
+
+ngx_int_t
+ngx_quic_ack_packet(ngx_connection_t *c, ngx_quic_header_t *pkt)
+{
+    uint64_t                base, largest, smallest, gs, ge, gap, range, pn;
+    uint64_t                prev_pending;
+    ngx_uint_t              i, nr;
+    ngx_quic_send_ctx_t    *ctx;
+    ngx_quic_ack_range_t   *r;
+    ngx_quic_connection_t  *qc;
+
+    c->log->action = "preparing ack";
+
+    qc = ngx_quic_get_connection(c);
+
+    ctx = ngx_quic_get_send_ctx(qc, pkt->level);
+
+    ngx_log_debug4(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic ngx_quic_ack_packet pn:%uL largest %L fr:%uL"
+                   " nranges:%ui", pkt->pn, (int64_t) ctx->largest_range,
+                   ctx->first_range, ctx->nranges);
+
+    prev_pending = ctx->pending_ack;
+
+    if (pkt->need_ack) {
+
+        ngx_post_event(&qc->push, &ngx_posted_events);
+
+        if (ctx->send_ack == 0) {
+            ctx->ack_delay_start = ngx_current_msec;
+        }
+
+        ctx->send_ack++;
+
+        if (ctx->pending_ack == NGX_QUIC_UNSET_PN
+            || ctx->pending_ack < pkt->pn)
+        {
+            ctx->pending_ack = pkt->pn;
+        }
+    }
+
+    base = ctx->largest_range;
+    pn = pkt->pn;
+
+    if (base == NGX_QUIC_UNSET_PN) {
+        ctx->largest_range = pn;
+        ctx->largest_received = pkt->received;
+        return NGX_OK;
+    }
+
+    if (base == pn) {
+        return NGX_OK;
+    }
+
+    largest = base;
+    smallest = largest - ctx->first_range;
+
+    if (pn > base) {
+
+        if (pn - base == 1) {
+            ctx->first_range++;
+            ctx->largest_range = pn;
+            ctx->largest_received = pkt->received;
+
+            return NGX_OK;
+
+        } else {
+            /* new gap in front of current largest */
+
+            /* no place for new range, send current range as is */
+            if (ctx->nranges == NGX_QUIC_MAX_RANGES) {
+
+                if (prev_pending != NGX_QUIC_UNSET_PN) {
+                    if (ngx_quic_send_ack(c, ctx) != NGX_OK) {
+                        return NGX_ERROR;
+                    }
+                }
+
+                if (prev_pending == ctx->pending_ack || !pkt->need_ack) {
+                    ctx->pending_ack = NGX_QUIC_UNSET_PN;
+                }
+            }
+
+            gap = pn - base - 2;
+            range = ctx->first_range;
+
+            ctx->first_range = 0;
+            ctx->largest_range = pn;
+            ctx->largest_received = pkt->received;
+
+            /* packet is out of order, force send */
+            if (pkt->need_ack) {
+                ctx->send_ack = NGX_QUIC_MAX_ACK_GAP;
+            }
+
+            i = 0;
+
+            goto insert;
+        }
+    }
+
+    /*  pn < base, perform lookup in existing ranges */
+
+    /* packet is out of order */
+    if (pkt->need_ack) {
+        ctx->send_ack = NGX_QUIC_MAX_ACK_GAP;
+    }
+
+    if (pn >= smallest && pn <= largest) {
+        return NGX_OK;
+    }
+
+#if (NGX_SUPPRESS_WARN)
+    r = NULL;
+#endif
+
+    for (i = 0; i < ctx->nranges; i++) {
+        r = &ctx->ranges[i];
+
+        ge = smallest - 1;
+        gs = ge - r->gap;
+
+        if (pn >= gs && pn <= ge) {
+
+            if (gs == ge) {
+                /* gap size is exactly one packet, now filled */
+
+                /* data moves to previous range, current is removed */
+
+                if (i == 0) {
+                    ctx->first_range += r->range + 2;
+
+                } else {
+                    ctx->ranges[i - 1].range += r->range + 2;
+                }
+
+                nr = ctx->nranges - i - 1;
+                if (nr) {
+                    ngx_memmove(&ctx->ranges[i], &ctx->ranges[i + 1],
+                                sizeof(ngx_quic_ack_range_t) * nr);
+                }
+
+                ctx->nranges--;
+
+            } else if (pn == gs) {
+                /* current gap shrinks from tail (current range grows) */
+                r->gap--;
+                r->range++;
+
+            } else if (pn == ge) {
+                /* current gap shrinks from head (previous range grows) */
+                r->gap--;
+
+                if (i == 0) {
+                    ctx->first_range++;
+
+                } else {
+                    ctx->ranges[i - 1].range++;
+                }
+
+            } else {
+                /* current gap is split into two parts */
+
+                gap = ge - pn - 1;
+                range = 0;
+
+                if (ctx->nranges == NGX_QUIC_MAX_RANGES) {
+                    if (prev_pending != NGX_QUIC_UNSET_PN) {
+                        if (ngx_quic_send_ack(c, ctx) != NGX_OK) {
+                            return NGX_ERROR;
+                        }
+                    }
+
+                    if (prev_pending == ctx->pending_ack || !pkt->need_ack) {
+                        ctx->pending_ack = NGX_QUIC_UNSET_PN;
+                    }
+                }
+
+                r->gap = pn - gs - 1;
+                goto insert;
+            }
+
+            return NGX_OK;
+        }
+
+        largest = smallest - r->gap - 2;
+        smallest = largest - r->range;
+
+        if (pn >= smallest && pn <= largest) {
+            /* this packet number is already known */
+            return NGX_OK;
+        }
+
+    }
+
+    if (pn == smallest - 1) {
+        /* extend first or last range */
+
+        if (i == 0) {
+            ctx->first_range++;
+
+        } else {
+            r->range++;
+        }
+
+        return NGX_OK;
+    }
+
+    /* nothing found, add new range at the tail  */
+
+    if (ctx->nranges == NGX_QUIC_MAX_RANGES) {
+        /* packet is too old to keep it */
+
+        if (pkt->need_ack) {
+            return ngx_quic_send_ack_range(c, ctx, pn, pn);
+        }
+
+        return NGX_OK;
+    }
+
+    gap = smallest - 2 - pn;
+    range = 0;
+
+insert:
+
+    if (ctx->nranges < NGX_QUIC_MAX_RANGES) {
+        ctx->nranges++;
+    }
+
+    ngx_memmove(&ctx->ranges[i + 1], &ctx->ranges[i],
+                sizeof(ngx_quic_ack_range_t) * (ctx->nranges - i - 1));
+
+    ctx->ranges[i].gap = gap;
+    ctx->ranges[i].range = range;
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_quic_generate_ack(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx)
+{
+    ngx_msec_t              delay;
+    ngx_quic_connection_t  *qc;
+
+    if (!ctx->send_ack) {
+        return NGX_OK;
+    }
+
+    if (ctx->level == ssl_encryption_application)  {
+
+        delay = ngx_current_msec - ctx->ack_delay_start;
+        qc = ngx_quic_get_connection(c);
+
+        if (ctx->send_ack < NGX_QUIC_MAX_ACK_GAP
+            && delay < qc->tp.max_ack_delay)
+        {
+            if (!qc->push.timer_set && !qc->closing) {
+                ngx_add_timer(&qc->push,
+                              qc->tp.max_ack_delay - delay);
+            }
+
+            return NGX_OK;
+        }
+    }
+
+    if (ngx_quic_send_ack(c, ctx) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    ctx->send_ack = 0;
+
+    return NGX_OK;
+}
new file mode 100644
--- /dev/null
+++ b/src/event/quic/ngx_event_quic_ack.h
@@ -0,0 +1,31 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_EVENT_QUIC_ACK_H_INCLUDED_
+#define _NGX_EVENT_QUIC_ACK_H_INCLUDED_
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+
+ngx_int_t ngx_quic_handle_ack_frame(ngx_connection_t *c,
+    ngx_quic_header_t *pkt, ngx_quic_frame_t *f);
+
+void ngx_quic_congestion_ack(ngx_connection_t *c,
+    ngx_quic_frame_t *frame);
+void ngx_quic_resend_frames(ngx_connection_t *c,
+    ngx_quic_send_ctx_t *ctx);
+void ngx_quic_set_lost_timer(ngx_connection_t *c);
+void ngx_quic_pto_handler(ngx_event_t *ev);
+ngx_msec_t ngx_quic_pto(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx);
+
+ngx_int_t ngx_quic_ack_packet(ngx_connection_t *c,
+    ngx_quic_header_t *pkt);
+ngx_int_t ngx_quic_generate_ack(ngx_connection_t *c,
+    ngx_quic_send_ctx_t *ctx);
+
+#endif /* _NGX_EVENT_QUIC_ACK_H_INCLUDED_ */
--- a/src/event/quic/ngx_event_quic_connection.h
+++ b/src/event/quic/ngx_event_quic_connection.h
@@ -15,39 +15,22 @@
 #include <ngx_event_quic_protection.h>
 
 typedef struct ngx_quic_connection_s  ngx_quic_connection_t;
+typedef struct ngx_quic_send_ctx_s    ngx_quic_send_ctx_t;
 
 #include <ngx_event_quic_frames.h>
 #include <ngx_event_quic_migration.h>
 #include <ngx_event_quic_connid.h>
 #include <ngx_event_quic_streams.h>
+#include <ngx_event_quic_ack.h>
+#include <ngx_event_quic_output.h>
 
 
-#define NGX_QUIC_MAX_SHORT_HEADER            25 /* 1 flags + 20 dcid + 4 pn */
-#define NGX_QUIC_MAX_LONG_HEADER             56
-    /* 1 flags + 4 version + 2 x (1 + 20) s/dcid + 4 pn + 4 len + token len */
-
-#define NGX_QUIC_MAX_UDP_PAYLOAD_OUT         1252
-#define NGX_QUIC_MAX_UDP_PAYLOAD_OUT6        1232
-
-#define NGX_QUIC_RETRY_TOKEN_LIFETIME          3 /* seconds */
-#define NGX_QUIC_NEW_TOKEN_LIFETIME          600 /* seconds */
-#define NGX_QUIC_RETRY_BUFFER_SIZE           256
-    /* 1 flags + 4 version + 3 x (1 + 20) s/o/dcid + itag + token(64) */
 #define NGX_QUIC_MAX_TOKEN_SIZE              64
     /* SHA-1(addr)=20 + sizeof(time_t) + retry(1) + odcid.len(1) + odcid */
 
 /* quic-recovery, section 6.2.2, kInitialRtt */
 #define NGX_QUIC_INITIAL_RTT                 333 /* ms */
 
-/* quic-recovery, section 6.1.1, Packet Threshold */
-#define NGX_QUIC_PKT_THR                     3 /* packets */
-/* quic-recovery, section 6.1.2, Time Threshold */
-#define NGX_QUIC_TIME_THR                    1.125
-#define NGX_QUIC_TIME_GRANULARITY            1 /* ms */
-
-#define NGX_QUIC_CC_MIN_INTERVAL             1000 /* 1s */
-
-
 #define NGX_QUIC_UNSET_PN                    (uint64_t) -1
 
 #define NGX_QUIC_SEND_CTX_LAST               (NGX_QUIC_ENCRYPTION_LAST - 1)
@@ -124,7 +107,7 @@ typedef struct {
  *  with Initial packet protection keys and acknowledged in packets which
  *  are also Initial packets.
 */
-typedef struct {
+struct ngx_quic_send_ctx_s {
     enum ssl_encryption_level_t       level;
 
     uint64_t                          pnum;        /* to be sent */
@@ -142,7 +125,7 @@ typedef struct {
     ngx_uint_t                        nranges;
     ngx_quic_ack_range_t              ranges[NGX_QUIC_MAX_RANGES];
     ngx_uint_t                        send_ack;
-} ngx_quic_send_ctx_t;
+};
 
 
 struct ngx_quic_connection_s {
@@ -221,16 +204,22 @@ struct ngx_quic_connection_s {
 
 
 void ngx_quic_close_connection(ngx_connection_t *c, ngx_int_t rc);
-ngx_msec_t ngx_quic_pto(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx);
+void ngx_quic_shutdown_quic(ngx_connection_t *c);
 
 ngx_int_t ngx_quic_new_sr_token(ngx_connection_t *c, ngx_str_t *cid,
     u_char *secret, u_char *token);
-
-ngx_int_t ngx_quic_output(ngx_connection_t *c);
-void ngx_quic_shutdown_quic(ngx_connection_t *c);
+ngx_int_t ngx_quic_new_token(ngx_connection_t *c, u_char *key,
+    ngx_str_t *token, ngx_str_t *odcid, time_t expires, ngx_uint_t is_retry);
 
 /********************************* DEBUG *************************************/
 
+#if (NGX_DEBUG)
+void ngx_quic_connstate_dbg(ngx_connection_t *c);
+#else
+#define ngx_quic_connstate_dbg(c)
+#endif
+
+
 /* #define NGX_QUIC_DEBUG_PACKETS */      /* dump packet contents */
 /* #define NGX_QUIC_DEBUG_FRAMES */       /* dump frames contents */
 /* #define NGX_QUIC_DEBUG_ALLOC */        /* log frames and bufs alloc */
new file mode 100644
--- /dev/null
+++ b/src/event/quic/ngx_event_quic_output.c
@@ -0,0 +1,851 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_event.h>
+#include <ngx_event_quic_connection.h>
+
+
+#define NGX_QUIC_MAX_SHORT_HEADER        25 /* 1 flags + 20 dcid + 4 pn */
+#define NGX_QUIC_MAX_LONG_HEADER         56
+    /* 1 flags + 4 version + 2 x (1 + 20) s/dcid + 4 pn + 4 len + token len */
+
+#define NGX_QUIC_MAX_UDP_PAYLOAD_OUT   1252
+#define NGX_QUIC_MAX_UDP_PAYLOAD_OUT6  1232
+
+#define NGX_QUIC_RETRY_TOKEN_LIFETIME     3 /* seconds */
+#define NGX_QUIC_NEW_TOKEN_LIFETIME     600 /* seconds */
+#define NGX_QUIC_RETRY_BUFFER_SIZE      256
+    /* 1 flags + 4 version + 3 x (1 + 20) s/o/dcid + itag + token(64) */
+
+/*
+ * Endpoints MUST discard packets that are too small to be valid QUIC
+ * packets.  With the set of AEAD functions defined in [QUIC-TLS],
+ * packets that are smaller than 21 bytes are never valid.
+ */
+#define NGX_QUIC_MIN_PKT_LEN             21
+
+#define NGX_QUIC_MIN_SR_PACKET           43 /* 5 rand + 16 srt + 22 padding */
+#define NGX_QUIC_MAX_SR_PACKET         1200
+
+#define NGX_QUIC_CC_MIN_INTERVAL       1000 /* 1s */
+
+
+static ssize_t ngx_quic_output_packet(ngx_connection_t *c,
+    ngx_quic_send_ctx_t *ctx, u_char *data, size_t max, size_t min);
+static ngx_uint_t ngx_quic_get_padding_level(ngx_connection_t *c);
+static ssize_t ngx_quic_send(ngx_connection_t *c, u_char *buf, size_t len);
+static void ngx_quic_set_packet_number(ngx_quic_header_t *pkt,
+    ngx_quic_send_ctx_t *ctx);
+
+
+size_t
+ngx_quic_max_udp_payload(ngx_connection_t *c)
+{
+    /* TODO: path MTU discovery */
+
+#if (NGX_HAVE_INET6)
+    if (c->sockaddr->sa_family == AF_INET6) {
+        return NGX_QUIC_MAX_UDP_PAYLOAD_OUT6;
+    }
+#endif
+
+    return NGX_QUIC_MAX_UDP_PAYLOAD_OUT;
+}
+
+
+ngx_int_t
+ngx_quic_output(ngx_connection_t *c)
+{
+    off_t                   max;
+    size_t                  len, min, in_flight;
+    ssize_t                 n;
+    u_char                 *p;
+    ngx_uint_t              i, pad;
+    ngx_quic_send_ctx_t    *ctx;
+    ngx_quic_congestion_t  *cg;
+    ngx_quic_connection_t  *qc;
+    static u_char           dst[NGX_QUIC_MAX_UDP_PAYLOAD_SIZE];
+
+    c->log->action = "sending frames";
+
+    qc = ngx_quic_get_connection(c);
+    cg = &qc->congestion;
+
+    in_flight = cg->in_flight;
+
+    for ( ;; ) {
+        p = dst;
+
+        len = ngx_min(qc->ctp.max_udp_payload_size,
+                      NGX_QUIC_MAX_UDP_PAYLOAD_SIZE);
+
+        if (!qc->validated) {
+            max = qc->received * 3;
+            max = (c->sent >= max) ? 0 : max - c->sent;
+            len = ngx_min(len, (size_t) max);
+        }
+
+        pad = ngx_quic_get_padding_level(c);
+
+        for (i = 0; i < NGX_QUIC_SEND_CTX_LAST; i++) {
+
+            ctx = &qc->send_ctx[i];
+
+            if (ngx_quic_generate_ack(c, ctx) != NGX_OK) {
+                return NGX_ERROR;
+            }
+
+            min = (i == pad && p - dst < NGX_QUIC_MIN_INITIAL_SIZE)
+                  ? NGX_QUIC_MIN_INITIAL_SIZE - (p - dst) : 0;
+
+            n = ngx_quic_output_packet(c, ctx, p, len, min);
+            if (n == NGX_ERROR) {
+                return NGX_ERROR;
+            }
+
+            p += n;
+            len -= n;
+        }
+
+        len = p - dst;
+        if (len == 0) {
+            break;
+        }
+
+        n = ngx_quic_send(c, dst, len);
+        if (n == NGX_ERROR) {
+            return NGX_ERROR;
+        }
+    }
+
+    if (in_flight != cg->in_flight && !qc->send_timer_set && !qc->closing) {
+        qc->send_timer_set = 1;
+        ngx_add_timer(c->read, qc->tp.max_idle_timeout);
+    }
+
+    ngx_quic_set_lost_timer(c);
+
+    return NGX_OK;
+}
+
+
+static ngx_uint_t
+ngx_quic_get_padding_level(ngx_connection_t *c)
+{
+    ngx_queue_t            *q;
+    ngx_quic_frame_t       *f;
+    ngx_quic_send_ctx_t    *ctx;
+    ngx_quic_connection_t  *qc;
+
+    /*
+     * 14.1.  Initial Datagram Size
+     *
+     * Similarly, a server MUST expand the payload of all UDP datagrams
+     * carrying ack-eliciting Initial packets to at least the smallest
+     * allowed maximum datagram size of 1200 bytes
+     */
+
+    qc = ngx_quic_get_connection(c);
+    ctx = ngx_quic_get_send_ctx(qc, ssl_encryption_initial);
+
+    for (q = ngx_queue_head(&ctx->frames);
+         q != ngx_queue_sentinel(&ctx->frames);
+         q = ngx_queue_next(q))
+    {
+        f = ngx_queue_data(q, ngx_quic_frame_t, queue);
+
+        if (f->need_ack) {
+            ctx = ngx_quic_get_send_ctx(qc, ssl_encryption_handshake);
+
+            if (ngx_queue_empty(&ctx->frames)) {
+                return 0;
+            }
+
+            return 1;
+        }
+    }
+
+    return NGX_QUIC_SEND_CTX_LAST;
+}
+
+
+static ssize_t
+ngx_quic_output_packet(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx,
+    u_char *data, size_t max, size_t min)
+{
+    size_t                  len, hlen, pad_len;
+    u_char                 *p;
+    ssize_t                 flen;
+    ngx_str_t               out, res;
+    ngx_int_t               rc;
+    ngx_uint_t              nframes;
+    ngx_msec_t              now;
+    ngx_queue_t            *q;
+    ngx_quic_frame_t       *f;
+    ngx_quic_header_t       pkt;
+    ngx_quic_congestion_t  *cg;
+    ngx_quic_connection_t  *qc;
+    static u_char           src[NGX_QUIC_MAX_UDP_PAYLOAD_SIZE];
+
+    if (ngx_queue_empty(&ctx->frames)) {
+        return 0;
+    }
+
+    ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic output %s packet max:%uz min:%uz",
+                   ngx_quic_level_name(ctx->level), max, min);
+
+    qc = ngx_quic_get_connection(c);
+    cg = &qc->congestion;
+
+    hlen = (ctx->level == ssl_encryption_application)
+           ? NGX_QUIC_MAX_SHORT_HEADER
+           : NGX_QUIC_MAX_LONG_HEADER;
+
+    hlen += EVP_GCM_TLS_TAG_LEN;
+    hlen -= NGX_QUIC_MAX_CID_LEN - qc->scid.len;
+
+    ngx_memzero(&pkt, sizeof(ngx_quic_header_t));
+
+    now = ngx_current_msec;
+    nframes = 0;
+    p = src;
+    len = 0;
+
+    for (q = ngx_queue_head(&ctx->frames);
+         q != ngx_queue_sentinel(&ctx->frames);
+         q = ngx_queue_next(q))
+    {
+        f = ngx_queue_data(q, ngx_quic_frame_t, queue);
+
+        if (!pkt.need_ack && f->need_ack && max > cg->window) {
+            max = cg->window;
+        }
+
+        if (hlen + len >= max) {
+            break;
+        }
+
+        if (hlen + len + f->len > max) {
+            rc = ngx_quic_split_frame(c, f, max - hlen - len);
+
+            if (rc == NGX_ERROR) {
+                return NGX_ERROR;
+            }
+
+            if (rc == NGX_DECLINED) {
+                break;
+            }
+        }
+
+        if (f->need_ack) {
+            pkt.need_ack = 1;
+        }
+
+        ngx_quic_log_frame(c->log, f, 1);
+
+        flen = ngx_quic_create_frame(p, f);
+        if (flen == -1) {
+            return NGX_ERROR;
+        }
+
+        len += flen;
+        p += flen;
+
+        f->pnum = ctx->pnum;
+        f->first = now;
+        f->last = now;
+        f->plen = 0;
+
+        nframes++;
+
+        if (f->flush) {
+            break;
+        }
+    }
+
+    if (nframes == 0) {
+        return 0;
+    }
+
+    out.data = src;
+    out.len = len;
+
+    pkt.keys = qc->keys;
+    pkt.flags = NGX_QUIC_PKT_FIXED_BIT;
+
+    if (ctx->level == ssl_encryption_initial) {
+        pkt.flags |= NGX_QUIC_PKT_LONG | NGX_QUIC_PKT_INITIAL;
+
+    } else if (ctx->level == ssl_encryption_handshake) {
+        pkt.flags |= NGX_QUIC_PKT_LONG | NGX_QUIC_PKT_HANDSHAKE;
+
+    } else {
+        if (qc->key_phase) {
+            pkt.flags |= NGX_QUIC_PKT_KPHASE;
+        }
+    }
+
+    ngx_quic_set_packet_number(&pkt, ctx);
+
+    pkt.version = qc->version;
+    pkt.log = c->log;
+    pkt.level = ctx->level;
+    pkt.dcid = qc->scid;
+    pkt.scid = qc->dcid;
+
+    pad_len = 4;
+
+    if (min) {
+        hlen = EVP_GCM_TLS_TAG_LEN
+               + ngx_quic_create_header(&pkt, NULL, out.len, NULL);
+
+        if (min > hlen + pad_len) {
+            pad_len = min - hlen;
+        }
+    }
+
+    if (out.len < pad_len) {
+        ngx_memset(p, NGX_QUIC_FT_PADDING, pad_len - out.len);
+        out.len = pad_len;
+    }
+
+    pkt.payload = out;
+
+    res.data = data;
+
+    ngx_log_debug6(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic packet tx %s bytes:%ui"
+                   " need_ack:%d number:%L encoded nl:%d trunc:0x%xD",
+                   ngx_quic_level_name(ctx->level), out.len, pkt.need_ack,
+                   pkt.number, pkt.num_len, pkt.trunc);
+
+    if (ngx_quic_encrypt(&pkt, &res) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    ctx->pnum++;
+
+    if (pkt.need_ack) {
+        /* move frames into the sent queue to wait for ack */
+
+        if (!qc->closing) {
+            q = ngx_queue_head(&ctx->frames);
+            f = ngx_queue_data(q, ngx_quic_frame_t, queue);
+            f->plen = res.len;
+
+            do {
+                q = ngx_queue_head(&ctx->frames);
+                ngx_queue_remove(q);
+                ngx_queue_insert_tail(&ctx->sent, q);
+            } while (--nframes);
+        }
+
+        cg->in_flight += res.len;
+
+        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic congestion send if:%uz", cg->in_flight);
+    }
+
+    while (nframes--) {
+        q = ngx_queue_head(&ctx->frames);
+        f = ngx_queue_data(q, ngx_quic_frame_t, queue);
+
+        ngx_queue_remove(q);
+        ngx_quic_free_frame(c, f);
+    }
+
+    return res.len;
+}
+
+
+ssize_t
+ngx_quic_send(ngx_connection_t *c, u_char *buf, size_t len)
+{
+    ngx_buf_t    b;
+    ngx_chain_t  cl, *res;
+
+    ngx_memzero(&b, sizeof(ngx_buf_t));
+
+    b.pos = b.start = buf;
+    b.last = b.end = buf + len;
+    b.last_buf = 1;
+    b.temporary = 1;
+
+    cl.buf = &b;
+    cl.next= NULL;
+
+    res = c->send_chain(c, &cl, 0);
+    if (res == NGX_CHAIN_ERROR) {
+        return NGX_ERROR;
+    }
+
+    return len;
+}
+
+
+static void
+ngx_quic_set_packet_number(ngx_quic_header_t *pkt, ngx_quic_send_ctx_t *ctx)
+{
+    uint64_t  delta;
+
+    delta = ctx->pnum - ctx->largest_ack;
+    pkt->number = ctx->pnum;
+
+    if (delta <= 0x7F) {
+        pkt->num_len = 1;
+        pkt->trunc = ctx->pnum & 0xff;
+
+    } else if (delta <= 0x7FFF) {
+        pkt->num_len = 2;
+        pkt->flags |= 0x1;
+        pkt->trunc = ctx->pnum & 0xffff;
+
+    } else if (delta <= 0x7FFFFF) {
+        pkt->num_len = 3;
+        pkt->flags |= 0x2;
+        pkt->trunc = ctx->pnum & 0xffffff;
+
+    } else {
+        pkt->num_len = 4;
+        pkt->flags |= 0x3;
+        pkt->trunc = ctx->pnum & 0xffffffff;
+    }
+}
+
+
+ngx_int_t
+ngx_quic_negotiate_version(ngx_connection_t *c, ngx_quic_header_t *inpkt)
+{
+    size_t             len;
+    ngx_quic_header_t  pkt;
+    static u_char      buf[NGX_QUIC_MAX_UDP_PAYLOAD_SIZE];
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "sending version negotiation packet");
+
+    pkt.log = c->log;
+    pkt.flags = NGX_QUIC_PKT_LONG | NGX_QUIC_PKT_FIXED_BIT;
+    pkt.dcid = inpkt->scid;
+    pkt.scid = inpkt->dcid;
+
+    len = ngx_quic_create_version_negotiation(&pkt, buf);
+
+#ifdef NGX_QUIC_DEBUG_PACKETS
+    ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic vnego packet to send len:%uz %*xs", len, len, buf);
+#endif
+
+    (void) ngx_quic_send(c, buf, len);
+
+    return NGX_ERROR;
+}
+
+
+int
+ngx_quic_send_alert(ngx_ssl_conn_t *ssl_conn, enum ssl_encryption_level_t level,
+    uint8_t alert)
+{
+    ngx_connection_t       *c;
+    ngx_quic_connection_t  *qc;
+
+    c = ngx_ssl_get_connection((ngx_ssl_conn_t *) ssl_conn);
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic ngx_quic_send_alert() lvl:%d  alert:%d",
+                   (int) level, (int) alert);
+
+    qc = ngx_quic_get_connection(c);
+    if (qc == NULL) {
+        return 1;
+    }
+
+    qc->error_level = level;
+    qc->error = NGX_QUIC_ERR_CRYPTO(alert);
+    qc->error_reason = "TLS alert";
+    qc->error_app = 0;
+    qc->error_ftype = 0;
+
+    if (ngx_quic_send_cc(c) != NGX_OK) {
+        return 0;
+    }
+
+    return 1;
+}
+
+
+ngx_int_t
+ngx_quic_send_stateless_reset(ngx_connection_t *c, ngx_quic_conf_t *conf,
+    ngx_quic_header_t *pkt)
+{
+    u_char    *token;
+    size_t     len, max;
+    uint16_t   rndbytes;
+    u_char     buf[NGX_QUIC_MAX_SR_PACKET];
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic handle stateless reset output");
+
+    if (pkt->len <= NGX_QUIC_MIN_PKT_LEN) {
+        return NGX_DECLINED;
+    }
+
+    if (pkt->len <= NGX_QUIC_MIN_SR_PACKET) {
+        len = pkt->len - 1;
+
+    } else {
+        max = ngx_min(NGX_QUIC_MAX_SR_PACKET, pkt->len * 3);
+
+        if (RAND_bytes((u_char *) &rndbytes, sizeof(rndbytes)) != 1) {
+            return NGX_ERROR;
+        }
+
+        len = (rndbytes % (max - NGX_QUIC_MIN_SR_PACKET + 1))
+              + NGX_QUIC_MIN_SR_PACKET;
+    }
+
+    if (RAND_bytes(buf, len - NGX_QUIC_SR_TOKEN_LEN) != 1) {
+        return NGX_ERROR;
+    }
+
+    buf[0] &= ~NGX_QUIC_PKT_LONG;
+    buf[0] |= NGX_QUIC_PKT_FIXED_BIT;
+
+    token = &buf[len - NGX_QUIC_SR_TOKEN_LEN];
+
+    if (ngx_quic_new_sr_token(c, &pkt->dcid, conf->sr_token_key, token)
+        != NGX_OK)
+    {
+        return NGX_ERROR;
+    }
+
+    (void) ngx_quic_send(c, buf, len);
+
+    return NGX_DECLINED;
+}
+
+
+ngx_int_t
+ngx_quic_send_cc(ngx_connection_t *c)
+{
+    ngx_quic_frame_t       *frame;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    if (qc->draining) {
+        return NGX_OK;
+    }
+
+    if (qc->closing
+        && ngx_current_msec - qc->last_cc < NGX_QUIC_CC_MIN_INTERVAL)
+    {
+        /* dot not send CC too often */
+        return NGX_OK;
+    }
+
+    frame = ngx_quic_alloc_frame(c);
+    if (frame == NULL) {
+        return NGX_ERROR;
+    }
+
+    frame->level = qc->error_level;
+    frame->type = qc->error_app ? NGX_QUIC_FT_CONNECTION_CLOSE_APP
+                                : NGX_QUIC_FT_CONNECTION_CLOSE;
+    frame->u.close.error_code = qc->error;
+    frame->u.close.frame_type = qc->error_ftype;
+
+    if (qc->error_reason) {
+        frame->u.close.reason.len = ngx_strlen(qc->error_reason);
+        frame->u.close.reason.data = (u_char *) qc->error_reason;
+    }
+
+    ngx_quic_queue_frame(qc, frame);
+
+    qc->last_cc = ngx_current_msec;
+
+    return ngx_quic_output(c);
+}
+
+
+ngx_int_t
+ngx_quic_send_early_cc(ngx_connection_t *c, ngx_quic_header_t *inpkt,
+    ngx_uint_t err, const char *reason)
+{
+    ssize_t            len;
+    ngx_str_t          res;
+    ngx_quic_frame_t   frame;
+    ngx_quic_header_t  pkt;
+
+    static u_char       src[NGX_QUIC_MAX_UDP_PAYLOAD_SIZE];
+    static u_char       dst[NGX_QUIC_MAX_UDP_PAYLOAD_SIZE];
+
+    ngx_memzero(&frame, sizeof(ngx_quic_frame_t));
+    ngx_memzero(&pkt, sizeof(ngx_quic_header_t));
+
+    frame.level = inpkt->level;
+    frame.type = NGX_QUIC_FT_CONNECTION_CLOSE;
+    frame.u.close.error_code = err;
+
+    frame.u.close.reason.data = (u_char *) reason;
+    frame.u.close.reason.len = ngx_strlen(reason);
+
+    len = ngx_quic_create_frame(NULL, &frame);
+    if (len > NGX_QUIC_MAX_UDP_PAYLOAD_SIZE) {
+        return NGX_ERROR;
+    }
+
+    ngx_quic_log_frame(c->log, &frame, 1);
+
+    len = ngx_quic_create_frame(src, &frame);
+    if (len == -1) {
+        return NGX_ERROR;
+    }
+
+    pkt.keys = ngx_quic_keys_new(c->pool);
+    if (pkt.keys == NULL) {
+        return NGX_ERROR;
+    }
+
+    if (ngx_quic_keys_set_initial_secret(c->pool, pkt.keys, &inpkt->dcid,
+                                         inpkt->version)
+        != NGX_OK)
+    {
+        return NGX_ERROR;
+    }
+
+    pkt.flags = NGX_QUIC_PKT_FIXED_BIT | NGX_QUIC_PKT_LONG
+                | NGX_QUIC_PKT_INITIAL;
+
+    pkt.num_len = 1;
+    /*
+     * pkt.num = 0;
+     * pkt.trunc = 0;
+     */
+
+    pkt.version = inpkt->version;
+    pkt.log = c->log;
+    pkt.level = inpkt->level;
+    pkt.dcid = inpkt->scid;
+    pkt.scid = inpkt->dcid;
+    pkt.payload.data = src;
+    pkt.payload.len = len;
+
+    res.data = dst;
+
+    if (ngx_quic_encrypt(&pkt, &res) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    if (ngx_quic_send(c, res.data, res.len) == NGX_ERROR) {
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_quic_send_retry(ngx_connection_t *c, ngx_quic_conf_t *conf,
+    ngx_quic_header_t *inpkt)
+{
+    time_t             expires;
+    ssize_t            len;
+    ngx_str_t          res, token;
+    ngx_quic_header_t  pkt;
+
+    u_char             buf[NGX_QUIC_RETRY_BUFFER_SIZE];
+    u_char             dcid[NGX_QUIC_SERVER_CID_LEN];
+
+    expires = ngx_time() + NGX_QUIC_RETRY_TOKEN_LIFETIME;
+
+    if (ngx_quic_new_token(c, conf->av_token_key, &token, &inpkt->dcid,
+                           expires, 1)
+        != NGX_OK)
+    {
+        return NGX_ERROR;
+    }
+
+    ngx_memzero(&pkt, sizeof(ngx_quic_header_t));
+    pkt.flags = NGX_QUIC_PKT_FIXED_BIT | NGX_QUIC_PKT_LONG | NGX_QUIC_PKT_RETRY;
+    pkt.version = inpkt->version;
+    pkt.log = c->log;
+
+    pkt.odcid = inpkt->dcid;
+    pkt.dcid = inpkt->scid;
+
+    /* TODO: generate routable dcid */
+    if (RAND_bytes(dcid, NGX_QUIC_SERVER_CID_LEN) != 1) {
+        return NGX_ERROR;
+    }
+
+    pkt.scid.len = NGX_QUIC_SERVER_CID_LEN;
+    pkt.scid.data = dcid;
+
+    pkt.token = token;
+
+    res.data = buf;
+
+    if (ngx_quic_encrypt(&pkt, &res) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+#ifdef NGX_QUIC_DEBUG_PACKETS
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic packet to send len:%uz %xV", res.len, &res);
+#endif
+
+    len = ngx_quic_send(c, res.data, res.len);
+    if (len == NGX_ERROR) {
+        return NGX_ERROR;
+    }
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic retry packet sent to %xV", &pkt.dcid);
+
+    /*
+     * quic-transport 17.2.5.1:  A server MUST NOT send more than one Retry
+     * packet in response to a single UDP datagram.
+     * NGX_DONE will stop quic_input() from processing further
+     */
+    return NGX_DONE;
+}
+
+
+ngx_int_t
+ngx_quic_send_new_token(ngx_connection_t *c)
+{
+    time_t                  expires;
+    ngx_str_t               token;
+    ngx_quic_frame_t       *frame;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    if (!qc->conf->retry) {
+        return NGX_OK;
+    }
+
+    expires = ngx_time() + NGX_QUIC_NEW_TOKEN_LIFETIME;
+
+    if (ngx_quic_new_token(c, qc->conf->av_token_key, &token, NULL, expires, 0)
+        != NGX_OK)
+    {
+        return NGX_ERROR;
+    }
+
+    frame = ngx_quic_alloc_frame(c);
+    if (frame == NULL) {
+        return NGX_ERROR;
+    }
+
+    frame->level = ssl_encryption_application;
+    frame->type = NGX_QUIC_FT_NEW_TOKEN;
+    frame->u.token.length = token.len;
+    frame->u.token.data = token.data;
+
+    ngx_quic_queue_frame(qc, frame);
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_quic_send_ack(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx)
+{
+    size_t                  len, left;
+    uint64_t                ack_delay;
+    ngx_buf_t              *b;
+    ngx_uint_t              i;
+    ngx_chain_t            *cl, **ll;
+    ngx_quic_frame_t       *frame;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    ack_delay = ngx_current_msec - ctx->largest_received;
+    ack_delay *= 1000;
+    ack_delay >>= qc->tp.ack_delay_exponent;
+
+    frame = ngx_quic_alloc_frame(c);
+    if (frame == NULL) {
+        return NGX_ERROR;
+    }
+
+    ll = &frame->data;
+    b = NULL;
+
+    for (i = 0; i < ctx->nranges; i++) {
+        len = ngx_quic_create_ack_range(NULL, ctx->ranges[i].gap,
+                                        ctx->ranges[i].range);
+
+        left = b ? b->end - b->last : 0;
+
+        if (left < len) {
+            cl = ngx_quic_alloc_buf(c);
+            if (cl == NULL) {
+                return NGX_ERROR;
+            }
+
+            *ll = cl;
+            ll = &cl->next;
+
+            b = cl->buf;
+            left = b->end - b->last;
+
+            if (left < len) {
+                return NGX_ERROR;
+            }
+        }
+
+        b->last += ngx_quic_create_ack_range(b->last, ctx->ranges[i].gap,
+                                             ctx->ranges[i].range);
+
+        frame->u.ack.ranges_length += len;
+    }
+
+    *ll = NULL;
+
+    frame->level = ctx->level;
+    frame->type = NGX_QUIC_FT_ACK;
+    frame->u.ack.largest = ctx->largest_range;
+    frame->u.ack.delay = ack_delay;
+    frame->u.ack.range_count = ctx->nranges;
+    frame->u.ack.first_range = ctx->first_range;
+
+    ngx_quic_queue_frame(qc, frame);
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_quic_send_ack_range(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx,
+    uint64_t smallest, uint64_t largest)
+{
+    ngx_quic_frame_t       *frame;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    frame = ngx_quic_alloc_frame(c);
+    if (frame == NULL) {
+        return NGX_ERROR;
+    }
+
+    frame->level = ctx->level;
+    frame->type = NGX_QUIC_FT_ACK;
+    frame->u.ack.largest = largest;
+    frame->u.ack.delay = 0;
+    frame->u.ack.range_count = 0;
+    frame->u.ack.first_range = largest - smallest;
+
+    ngx_quic_queue_frame(qc, frame);
+
+    return NGX_OK;
+}
new file mode 100644
--- /dev/null
+++ b/src/event/quic/ngx_event_quic_output.h
@@ -0,0 +1,40 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_EVENT_QUIC_OUTPUT_H_INCLUDED_
+#define _NGX_EVENT_QUIC_OUTPUT_H_INCLUDED_
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+
+size_t ngx_quic_max_udp_payload(ngx_connection_t *c);
+
+ngx_int_t ngx_quic_output(ngx_connection_t *c);
+
+ngx_int_t ngx_quic_negotiate_version(ngx_connection_t *c,
+    ngx_quic_header_t *inpkt);
+
+int ngx_quic_send_alert(ngx_ssl_conn_t *ssl_conn,
+    enum ssl_encryption_level_t level, uint8_t alert);
+
+ngx_int_t ngx_quic_send_stateless_reset(ngx_connection_t *c,
+    ngx_quic_conf_t *conf, ngx_quic_header_t *pkt);
+ngx_int_t ngx_quic_send_cc(ngx_connection_t *c);
+ngx_int_t ngx_quic_send_early_cc(ngx_connection_t *c,
+    ngx_quic_header_t *inpkt, ngx_uint_t err, const char *reason);
+
+ngx_int_t ngx_quic_send_retry(ngx_connection_t *c,
+    ngx_quic_conf_t *conf, ngx_quic_header_t *pkt);
+ngx_int_t ngx_quic_send_new_token(ngx_connection_t *c);
+
+ngx_int_t ngx_quic_send_ack(ngx_connection_t *c,
+    ngx_quic_send_ctx_t *ctx);
+ngx_int_t ngx_quic_send_ack_range(ngx_connection_t *c,
+    ngx_quic_send_ctx_t *ctx, uint64_t smallest, uint64_t largest);
+
+#endif /* _NGX_EVENT_QUIC_OUTPUT_H_INCLUDED_ */