changeset 8330:5b7ec588de48 quic

Merged with the default branch.
author Sergey Kandaurov <pluknet@nginx.com>
date Tue, 14 Apr 2020 19:35:20 +0300
parents 7bd334b8d91a (diff) 3a860f22c879 (current diff)
children bda817d16cc2
files src/http/ngx_http_core_module.c src/http/ngx_http_core_module.h src/http/ngx_http_request.c
diffstat 35 files changed, 9973 insertions(+), 95 deletions(-) [+]
line wrap: on
line diff
--- a/auto/lib/openssl/conf
+++ b/auto/lib/openssl/conf
@@ -140,3 +140,12 @@ END
     fi
 
 fi
+
+ngx_feature="OpenSSL QUIC support"
+ngx_feature_name="NGX_OPENSSL_QUIC"
+ngx_feature_run=no
+ngx_feature_incs="#include <openssl/ssl.h>"
+ngx_feature_path=
+ngx_feature_libs="-lssl -lcrypto $NGX_LIBDL"
+ngx_feature_test="SSL_CTX_set_quic_method(NULL, NULL)"
+. auto/feature
--- a/auto/make
+++ b/auto/make
@@ -7,8 +7,8 @@ echo "creating $NGX_MAKEFILE"
 
 mkdir -p $NGX_OBJS/src/core $NGX_OBJS/src/event $NGX_OBJS/src/event/modules \
          $NGX_OBJS/src/os/unix $NGX_OBJS/src/os/win32 \
-         $NGX_OBJS/src/http $NGX_OBJS/src/http/v2 $NGX_OBJS/src/http/modules \
-         $NGX_OBJS/src/http/modules/perl \
+         $NGX_OBJS/src/http $NGX_OBJS/src/http/v2 $NGX_OBJS/src/http/v3 \
+         $NGX_OBJS/src/http/modules $NGX_OBJS/src/http/modules/perl \
          $NGX_OBJS/src/mail \
          $NGX_OBJS/src/stream \
          $NGX_OBJS/src/misc
--- a/auto/modules
+++ b/auto/modules
@@ -403,6 +403,29 @@ if [ $HTTP = YES ]; then
 
     ngx_module_type=HTTP
 
+    if [ $HTTP_V3 = YES ]; then
+        have=NGX_HTTP_V3 . auto/have
+        have=NGX_HTTP_HEADERS . auto/have
+
+        # XXX for Huffman
+        HTTP_V2=YES
+
+        ngx_module_name=ngx_http_v3_module
+        ngx_module_incs=src/http/v3
+        ngx_module_deps="src/http/v3/ngx_http_v3.h \
+                         src/http/v3/ngx_http_v3_parse.h"
+        ngx_module_srcs="src/http/v3/ngx_http_v3.c \
+                         src/http/v3/ngx_http_v3_parse.c \
+                         src/http/v3/ngx_http_v3_tables.c \
+                         src/http/v3/ngx_http_v3_streams.c \
+                         src/http/v3/ngx_http_v3_request.c \
+                         src/http/v3/ngx_http_v3_module.c"
+        ngx_module_libs=
+        ngx_module_link=$HTTP_V3
+
+        . auto/module
+    fi
+
     if [ $HTTP_V2 = YES ]; then
         have=NGX_HTTP_V2 . auto/have
         have=NGX_HTTP_HEADERS . auto/have
@@ -1242,9 +1265,15 @@ if [ $USE_OPENSSL = YES ]; then
     ngx_module_type=CORE
     ngx_module_name=ngx_openssl_module
     ngx_module_incs=
-    ngx_module_deps=src/event/ngx_event_openssl.h
-    ngx_module_srcs="src/event/ngx_event_openssl.c
-                     src/event/ngx_event_openssl_stapling.c"
+    ngx_module_deps="src/event/ngx_event_openssl.h \
+                     src/event/ngx_event_quic.h \
+                     src/event/ngx_event_quic_transport.h \
+                     src/event/ngx_event_quic_protection.h"
+    ngx_module_srcs="src/event/ngx_event_openssl.c \
+                     src/event/ngx_event_openssl_stapling.c \
+                     src/event/ngx_event_quic.c \
+                     src/event/ngx_event_quic_transport.c \
+                     src/event/ngx_event_quic_protection.c"
     ngx_module_libs=
     ngx_module_link=YES
     ngx_module_order=
--- a/auto/options
+++ b/auto/options
@@ -59,6 +59,7 @@ HTTP_CHARSET=YES
 HTTP_GZIP=YES
 HTTP_SSL=NO
 HTTP_V2=NO
+HTTP_V3=YES
 HTTP_SSI=YES
 HTTP_REALIP=NO
 HTTP_XSLT=NO
--- a/src/core/ngx_connection.c
+++ b/src/core/ngx_connection.c
@@ -1178,11 +1178,6 @@ ngx_close_connection(ngx_connection_t *c
     ngx_uint_t    log_error, level;
     ngx_socket_t  fd;
 
-    if (c->fd == (ngx_socket_t) -1) {
-        ngx_log_error(NGX_LOG_ALERT, c->log, 0, "connection already closed");
-        return;
-    }
-
     if (c->read->timer_set) {
         ngx_del_timer(c->read);
     }
@@ -1191,7 +1186,7 @@ ngx_close_connection(ngx_connection_t *c
         ngx_del_timer(c->write);
     }
 
-    if (!c->shared) {
+    if (!c->shared && c->fd != (ngx_socket_t) -1) {
         if (ngx_del_conn) {
             ngx_del_conn(c, NGX_CLOSE_EVENT);
 
@@ -1223,6 +1218,11 @@ ngx_close_connection(ngx_connection_t *c
 
     ngx_free_connection(c);
 
+    if (c->fd == (ngx_socket_t) -1) {
+        ngx_log_debug0(NGX_LOG_DEBUG_CORE, c->log, 0, "connection has no fd");
+        return;
+    }
+
     fd = c->fd;
     c->fd = (ngx_socket_t) -1;
 
--- a/src/core/ngx_connection.h
+++ b/src/core/ngx_connection.h
@@ -147,13 +147,15 @@ struct ngx_connection_s {
     socklen_t           socklen;
     ngx_str_t           addr_text;
 
-    ngx_proxy_protocol_t  *proxy_protocol;
+    ngx_proxy_protocol_t   *proxy_protocol;
 
 #if (NGX_SSL || NGX_COMPAT)
-    ngx_ssl_connection_t  *ssl;
+    ngx_quic_connection_t  *quic;
+    ngx_quic_stream_t      *qs;
+    ngx_ssl_connection_t   *ssl;
 #endif
 
-    ngx_udp_connection_t  *udp;
+    ngx_udp_connection_t   *udp;
 
     struct sockaddr    *local_sockaddr;
     socklen_t           local_socklen;
--- a/src/core/ngx_core.h
+++ b/src/core/ngx_core.h
@@ -12,23 +12,25 @@
 #include <ngx_config.h>
 
 
-typedef struct ngx_module_s          ngx_module_t;
-typedef struct ngx_conf_s            ngx_conf_t;
-typedef struct ngx_cycle_s           ngx_cycle_t;
-typedef struct ngx_pool_s            ngx_pool_t;
-typedef struct ngx_chain_s           ngx_chain_t;
-typedef struct ngx_log_s             ngx_log_t;
-typedef struct ngx_open_file_s       ngx_open_file_t;
-typedef struct ngx_command_s         ngx_command_t;
-typedef struct ngx_file_s            ngx_file_t;
-typedef struct ngx_event_s           ngx_event_t;
-typedef struct ngx_event_aio_s       ngx_event_aio_t;
-typedef struct ngx_connection_s      ngx_connection_t;
-typedef struct ngx_thread_task_s     ngx_thread_task_t;
-typedef struct ngx_ssl_s             ngx_ssl_t;
-typedef struct ngx_proxy_protocol_s  ngx_proxy_protocol_t;
-typedef struct ngx_ssl_connection_s  ngx_ssl_connection_t;
-typedef struct ngx_udp_connection_s  ngx_udp_connection_t;
+typedef struct ngx_module_s           ngx_module_t;
+typedef struct ngx_conf_s             ngx_conf_t;
+typedef struct ngx_cycle_s            ngx_cycle_t;
+typedef struct ngx_pool_s             ngx_pool_t;
+typedef struct ngx_chain_s            ngx_chain_t;
+typedef struct ngx_log_s              ngx_log_t;
+typedef struct ngx_open_file_s        ngx_open_file_t;
+typedef struct ngx_command_s          ngx_command_t;
+typedef struct ngx_file_s             ngx_file_t;
+typedef struct ngx_event_s            ngx_event_t;
+typedef struct ngx_event_aio_s        ngx_event_aio_t;
+typedef struct ngx_connection_s       ngx_connection_t;
+typedef struct ngx_thread_task_s      ngx_thread_task_t;
+typedef struct ngx_ssl_s              ngx_ssl_t;
+typedef struct ngx_proxy_protocol_s   ngx_proxy_protocol_t;
+typedef struct ngx_quic_connection_s  ngx_quic_connection_t;
+typedef struct ngx_quic_stream_s      ngx_quic_stream_t;
+typedef struct ngx_ssl_connection_s   ngx_ssl_connection_t;
+typedef struct ngx_udp_connection_s   ngx_udp_connection_t;
 
 typedef void (*ngx_event_handler_pt)(ngx_event_t *ev);
 typedef void (*ngx_connection_handler_pt)(ngx_connection_t *c);
@@ -82,6 +84,9 @@ typedef void (*ngx_connection_handler_pt
 #include <ngx_resolver.h>
 #if (NGX_OPENSSL)
 #include <ngx_event_openssl.h>
+#include <ngx_event_quic.h>
+#include <ngx_event_quic_transport.h>
+#include <ngx_event_quic_protection.h>
 #endif
 #include <ngx_process_cycle.h>
 #include <ngx_conf_file.h>
--- a/src/event/ngx_event.c
+++ b/src/event/ngx_event.c
@@ -268,6 +268,22 @@ ngx_process_events_and_timers(ngx_cycle_
 ngx_int_t
 ngx_handle_read_event(ngx_event_t *rev, ngx_uint_t flags)
 {
+    ngx_connection_t  *c;
+
+    c = rev->data;
+
+    if (c->qs) {
+
+        if (!rev->active && !rev->ready) {
+            rev->active = 1;
+
+        } else if (rev->active && (rev->ready || (flags & NGX_CLOSE_EVENT))) {
+            rev->active = 0;
+        }
+
+        return NGX_OK;
+    }
+
     if (ngx_event_flags & NGX_USE_CLEAR_EVENT) {
 
         /* kqueue, epoll */
@@ -338,14 +354,26 @@ ngx_handle_write_event(ngx_event_t *wev,
 {
     ngx_connection_t  *c;
 
+    c = wev->data;
+
     if (lowat) {
-        c = wev->data;
-
         if (ngx_send_lowat(c, lowat) == NGX_ERROR) {
             return NGX_ERROR;
         }
     }
 
+    if (c->qs) {
+
+        if (!wev->active && !wev->ready) {
+            wev->active = 1;
+
+        } else if (wev->active && wev->ready) {
+            wev->active = 0;
+        }
+
+        return NGX_OK;
+    }
+
     if (ngx_event_flags & NGX_USE_CLEAR_EVENT) {
 
         /* kqueue, epoll */
@@ -916,6 +944,10 @@ ngx_send_lowat(ngx_connection_t *c, size
 {
     int  sndlowat;
 
+    if (c->qs) {
+        return NGX_OK;
+    }
+
 #if (NGX_HAVE_LOWAT_EVENT)
 
     if (ngx_event_flags & NGX_USE_KQUEUE_EVENT) {
--- a/src/event/ngx_event_openssl.c
+++ b/src/event/ngx_event_openssl.c
@@ -2735,6 +2735,11 @@ ngx_ssl_shutdown(ngx_connection_t *c)
     int        n, sslerr, mode;
     ngx_err_t  err;
 
+    if (c->qs) {
+        /* QUIC streams inherit SSL object */
+        return NGX_OK;
+    }
+
     if (SSL_in_init(c->ssl->connection)) {
         /*
          * OpenSSL 1.0.2f complains if SSL_shutdown() is called during
--- a/src/event/ngx_event_openssl.h
+++ b/src/event/ngx_event_openssl.h
@@ -14,6 +14,7 @@
 
 #include <openssl/ssl.h>
 #include <openssl/err.h>
+#include <openssl/aes.h>
 #include <openssl/bn.h>
 #include <openssl/conf.h>
 #include <openssl/crypto.h>
@@ -22,6 +23,12 @@
 #include <openssl/engine.h>
 #endif
 #include <openssl/evp.h>
+#ifdef OPENSSL_IS_BORINGSSL
+#include <openssl/hkdf.h>
+#include <openssl/chacha.h>
+#else
+#include <openssl/kdf.h>
+#endif
 #include <openssl/hmac.h>
 #ifndef OPENSSL_NO_OCSP
 #include <openssl/ocsp.h>
new file mode 100644
--- /dev/null
+++ b/src/event/ngx_event_quic.c
@@ -0,0 +1,2488 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_event.h>
+
+
+/*  0-RTT and 1-RTT data exist in the same packet number space,
+ *  so we have 3 packet number spaces:
+ *
+ *  0 - Initial
+ *  1 - Handshake
+ *  2 - 0-RTT and 1-RTT
+ */
+#define ngx_quic_ns(level)                                                    \
+    ((level) == ssl_encryption_initial) ? 0                                   \
+        : (((level) == ssl_encryption_handshake) ? 1 : 2)
+
+#define NGX_QUIC_NAMESPACE_LAST  (NGX_QUIC_ENCRYPTION_LAST - 1)
+
+#define NGX_QUIC_STREAMS_INC     16
+#define NGX_QUIC_STREAMS_LIMIT   (1ULL < 60)
+
+
+typedef enum {
+    NGX_QUIC_ST_INITIAL,     /* connection just created */
+    NGX_QUIC_ST_HANDSHAKE,   /* handshake started */
+    NGX_QUIC_ST_EARLY_DATA,  /* handshake in progress */
+    NGX_QUIC_ST_APPLICATION  /* handshake complete */
+} ngx_quic_state_t;
+
+
+typedef struct {
+    ngx_rbtree_t                      tree;
+    ngx_rbtree_node_t                 sentinel;
+    ngx_connection_handler_pt         handler;
+
+    ngx_uint_t                        id_counter;
+} ngx_quic_streams_t;
+
+
+typedef struct {
+    ngx_quic_secret_t                 client_secret;
+    ngx_quic_secret_t                 server_secret;
+
+    uint64_t                          pnum;
+    uint64_t                          largest; /* number received from peer */
+
+    ngx_queue_t                       frames;
+    ngx_queue_t                       sent;
+} ngx_quic_namespace_t;
+
+
+struct ngx_quic_connection_s {
+    ngx_str_t                         scid;
+    ngx_str_t                         dcid;
+    ngx_str_t                         token;
+
+    ngx_uint_t                        client_tp_done;
+    ngx_quic_tp_t                     tp;
+    ngx_quic_tp_t                     ctp;
+
+    ngx_quic_state_t                  state;
+
+    ngx_quic_namespace_t              ns[NGX_QUIC_NAMESPACE_LAST];
+    ngx_quic_secrets_t                keys[NGX_QUIC_ENCRYPTION_LAST];
+    ngx_quic_secrets_t                next_key;
+    uint64_t                          crypto_offset_out[NGX_QUIC_ENCRYPTION_LAST];
+    uint64_t                          crypto_offset_in[NGX_QUIC_ENCRYPTION_LAST];
+
+    ngx_ssl_t                        *ssl;
+
+    ngx_event_t                       push;
+    ngx_event_t                       retry;
+    ngx_queue_t                       free_frames;
+
+#if (NGX_DEBUG)
+    ngx_uint_t                        nframes;
+#endif
+
+    ngx_quic_streams_t                streams;
+    ngx_uint_t                        max_data;
+
+    uint64_t                          cur_streams;
+    uint64_t                          max_streams;
+
+    unsigned                          send_timer_set:1;
+    unsigned                          closing:1;
+    unsigned                          key_phase:1;
+};
+
+
+#if BORINGSSL_API_VERSION >= 10
+static int ngx_quic_set_read_secret(ngx_ssl_conn_t *ssl_conn,
+    enum ssl_encryption_level_t level, const SSL_CIPHER *cipher,
+    const uint8_t *secret, size_t secret_len);
+static int ngx_quic_set_write_secret(ngx_ssl_conn_t *ssl_conn,
+    enum ssl_encryption_level_t level, const SSL_CIPHER *cipher,
+    const uint8_t *secret, size_t secret_len);
+#else
+static int ngx_quic_set_encryption_secrets(ngx_ssl_conn_t *ssl_conn,
+    enum ssl_encryption_level_t level, const uint8_t *read_secret,
+    const uint8_t *write_secret, size_t secret_len);
+#endif
+
+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_new_connection(ngx_connection_t *c, ngx_ssl_t *ssl,
+    ngx_quic_tp_t *tp, ngx_quic_header_t *pkt,
+    ngx_connection_handler_pt handler);
+static ngx_int_t ngx_quic_init_connection(ngx_connection_t *c);
+static void ngx_quic_input_handler(ngx_event_t *rev);
+static void ngx_quic_close_connection(ngx_connection_t *c);
+
+static ngx_int_t ngx_quic_input(ngx_connection_t *c, ngx_buf_t *b);
+static ngx_int_t ngx_quic_initial_input(ngx_connection_t *c,
+    ngx_quic_header_t *pkt);
+static ngx_int_t ngx_quic_handshake_input(ngx_connection_t *c,
+    ngx_quic_header_t *pkt);
+static ngx_int_t ngx_quic_early_input(ngx_connection_t *c,
+    ngx_quic_header_t *pkt);
+static ngx_int_t ngx_quic_app_input(ngx_connection_t *c,
+    ngx_quic_header_t *pkt);
+static ngx_int_t ngx_quic_payload_handler(ngx_connection_t *c,
+    ngx_quic_header_t *pkt);
+
+static ngx_int_t ngx_quic_handle_ack_frame(ngx_connection_t *c,
+    ngx_quic_header_t *pkt, ngx_quic_ack_frame_t *f);
+static ngx_int_t ngx_quic_handle_ack_frame_range(ngx_connection_t *c,
+    ngx_quic_namespace_t *ns, uint64_t min, uint64_t max);
+static ngx_int_t ngx_quic_handle_crypto_frame(ngx_connection_t *c,
+    ngx_quic_header_t *pkt, ngx_quic_crypto_frame_t *frame);
+static ngx_int_t ngx_quic_handle_stream_frame(ngx_connection_t *c,
+    ngx_quic_header_t *pkt, ngx_quic_stream_frame_t *frame);
+static ngx_int_t ngx_quic_handle_max_streams(ngx_connection_t *c);
+static ngx_int_t ngx_quic_handle_streams_blocked_frame(ngx_connection_t *c,
+    ngx_quic_header_t *pkt, ngx_quic_streams_blocked_frame_t *f);
+static ngx_int_t ngx_quic_handle_stream_data_blocked_frame(ngx_connection_t *c,
+    ngx_quic_header_t *pkt, ngx_quic_stream_data_blocked_frame_t *f);
+
+static void ngx_quic_queue_frame(ngx_quic_connection_t *qc,
+    ngx_quic_frame_t *frame);
+
+static ngx_int_t ngx_quic_output(ngx_connection_t *c);
+static ngx_int_t ngx_quic_output_ns(ngx_connection_t *c,
+    ngx_quic_namespace_t *ns, ngx_uint_t nsi);
+static void ngx_quic_free_frames(ngx_connection_t *c, ngx_queue_t *frames);
+static ngx_int_t ngx_quic_send_frames(ngx_connection_t *c, ngx_queue_t *frames);
+
+static void ngx_quic_set_packet_number(ngx_quic_header_t *pkt,
+    ngx_quic_namespace_t *ns);
+static void ngx_quic_retransmit_handler(ngx_event_t *ev);
+static ngx_int_t ngx_quic_retransmit_ns(ngx_connection_t *c,
+    ngx_quic_namespace_t *ns, ngx_msec_t *waitp);
+static void ngx_quic_push_handler(ngx_event_t *ev);
+
+static void ngx_quic_rbtree_insert_stream(ngx_rbtree_node_t *temp,
+    ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel);
+static ngx_quic_stream_t *ngx_quic_find_stream(ngx_rbtree_t *rbtree,
+    uint64_t id);
+static ngx_quic_stream_t *ngx_quic_create_stream(ngx_connection_t *c,
+    uint64_t id, size_t rcvbuf_size);
+static ssize_t ngx_quic_stream_recv(ngx_connection_t *c, u_char *buf,
+    size_t size);
+static ssize_t ngx_quic_stream_send(ngx_connection_t *c, u_char *buf,
+    size_t size);
+static void ngx_quic_stream_cleanup_handler(void *data);
+static ngx_chain_t *ngx_quic_stream_send_chain(ngx_connection_t *c,
+    ngx_chain_t *in, off_t limit);
+static ngx_quic_frame_t *ngx_quic_alloc_frame(ngx_connection_t *c, size_t size);
+static void ngx_quic_free_frame(ngx_connection_t *c, ngx_quic_frame_t *frame);
+
+
+static SSL_QUIC_METHOD quic_method = {
+#if BORINGSSL_API_VERSION >= 10
+    ngx_quic_set_read_secret,
+    ngx_quic_set_write_secret,
+#else
+    ngx_quic_set_encryption_secrets,
+#endif
+    ngx_quic_add_handshake_data,
+    ngx_quic_flush_flight,
+    ngx_quic_send_alert,
+};
+
+
+#if BORINGSSL_API_VERSION >= 10
+
+static int
+ngx_quic_set_read_secret(ngx_ssl_conn_t *ssl_conn,
+    enum ssl_encryption_level_t level, const SSL_CIPHER *cipher,
+    const uint8_t *rsecret, size_t secret_len)
+{
+    ngx_connection_t    *c;
+    ngx_quic_secrets_t  *keys;
+
+    c = ngx_ssl_get_connection((ngx_ssl_conn_t *) ssl_conn);
+
+    ngx_quic_hexdump(c->log, "level:%d read secret",
+                     rsecret, secret_len, level);
+
+    keys = &c->quic->keys[level];
+
+    if (level == ssl_encryption_early_data) {
+        c->quic->state = NGX_QUIC_ST_EARLY_DATA;
+    }
+
+    return ngx_quic_set_encryption_secret(c->pool, ssl_conn, level,
+                                          rsecret, secret_len,
+                                          &keys->client);
+}
+
+
+static int
+ngx_quic_set_write_secret(ngx_ssl_conn_t *ssl_conn,
+    enum ssl_encryption_level_t level, const SSL_CIPHER *cipher,
+    const uint8_t *wsecret, size_t secret_len)
+{
+    ngx_connection_t    *c;
+    ngx_quic_secrets_t  *keys;
+
+    c = ngx_ssl_get_connection((ngx_ssl_conn_t *) ssl_conn);
+
+    ngx_quic_hexdump(c->log, "level:%d write secret",
+                     wsecret, secret_len, level);
+
+    keys = &c->quic->keys[level];
+
+    return ngx_quic_set_encryption_secret(c->pool, ssl_conn, level,
+                                          wsecret, secret_len,
+                                          &keys->server);
+}
+
+#else
+
+static int
+ngx_quic_set_encryption_secrets(ngx_ssl_conn_t *ssl_conn,
+    enum ssl_encryption_level_t level, const uint8_t *rsecret,
+    const uint8_t *wsecret, size_t secret_len)
+{
+    ngx_int_t            rc;
+    ngx_connection_t    *c;
+    ngx_quic_secrets_t  *keys;
+
+    c = ngx_ssl_get_connection((ngx_ssl_conn_t *) ssl_conn);
+
+    ngx_quic_hexdump(c->log, "level:%d read", rsecret, secret_len, level);
+
+    keys = &c->quic->keys[level];
+
+    rc = ngx_quic_set_encryption_secret(c->pool, ssl_conn, level,
+                                        rsecret, secret_len,
+                                        &keys->client);
+    if (rc != 1) {
+        return rc;
+    }
+
+    if (level == ssl_encryption_early_data) {
+        c->quic->state = NGX_QUIC_ST_EARLY_DATA;
+        return 1;
+    }
+
+    ngx_quic_hexdump(c->log, "level:%d write", wsecret, secret_len, level);
+
+    return ngx_quic_set_encryption_secret(c->pool, ssl_conn, level,
+                                          wsecret, secret_len,
+                                          &keys->server);
+}
+
+#endif
+
+
+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)
+{
+    u_char                 *p, *end;
+    size_t                  client_params_len;
+    const uint8_t          *client_params;
+    ngx_quic_frame_t       *frame;
+    ngx_connection_t       *c;
+    ngx_quic_connection_t  *qc;
+
+    c = ngx_ssl_get_connection((ngx_ssl_conn_t *) ssl_conn);
+    qc = c->quic;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "ngx_quic_add_handshake_data");
+
+    /* XXX: obtain client parameters after the handshake? */
+    if (!qc->client_tp_done) {
+
+        SSL_get_peer_quic_transport_params(ssl_conn, &client_params,
+                                           &client_params_len);
+
+        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "SSL_get_peer_quic_transport_params(): params_len %ui",
+                       client_params_len);
+
+        if (client_params_len != 0) {
+            p = (u_char *) client_params;
+            end = p + client_params_len;
+
+            if (ngx_quic_parse_transport_params(p, end, &qc->ctp, c->log)
+                != NGX_OK)
+            {
+                return NGX_ERROR;
+            }
+
+            if (qc->ctp.max_idle_timeout > 0
+                && qc->ctp.max_idle_timeout < qc->tp.max_idle_timeout)
+            {
+                qc->tp.max_idle_timeout = qc->ctp.max_idle_timeout;
+            }
+
+            qc->client_tp_done = 1;
+        }
+    }
+
+    frame = ngx_quic_alloc_frame(c, len);
+    if (frame == NULL) {
+        return 0;
+    }
+
+    ngx_memcpy(frame->data, data, len);
+
+    frame->level = level;
+    frame->type = NGX_QUIC_FT_CRYPTO;
+    frame->u.crypto.offset += qc->crypto_offset_out[level];
+    frame->u.crypto.len = len;
+    frame->u.crypto.data = frame->data;
+
+    qc->crypto_offset_out[level] += len;
+
+    ngx_sprintf(frame->info, "crypto, generated by SSL len=%ui level=%d", len, level);
+
+    ngx_quic_queue_frame(qc, frame);
+
+    return 1;
+}
+
+
+static int
+ngx_quic_flush_flight(ngx_ssl_conn_t *ssl_conn)
+{
+    ngx_connection_t  *c;
+
+    c = ngx_ssl_get_connection((ngx_ssl_conn_t *) ssl_conn);
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, "ngx_quic_flush_flight()");
+
+    return 1;
+}
+
+
+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_frame_t  *frame;
+
+    c = ngx_ssl_get_connection((ngx_ssl_conn_t *) ssl_conn);
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "ngx_quic_send_alert(), lvl=%d, alert=%d",
+                   (int) level, (int) alert);
+
+    frame = ngx_quic_alloc_frame(c, 0);
+    if (frame == NULL) {
+        return 0;
+    }
+
+    frame->level = level;
+    frame->type = NGX_QUIC_FT_CONNECTION_CLOSE;
+    frame->u.close.error_code = 0x100 + alert;
+    ngx_sprintf(frame->info, "cc from send_alert level=%d", frame->level);
+
+    ngx_quic_queue_frame(c->quic, frame);
+
+    if (ngx_quic_output(c) != NGX_OK) {
+        return 0;
+    }
+
+    return 1;
+}
+
+
+void
+ngx_quic_run(ngx_connection_t *c, ngx_ssl_t *ssl, ngx_quic_tp_t *tp,
+    ngx_connection_handler_pt handler)
+{
+    ngx_buf_t          *b;
+    ngx_quic_header_t   pkt;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, "quic run");
+
+    c->log->action = "QUIC initialization";
+
+    ngx_memzero(&pkt, sizeof(ngx_quic_header_t));
+
+    b = c->buffer;
+
+    pkt.log = c->log;
+    pkt.raw = b;
+    pkt.data = b->start;
+    pkt.len = b->last - b->start;
+
+    if (ngx_quic_new_connection(c, ssl, tp, &pkt, handler) != NGX_OK) {
+        ngx_quic_close_connection(c);
+        return;
+    }
+
+    ngx_add_timer(c->read, c->quic->tp.max_idle_timeout);
+
+    c->read->handler = ngx_quic_input_handler;
+
+    return;
+}
+
+
+static ngx_int_t
+ngx_quic_new_connection(ngx_connection_t *c, ngx_ssl_t *ssl, ngx_quic_tp_t *tp,
+    ngx_quic_header_t *pkt, ngx_connection_handler_pt handler)
+{
+    ngx_uint_t              i;
+    ngx_quic_tp_t          *ctp;
+    ngx_quic_secrets_t     *keys;
+    ngx_quic_connection_t  *qc;
+    static u_char           buf[NGX_QUIC_DEFAULT_MAX_PACKET_SIZE];
+
+    if (ngx_buf_size(pkt->raw) < 1200) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0, "too small UDP datagram");
+        return NGX_ERROR;
+    }
+
+    if (ngx_quic_parse_long_header(pkt) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    if (!ngx_quic_pkt_in(pkt->flags)) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "invalid initial packet: 0x%xi", pkt->flags);
+        return NGX_ERROR;
+    }
+
+    if (ngx_quic_parse_initial_header(pkt) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    c->log->action = "creating new quic connection";
+
+    qc = ngx_pcalloc(c->pool, sizeof(ngx_quic_connection_t));
+    if (qc == NULL) {
+        return NGX_ERROR;
+    }
+
+    qc->state = NGX_QUIC_ST_INITIAL;
+
+    ngx_rbtree_init(&qc->streams.tree, &qc->streams.sentinel,
+                    ngx_quic_rbtree_insert_stream);
+
+    for (i = 0; i < 3; i++) {
+        ngx_queue_init(&qc->ns[i].frames);
+        ngx_queue_init(&qc->ns[i].sent);
+     }
+
+    ngx_queue_init(&qc->free_frames);
+
+    qc->retry.log = c->log;
+    qc->retry.data = c;
+    qc->retry.handler = ngx_quic_retransmit_handler;
+    qc->retry.cancelable = 1;
+
+    qc->push.log = c->log;
+    qc->push.data = c;
+    qc->push.handler = ngx_quic_push_handler;
+    qc->push.cancelable = 1;
+
+    c->quic = qc;
+    qc->ssl = ssl;
+    qc->tp = *tp;
+    qc->streams.handler = handler;
+
+    ctp = &qc->ctp;
+    ctp->max_packet_size = NGX_QUIC_DEFAULT_MAX_PACKET_SIZE;
+    ctp->ack_delay_exponent = NGX_QUIC_DEFAULT_ACK_DELAY_EXPONENT;
+    ctp->max_ack_delay = NGX_QUIC_DEFAULT_MAX_ACK_DELAY;
+
+    qc->dcid.len = pkt->dcid.len;
+    qc->dcid.data = ngx_pnalloc(c->pool, pkt->dcid.len);
+    if (qc->dcid.data == NULL) {
+        return NGX_ERROR;
+    }
+    ngx_memcpy(qc->dcid.data, pkt->dcid.data, qc->dcid.len);
+
+    qc->scid.len = pkt->scid.len;
+    qc->scid.data = ngx_pnalloc(c->pool, qc->scid.len);
+    if (qc->scid.data == NULL) {
+        return NGX_ERROR;
+    }
+    ngx_memcpy(qc->scid.data, pkt->scid.data, qc->scid.len);
+
+    qc->token.len = pkt->token.len;
+    qc->token.data = ngx_pnalloc(c->pool, qc->token.len);
+    if (qc->token.data == NULL) {
+        return NGX_ERROR;
+    }
+    ngx_memcpy(qc->token.data, pkt->token.data, qc->token.len);
+
+    keys = &c->quic->keys[ssl_encryption_initial];
+
+    if (ngx_quic_set_initial_secret(c->pool, &keys->client, &keys->server,
+                                    &qc->dcid)
+        != NGX_OK)
+    {
+        return NGX_ERROR;
+    }
+
+    pkt->secret = &keys->client;
+    pkt->level = ssl_encryption_initial;
+    pkt->plaintext = buf;
+
+    if (ngx_quic_decrypt(pkt, NULL) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    if (pkt->pn != 0) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "invalid initial packet number %L", pkt->pn);
+        return NGX_ERROR;
+    }
+
+    if (ngx_quic_init_connection(c) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    if (ngx_quic_payload_handler(c, pkt) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    /* pos is at header end, adjust by actual packet length */
+    pkt->raw->pos += pkt->len;
+
+    return ngx_quic_input(c, pkt->raw);
+}
+
+
+static ngx_int_t
+ngx_quic_init_connection(ngx_connection_t *c)
+{
+    int                     n, sslerr;
+    u_char                 *p;
+    ssize_t                 len;
+    ngx_ssl_conn_t         *ssl_conn;
+    ngx_quic_connection_t  *qc;
+
+    qc = c->quic;
+
+    if (ngx_ssl_create_connection(qc->ssl, c, NGX_SSL_BUFFER) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    ssl_conn = c->ssl->connection;
+
+    if (SSL_set_quic_method(ssl_conn, &quic_method) == 0) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "SSL_set_quic_method() failed");
+        return NGX_ERROR;
+    }
+
+#ifdef SSL_READ_EARLY_DATA_SUCCESS
+    if (SSL_CTX_get_max_early_data(qc->ssl->ctx)) {
+        SSL_set_quic_early_data_enabled(ssl_conn, 1);
+    }
+#endif
+
+    len = ngx_quic_create_transport_params(NULL, NULL, &qc->tp);
+    /* always succeeds */
+
+    p = ngx_pnalloc(c->pool, len);
+    if (p == NULL) {
+        return NGX_ERROR;
+    }
+
+    len = ngx_quic_create_transport_params(p, p + len, &qc->tp);
+    if (len < 0) {
+        return NGX_ERROR;
+    }
+
+    if (SSL_set_quic_transport_params(ssl_conn, p, len) == 0) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "SSL_set_quic_transport_params() failed");
+        return NGX_ERROR;
+    }
+
+    qc->max_streams = qc->tp.initial_max_streams_bidi;
+    qc->state = NGX_QUIC_ST_HANDSHAKE;
+
+    n = SSL_do_handshake(ssl_conn);
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, "SSL_do_handshake: %d", n);
+
+    if (n == -1) {
+        sslerr = SSL_get_error(ssl_conn, n);
+
+        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, "SSL_get_error: %d",
+                       sslerr);
+
+        if (sslerr != SSL_ERROR_WANT_READ) {
+            ngx_ssl_error(NGX_LOG_ERR, c->log, 0, "SSL_do_handshake() failed");
+            return NGX_ERROR;
+        }
+    }
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "SSL_quic_read_level: %d, SSL_quic_write_level: %d",
+                   (int) SSL_quic_read_level(ssl_conn),
+                   (int) SSL_quic_write_level(ssl_conn));
+
+    return NGX_OK;
+}
+
+
+static void
+ngx_quic_input_handler(ngx_event_t *rev)
+{
+    ssize_t                 n;
+    ngx_buf_t               b;
+    ngx_connection_t       *c;
+    ngx_quic_connection_t  *qc;
+    static u_char           buf[NGX_QUIC_DEFAULT_MAX_PACKET_SIZE];
+
+    b.start = buf;
+    b.end = buf + sizeof(buf);
+    b.pos = b.last = b.start;
+
+    c = rev->data;
+    qc = c->quic;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, rev->log, 0, "quic input handler");
+
+    if (qc->closing) {
+        ngx_quic_close_connection(c);
+        return;
+    }
+
+    if (rev->timedout) {
+        ngx_log_error(NGX_LOG_INFO, c->log, NGX_ETIMEDOUT, "client timed out");
+        ngx_quic_close_connection(c);
+        return;
+    }
+
+    if (c->close) {
+        ngx_quic_close_connection(c);
+        return;
+    }
+
+    n = c->recv(c, b.start, b.end - b.start);
+
+    if (n == NGX_AGAIN) {
+        return;
+    }
+
+    if (n == NGX_ERROR) {
+        c->read->eof = 1;
+        ngx_quic_close_connection(c);
+        return;
+    }
+
+    b.last += n;
+
+    if (ngx_quic_input(c, &b) != NGX_OK) {
+        ngx_quic_close_connection(c);
+        return;
+    }
+
+    qc->send_timer_set = 0;
+    ngx_add_timer(rev, qc->tp.max_idle_timeout);
+}
+
+
+static void
+ngx_quic_close_connection(ngx_connection_t *c)
+{
+#if (NGX_DEBUG)
+    ngx_uint_t              ns;
+#endif
+    ngx_pool_t             *pool;
+    ngx_event_t            *rev;
+    ngx_rbtree_t           *tree;
+    ngx_rbtree_node_t      *node;
+    ngx_quic_stream_t      *qs;
+    ngx_quic_connection_t  *qc;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, "close quic connection");
+
+    qc = c->quic;
+
+    if (qc) {
+        qc->closing = 1;
+        tree = &qc->streams.tree;
+
+        if (tree->root != tree->sentinel) {
+            if (c->read->timer_set) {
+                ngx_del_timer(c->read);
+            }
+
+#if (NGX_DEBUG)
+            ns = 0;
+#endif
+
+            for (node = ngx_rbtree_min(tree->root, tree->sentinel);
+                 node;
+                 node = ngx_rbtree_next(tree, node))
+            {
+                qs = (ngx_quic_stream_t *) node;
+
+                rev = qs->c->read;
+                rev->ready = 1;
+                rev->pending_eof = 1;
+
+                ngx_post_event(rev, &ngx_posted_events);
+
+                if (rev->timer_set) {
+                    ngx_del_timer(rev);
+                }
+
+#if (NGX_DEBUG)
+                ns++;
+#endif
+            }
+
+            ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                           "quic connection has %ui active streams", ns);
+
+            return;
+        }
+
+        if (qc->push.timer_set) {
+            ngx_del_timer(&qc->push);
+        }
+
+        if (qc->retry.timer_set) {
+            ngx_del_timer(&qc->retry);
+        }
+    }
+
+    if (c->ssl) {
+        (void) ngx_ssl_shutdown(c);
+    }
+
+#if (NGX_STAT_STUB)
+    (void) ngx_atomic_fetch_add(ngx_stat_active, -1);
+#endif
+
+    c->destroyed = 1;
+
+    pool = c->pool;
+
+    ngx_close_connection(c);
+
+    ngx_destroy_pool(pool);
+}
+
+
+static ngx_int_t
+ngx_quic_input(ngx_connection_t *c, ngx_buf_t *b)
+{
+    u_char             *p;
+    ngx_int_t           rc;
+    ngx_quic_header_t   pkt;
+
+    p = b->pos;
+
+    while (p < b->last) {
+        c->log->action = "processing quic packet";
+
+        ngx_memzero(&pkt, sizeof(ngx_quic_header_t));
+        pkt.raw = b;
+        pkt.data = p;
+        pkt.len = b->last - p;
+        pkt.log = c->log;
+        pkt.flags = p[0];
+
+        if (pkt.flags == 0) {
+            /* XXX: no idea WTF is this, just ignore */
+            ngx_log_error(NGX_LOG_ALERT, c->log, 0, "FIREFOX: ZEROES");
+            break;
+        }
+
+        // TODO: check current state
+        if (ngx_quic_long_pkt(pkt.flags)) {
+
+            if (ngx_quic_pkt_in(pkt.flags)) {
+                rc = ngx_quic_initial_input(c, &pkt);
+
+            } else if (ngx_quic_pkt_hs(pkt.flags)) {
+                rc = ngx_quic_handshake_input(c, &pkt);
+
+            } else if (ngx_quic_pkt_zrtt(pkt.flags)) {
+                rc = ngx_quic_early_input(c, &pkt);
+
+            } else {
+                ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                              "BUG: unknown quic state");
+                return NGX_ERROR;
+            }
+
+        } else {
+            rc = ngx_quic_app_input(c, &pkt);
+        }
+
+        if (rc != NGX_OK) {
+            return rc;
+        }
+
+        /* b->pos is at header end, adjust by actual packet length */
+        p = b->pos + pkt.len;
+        b->pos = p;       /* reset b->pos to the next packet start */
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_initial_input(ngx_connection_t *c, ngx_quic_header_t *pkt)
+{
+    ngx_ssl_conn_t      *ssl_conn;
+    ngx_quic_secrets_t  *keys;
+    static u_char        buf[NGX_QUIC_DEFAULT_MAX_PACKET_SIZE];
+
+    c->log->action = "processing initial quic packet";
+
+    ssl_conn = c->ssl->connection;
+
+    if (ngx_quic_parse_long_header(pkt) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    if (ngx_quic_parse_initial_header(pkt) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    keys = &c->quic->keys[ssl_encryption_initial];
+
+    pkt->secret = &keys->client;
+    pkt->level = ssl_encryption_initial;
+    pkt->plaintext = buf;
+
+    if (ngx_quic_decrypt(pkt, ssl_conn) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    return ngx_quic_payload_handler(c, pkt);
+}
+
+
+static ngx_int_t
+ngx_quic_handshake_input(ngx_connection_t *c, ngx_quic_header_t *pkt)
+{
+    ngx_quic_secrets_t     *keys;
+    ngx_quic_connection_t  *qc;
+    static u_char           buf[NGX_QUIC_DEFAULT_MAX_PACKET_SIZE];
+
+    c->log->action = "processing handshake quic packet";
+
+    qc = c->quic;
+
+    keys = &c->quic->keys[ssl_encryption_handshake];
+
+    if (keys->client.key.len == 0) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "no read keys yet, packet ignored");
+        return NGX_DECLINED;
+    }
+
+    /* extract cleartext data into pkt */
+    if (ngx_quic_parse_long_header(pkt) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    if (pkt->dcid.len != qc->dcid.len) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0, "unexpected quic dcidl");
+        return NGX_ERROR;
+    }
+
+    if (ngx_memcmp(pkt->dcid.data, qc->dcid.data, qc->dcid.len) != 0) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0, "unexpected quic dcid");
+        return NGX_ERROR;
+    }
+
+    if (pkt->scid.len != qc->scid.len) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0, "unexpected quic scidl");
+        return NGX_ERROR;
+    }
+
+    if (ngx_memcmp(pkt->scid.data, qc->scid.data, qc->scid.len) != 0) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0, "unexpected quic scid");
+        return NGX_ERROR;
+    }
+
+    if (!ngx_quic_pkt_hs(pkt->flags)) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "invalid packet type: 0x%xi", pkt->flags);
+        return NGX_ERROR;
+    }
+
+    if (ngx_quic_parse_handshake_header(pkt) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    pkt->secret = &keys->client;
+    pkt->level = ssl_encryption_handshake;
+    pkt->plaintext = buf;
+
+    if (ngx_quic_decrypt(pkt, c->ssl->connection) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    return ngx_quic_payload_handler(c, pkt);
+}
+
+
+static ngx_int_t
+ngx_quic_early_input(ngx_connection_t *c, ngx_quic_header_t *pkt)
+{
+    ngx_quic_secrets_t     *keys;
+    ngx_quic_connection_t  *qc;
+    static u_char           buf[NGX_QUIC_DEFAULT_MAX_PACKET_SIZE];
+
+    c->log->action = "processing early data quic packet";
+
+    qc = c->quic;
+
+    /* extract cleartext data into pkt */
+    if (ngx_quic_parse_long_header(pkt) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    if (pkt->dcid.len != qc->dcid.len) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0, "unexpected quic dcidl");
+        return NGX_ERROR;
+    }
+
+    if (ngx_memcmp(pkt->dcid.data, qc->dcid.data, qc->dcid.len) != 0) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0, "unexpected quic dcid");
+        return NGX_ERROR;
+    }
+
+    if (pkt->scid.len != qc->scid.len) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0, "unexpected quic scidl");
+        return NGX_ERROR;
+    }
+
+    if (ngx_memcmp(pkt->scid.data, qc->scid.data, qc->scid.len) != 0) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0, "unexpected quic scid");
+        return NGX_ERROR;
+    }
+
+    if (!ngx_quic_pkt_zrtt(pkt->flags)) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "invalid packet type: 0x%xi", pkt->flags);
+        return NGX_ERROR;
+    }
+
+    if (ngx_quic_parse_handshake_header(pkt) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    if (c->quic->state != NGX_QUIC_ST_EARLY_DATA) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0, "unexpected 0-RTT packet");
+        return NGX_OK;
+    }
+
+    keys = &c->quic->keys[ssl_encryption_early_data];
+
+    pkt->secret = &keys->client;
+    pkt->level = ssl_encryption_early_data;
+    pkt->plaintext = buf;
+
+    if (ngx_quic_decrypt(pkt, c->ssl->connection) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    return ngx_quic_payload_handler(c, pkt);
+}
+
+
+static ngx_int_t
+ngx_quic_app_input(ngx_connection_t *c, ngx_quic_header_t *pkt)
+{
+    ngx_int_t               rc;
+    ngx_quic_secrets_t     *keys, *next, tmp;
+    ngx_quic_connection_t  *qc;
+    static u_char           buf[NGX_QUIC_DEFAULT_MAX_PACKET_SIZE];
+
+    c->log->action = "processing application data quic packet";
+
+    qc = c->quic;
+
+    keys = &c->quic->keys[ssl_encryption_application];
+    next = &c->quic->next_key;
+
+    if (keys->client.key.len == 0) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "no read keys yet, packet ignored");
+        return NGX_DECLINED;
+    }
+
+    if (ngx_quic_parse_short_header(pkt, &qc->dcid) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    pkt->secret = &keys->client;
+    pkt->next = &next->client;
+    pkt->key_phase = c->quic->key_phase;
+    pkt->level = ssl_encryption_application;
+    pkt->plaintext = buf;
+
+    if (ngx_quic_decrypt(pkt, c->ssl->connection) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    /* switch keys on Key Phase change */
+
+    if (pkt->key_update) {
+        c->quic->key_phase ^= 1;
+
+        tmp = *keys;
+        *keys = *next;
+        *next = tmp;
+    }
+
+    rc = ngx_quic_payload_handler(c, pkt);
+
+    if (rc == NGX_ERROR) {
+        return NGX_ERROR;
+    }
+
+    /* generate next keys */
+
+    if (pkt->key_update) {
+        if (ngx_quic_key_update(c, keys, next) != NGX_OK) {
+            return NGX_ERROR;
+        }
+    }
+
+    return rc;
+}
+
+
+static ngx_int_t
+ngx_quic_payload_handler(ngx_connection_t *c, ngx_quic_header_t *pkt)
+{
+    u_char                 *end, *p;
+    ssize_t                 len;
+    ngx_uint_t              ack_this, do_close;
+    ngx_quic_frame_t        frame, *ack_frame;
+    ngx_quic_connection_t  *qc;
+
+
+    qc = c->quic;
+
+    p = pkt->payload.data;
+    end = p + pkt->payload.len;
+
+    ack_this = 0;
+    do_close = 0;
+
+    while (p < end) {
+
+        c->log->action = "parsing frames";
+
+        len = ngx_quic_parse_frame(pkt, p, end, &frame);
+
+        if (len == NGX_DECLINED) {
+            /* TODO: handle protocol violation:
+             *       such frame not allowed in this packet
+             */
+            return NGX_ERROR;
+        }
+
+        if (len < 0) {
+            return NGX_ERROR;
+        }
+
+        c->log->action = "handling frames";
+
+        p += len;
+
+        switch (frame.type) {
+
+        case NGX_QUIC_FT_ACK:
+            if (ngx_quic_handle_ack_frame(c, pkt, &frame.u.ack) != NGX_OK) {
+                return NGX_ERROR;
+            }
+
+            break;
+
+        case NGX_QUIC_FT_CRYPTO:
+
+            if (ngx_quic_handle_crypto_frame(c, pkt, &frame.u.crypto)
+                != NGX_OK)
+            {
+                return NGX_ERROR;
+            }
+
+            ack_this = 1;
+            break;
+
+        case NGX_QUIC_FT_PADDING:
+            /* no action required */
+            break;
+
+        case NGX_QUIC_FT_PING:
+            ack_this = 1;
+            break;
+
+        case NGX_QUIC_FT_CONNECTION_CLOSE:
+        case NGX_QUIC_FT_CONNECTION_CLOSE2:
+            do_close = 1;
+            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:
+
+            if (ngx_quic_handle_stream_frame(c, pkt, &frame.u.stream)
+                != NGX_OK)
+            {
+                return NGX_ERROR;
+            }
+
+            ack_this = 1;
+            break;
+
+        case NGX_QUIC_FT_MAX_DATA:
+            c->quic->max_data = frame.u.max_data.max_data;
+            ack_this = 1;
+            break;
+
+        case NGX_QUIC_FT_STREAMS_BLOCKED:
+        case NGX_QUIC_FT_STREAMS_BLOCKED2:
+
+            if (ngx_quic_handle_streams_blocked_frame(c, pkt,
+                                                      &frame.u.streams_blocked)
+                != NGX_OK)
+            {
+                return NGX_ERROR;
+            }
+
+            ack_this = 1;
+            break;
+
+        case NGX_QUIC_FT_STREAM_DATA_BLOCKED:
+
+            if (ngx_quic_handle_stream_data_blocked_frame(c, pkt,
+                                                  &frame.u.stream_data_blocked)
+                != NGX_OK)
+            {
+                return NGX_ERROR;
+            }
+
+            ack_this = 1;
+            break;
+
+        case NGX_QUIC_FT_NEW_CONNECTION_ID:
+        case NGX_QUIC_FT_RETIRE_CONNECTION_ID:
+        case NGX_QUIC_FT_NEW_TOKEN:
+        case NGX_QUIC_FT_RESET_STREAM:
+        case NGX_QUIC_FT_STOP_SENDING:
+        case NGX_QUIC_FT_PATH_CHALLENGE:
+        case NGX_QUIC_FT_PATH_RESPONSE:
+
+            /* TODO: handle */
+            ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                           "frame handler not implemented");
+            ack_this = 1;
+            break;
+
+        default:
+            return NGX_ERROR;
+        }
+    }
+
+    if (p != end) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "trailing garbage in payload: %ui bytes", end - p);
+        return NGX_ERROR;
+    }
+
+    if (do_close) {
+        return NGX_DONE;
+    }
+
+    if (ack_this == 0) {
+        /* do not ack packets with ACKs and PADDING */
+        return NGX_OK;
+    }
+
+    c->log->action = "generating acknowledgment";
+
+    // packet processed, ACK it now if required
+    // TODO: if (ack_required) ...  - currently just ack each packet
+
+    ack_frame = ngx_quic_alloc_frame(c, 0);
+    if (ack_frame == NULL) {
+        return NGX_ERROR;
+    }
+
+    ack_frame->level = (pkt->level == ssl_encryption_early_data)
+                       ? ssl_encryption_application
+                       : pkt->level;
+
+    ack_frame->type = NGX_QUIC_FT_ACK;
+    ack_frame->u.ack.largest = pkt->pn;
+    /* only ack immediate packet ]*/
+    ack_frame->u.ack.first_range = 0;
+
+    ngx_sprintf(ack_frame->info, "ACK for PN=%d from frame handler level=%d", pkt->pn, ack_frame->level);
+    ngx_quic_queue_frame(qc, ack_frame);
+
+    // TODO: call output() after processing some special frames?
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_handle_ack_frame(ngx_connection_t *c, ngx_quic_header_t *pkt,
+    ngx_quic_ack_frame_t *ack)
+{
+    ssize_t                n;
+    u_char                *pos, *end;
+    uint64_t               gap, range;
+    ngx_uint_t             i, min, max;
+    ngx_quic_namespace_t  *ns;
+
+    ns = &c->quic->ns[ngx_quic_ns(pkt->level)];
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "ngx_quic_handle_ack_frame in namespace %d",
+                   ngx_quic_ns(pkt->level));
+
+    /*
+     * TODO: 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) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "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, ns, min, max) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    /* 13.2.3.  Receiver Tracking of ACK Frames */
+    if (ns->largest < max) {
+        ns->largest = max;
+        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "updated largest received: %ui", max);
+    }
+
+    pos = ack->ranges_start;
+    end = ack->ranges_end;
+
+    for (i = 0; i < ack->range_count; i++) {
+
+        n = ngx_quic_parse_ack_range(pkt, pos, end, &gap, &range);
+        if (n == NGX_ERROR) {
+            return NGX_ERROR;
+        }
+        pos += n;
+
+        if (gap >= min) {
+            ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                         "invalid range %ui in ack frame", i);
+            return NGX_ERROR;
+        }
+
+        max = min - 1 - gap;
+
+        if (range > max + 1) {
+            ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                         "invalid range %ui in ack frame", i);
+            return NGX_ERROR;
+        }
+
+        min = max - range + 1;
+
+        if (ngx_quic_handle_ack_frame_range(c, ns, min, max) != NGX_OK) {
+            return NGX_ERROR;
+        }
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_handle_ack_frame_range(ngx_connection_t *c, ngx_quic_namespace_t *ns,
+    uint64_t min, uint64_t max)
+{
+    ngx_uint_t         found;
+    ngx_queue_t       *q, range;
+    ngx_quic_frame_t  *f;
+
+    found = 0;
+
+    ngx_queue_init(&range);
+
+    q = ngx_queue_head(&ns->sent);
+
+    while (q != ngx_queue_sentinel(&ns->sent)) {
+
+        f = ngx_queue_data(q, ngx_quic_frame_t, queue);
+
+        if (f->pnum >= min && f->pnum <= max) {
+            q = ngx_queue_next(q);
+            ngx_queue_remove(&f->queue);
+            ngx_quic_free_frame(c, f);
+            found = 1;
+
+        } else {
+            q = ngx_queue_next(q);
+        }
+    }
+
+    if (!found) {
+
+        if (max <= ns->pnum) {
+            /* duplicate ACK or ACK for non-ack-eliciting frame */
+            return NGX_OK;
+        }
+
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "ACK for the packet not in sent queue ");
+        // TODO: handle error properly: PROTOCOL VIOLATION?
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_handle_crypto_frame(ngx_connection_t *c, ngx_quic_header_t *pkt,
+    ngx_quic_crypto_frame_t *f)
+{
+    int                     sslerr;
+    ssize_t                 n;
+    uint64_t               *curr_offset;
+    ngx_ssl_conn_t         *ssl_conn;
+    ngx_quic_connection_t  *qc;
+
+    qc = c->quic;
+
+    curr_offset = &qc->crypto_offset_in[pkt->level];
+
+    if (f->offset != *curr_offset) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "crypto frame with unexpected offset");
+
+        /* TODO: support reordering/buffering of data */
+        return NGX_ERROR;
+    }
+
+    ssl_conn = c->ssl->connection;
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "SSL_quic_read_level: %d, SSL_quic_write_level: %d",
+                   (int) SSL_quic_read_level(ssl_conn),
+                   (int) SSL_quic_write_level(ssl_conn));
+
+    if (!SSL_provide_quic_data(ssl_conn, SSL_quic_read_level(ssl_conn),
+                               f->data, f->len))
+    {
+        ngx_ssl_error(NGX_LOG_INFO, c->log, 0,
+                      "SSL_provide_quic_data() failed");
+        return NGX_ERROR;
+    }
+
+    *curr_offset += f->len;
+
+    n = SSL_do_handshake(ssl_conn);
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, "SSL_do_handshake: %d", n);
+
+    if (n == -1) {
+        sslerr = SSL_get_error(ssl_conn, n);
+
+        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, "SSL_get_error: %d",
+                       sslerr);
+
+        if (sslerr != SSL_ERROR_WANT_READ) {
+            ngx_ssl_error(NGX_LOG_ERR, c->log, 0, "SSL_do_handshake() failed");
+            return NGX_ERROR;
+        }
+
+    } else if (n == 1 && !SSL_in_init(ssl_conn)) {
+        c->quic->state = NGX_QUIC_ST_APPLICATION;
+
+        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic ssl cipher: %s", SSL_get_cipher(ssl_conn));
+
+        ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "handshake completed successfully");
+
+#if (NGX_QUIC_DRAFT_VERSION >= 27)
+        {
+        ngx_quic_frame_t  *frame;
+
+        frame = ngx_quic_alloc_frame(c, 0);
+        if (frame == NULL) {
+            return NGX_ERROR;
+        }
+
+        /* 12.4 Frames and frame types, figure 8 */
+        frame->level = ssl_encryption_application;
+        frame->type = NGX_QUIC_FT_HANDSHAKE_DONE;
+        ngx_sprintf(frame->info, "HANDSHAKE DONE on handshake completed");
+        ngx_quic_queue_frame(c->quic, frame);
+        }
+#endif
+
+        /*
+         * Generating next keys before a key update is received.
+         * See quic-tls 9.4 Header Protection Timing Side-Channels.
+         */
+
+        if (ngx_quic_key_update(c, &c->quic->keys[ssl_encryption_application],
+                                &c->quic->next_key)
+            != NGX_OK)
+        {
+            return NGX_ERROR;
+        }
+    }
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "SSL_quic_read_level: %d, SSL_quic_write_level: %d",
+                   (int) SSL_quic_read_level(ssl_conn),
+                   (int) SSL_quic_write_level(ssl_conn));
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_handle_stream_frame(ngx_connection_t *c,
+    ngx_quic_header_t *pkt, ngx_quic_stream_frame_t *f)
+{
+    size_t                  n;
+    ngx_buf_t              *b;
+    ngx_event_t            *rev;
+    ngx_quic_stream_t      *sn;
+    ngx_quic_connection_t  *qc;
+
+    qc = c->quic;
+
+    sn = ngx_quic_find_stream(&qc->streams.tree, f->stream_id);
+
+    if (sn) {
+        ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, "existing stream");
+        b = sn->b;
+
+        if ((size_t) ((b->pos - b->start) + (b->end - b->last)) < f->length) {
+            ngx_log_error(NGX_LOG_INFO, c->log, 0, "no space in stream buffer");
+            return NGX_ERROR;
+        }
+
+        if ((size_t) (b->end - b->last) < f->length) {
+            b->last = ngx_movemem(b->start, b->pos, b->last - b->pos);
+            b->pos = b->start;
+        }
+
+        b->last = ngx_cpymem(b->last, f->data, f->length);
+
+        rev = sn->c->read;
+        rev->ready = 1;
+
+        if (f->fin) {
+            rev->pending_eof = 1;
+        }
+
+        if (rev->active) {
+            rev->handler(rev);
+        }
+
+        return NGX_OK;
+    }
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, "stream is new");
+
+    n = (f->stream_id & NGX_QUIC_STREAM_UNIDIRECTIONAL)
+        ? qc->tp.initial_max_stream_data_uni
+        : qc->tp.initial_max_stream_data_bidi_remote;
+
+    if (n < NGX_QUIC_STREAM_BUFSIZE) {
+        n = NGX_QUIC_STREAM_BUFSIZE;
+    }
+
+    if (n < f->length) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0, "no space in stream buffer");
+        return NGX_ERROR;
+    }
+
+    sn = ngx_quic_create_stream(c, f->stream_id, n);
+    if (sn == NULL) {
+        return NGX_ERROR;
+    }
+
+    b = sn->b;
+    b->last = ngx_cpymem(b->last, f->data, f->length);
+
+    rev = sn->c->read;
+    rev->ready = 1;
+
+    if (f->fin) {
+        rev->pending_eof = 1;
+    }
+
+    if ((f->stream_id & NGX_QUIC_STREAM_UNIDIRECTIONAL) == 0) {
+        ngx_quic_handle_max_streams(c);
+    }
+
+    qc->streams.handler(sn->c);
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_handle_max_streams(ngx_connection_t *c)
+{
+    ngx_quic_frame_t       *frame;
+    ngx_quic_connection_t  *qc;
+
+    qc = c->quic;
+    qc->cur_streams++;
+
+    if (qc->cur_streams + NGX_QUIC_STREAMS_INC / 2 < qc->max_streams) {
+        return NGX_OK;
+    }
+
+    frame = ngx_quic_alloc_frame(c, 0);
+    if (frame == NULL) {
+        return NGX_ERROR;
+    }
+
+    qc->max_streams = ngx_max(qc->max_streams + NGX_QUIC_STREAMS_INC,
+                              NGX_QUIC_STREAMS_LIMIT);
+
+    frame->level = ssl_encryption_application;
+    frame->type = NGX_QUIC_FT_MAX_STREAMS;
+    frame->u.max_streams.limit = qc->max_streams;
+    frame->u.max_streams.bidi = 1;
+
+    ngx_sprintf(frame->info, "MAX_STREAMS limit:%d bidi:%d level=%d",
+                (int) frame->u.max_streams.limit,
+                (int) frame->u.max_streams.bidi,
+                frame->level);
+
+    ngx_quic_queue_frame(qc, frame);
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_handle_streams_blocked_frame(ngx_connection_t *c,
+    ngx_quic_header_t *pkt, ngx_quic_streams_blocked_frame_t *f)
+{
+    ngx_quic_frame_t  *frame;
+
+    frame = ngx_quic_alloc_frame(c, 0);
+    if (frame == NULL) {
+        return NGX_ERROR;
+    }
+
+    frame->level = pkt->level;
+    frame->type = NGX_QUIC_FT_MAX_STREAMS;
+    frame->u.max_streams.limit = ngx_max(f->limit * 2, NGX_QUIC_STREAMS_LIMIT);
+    frame->u.max_streams.bidi = f->bidi;
+
+    c->quic->max_streams = frame->u.max_streams.limit;
+
+    ngx_sprintf(frame->info, "MAX_STREAMS limit:%d bidi:%d level=%d",
+                (int) frame->u.max_streams.limit,
+                (int) frame->u.max_streams.bidi,
+                frame->level);
+
+    ngx_quic_queue_frame(c->quic, frame);
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_handle_stream_data_blocked_frame(ngx_connection_t *c,
+    ngx_quic_header_t *pkt, ngx_quic_stream_data_blocked_frame_t *f)
+{
+    size_t                  n;
+    ngx_buf_t              *b;
+    ngx_quic_frame_t       *frame;
+    ngx_quic_stream_t      *sn;
+    ngx_quic_connection_t  *qc;
+
+    qc = c->quic;
+    sn = ngx_quic_find_stream(&qc->streams.tree, f->id);
+
+    if (sn == NULL) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0, "unknown stream id:%uL", f->id);
+        return NGX_ERROR;
+    }
+
+    b = sn->b;
+    n = (b->pos - b->start) + (b->end - b->last);
+
+    frame = ngx_quic_alloc_frame(c, 0);
+    if (frame == NULL) {
+        return NGX_ERROR;
+    }
+
+    frame->level = pkt->level;
+    frame->type = NGX_QUIC_FT_MAX_STREAM_DATA;
+    frame->u.max_stream_data.id = f->id;
+    frame->u.max_stream_data.limit = n;
+
+    ngx_sprintf(frame->info, "MAX_STREAM_DATA id:%d limit:%d level=%d",
+                (int) frame->u.max_stream_data.id,
+                (int) frame->u.max_stream_data.limit,
+                frame->level);
+
+    ngx_quic_queue_frame(c->quic, frame);
+
+    return NGX_OK;
+}
+
+
+static void
+ngx_quic_queue_frame(ngx_quic_connection_t *qc, ngx_quic_frame_t *frame)
+{
+    ngx_quic_namespace_t  *ns;
+
+    ns = &qc->ns[ngx_quic_ns(frame->level)];
+
+    ngx_queue_insert_tail(&ns->frames, &frame->queue);
+
+    /* TODO: check PUSH flag on stream and call output */
+
+    if (!qc->push.timer_set && !qc->closing) {
+        ngx_add_timer(&qc->push, qc->tp.max_ack_delay);
+    }
+}
+
+
+static ngx_int_t
+ngx_quic_output(ngx_connection_t *c)
+{
+    ngx_uint_t              i;
+    ngx_quic_namespace_t   *ns;
+    ngx_quic_connection_t  *qc;
+
+    c->log->action = "sending frames";
+
+    qc = c->quic;
+
+    for (i = 0; i < 3; i++) {
+        ns = &qc->ns[i];
+        if (ngx_quic_output_ns(c, ns, i) != NGX_OK) {
+            return NGX_ERROR;
+        }
+    }
+
+    if (!qc->send_timer_set && !qc->closing) {
+        qc->send_timer_set = 1;
+        ngx_add_timer(c->read, qc->tp.max_idle_timeout);
+    }
+
+    if (!qc->retry.timer_set && !qc->closing) {
+        ngx_add_timer(&qc->retry, qc->tp.max_ack_delay);
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_output_ns(ngx_connection_t *c, ngx_quic_namespace_t *ns,
+    ngx_uint_t nsi)
+{
+    size_t                  len, hlen, n;
+    ngx_int_t               rc;
+    ngx_queue_t            *q, range;
+    ngx_quic_frame_t       *f;
+    ngx_quic_connection_t  *qc;
+
+    qc = c->quic;
+
+    if (ngx_queue_empty(&ns->frames)) {
+        return NGX_OK;
+    }
+
+    hlen = (nsi == 2) ? NGX_QUIC_MAX_SHORT_HEADER
+                      : NGX_QUIC_MAX_LONG_HEADER;
+
+    hlen += EVP_GCM_TLS_TAG_LEN;
+
+    q = ngx_queue_head(&ns->frames);
+
+    do {
+        len = 0;
+        ngx_queue_init(&range);
+
+        do {
+            /* process group of frames that fits into packet */
+            f = ngx_queue_data(q, ngx_quic_frame_t, queue);
+
+            n = ngx_quic_create_frame(NULL, f);
+
+            if (len && hlen + len + n > qc->ctp.max_packet_size) {
+                break;
+            }
+
+            q = ngx_queue_next(q);
+
+            f->first = ngx_current_msec;
+
+            ngx_queue_remove(&f->queue);
+            ngx_queue_insert_tail(&range, &f->queue);
+
+            len += n;
+
+        } while (q != ngx_queue_sentinel(&ns->frames));
+
+        rc = ngx_quic_send_frames(c, &range);
+
+        if (rc == NGX_OK) {
+            /*
+             * frames are moved into the sent queue
+             * to wait for ack/be retransmitted
+            */
+            ngx_queue_add(&ns->sent, &range);
+
+        } else if (rc == NGX_DONE) {
+
+            /* no ack is expected for this frames, can free them */
+            ngx_quic_free_frames(c, &range);
+
+        } else {
+            return NGX_ERROR;
+        }
+
+
+    } while (q != ngx_queue_sentinel(&ns->frames));
+
+    return NGX_OK;
+}
+
+
+static void
+ngx_quic_free_frames(ngx_connection_t *c, ngx_queue_t *frames)
+{
+    ngx_queue_t       *q;
+    ngx_quic_frame_t  *f;
+
+    q = ngx_queue_head(frames);
+
+    do {
+        f = ngx_queue_data(q, ngx_quic_frame_t, queue);
+        q = ngx_queue_next(q);
+
+        ngx_quic_free_frame(c, f);
+
+    } while (q != ngx_queue_sentinel(frames));
+}
+
+
+/* pack a group of frames [start; end) into memory p and send as single packet */
+static ngx_int_t
+ngx_quic_send_frames(ngx_connection_t *c, ngx_queue_t *frames)
+{
+    ssize_t                 len;
+    u_char                 *p;
+    ngx_msec_t              now;
+    ngx_str_t               out, res;
+    ngx_queue_t            *q;
+    ngx_quic_frame_t       *f, *start;
+    ngx_quic_header_t       pkt;
+    ngx_quic_secrets_t     *keys;
+    ngx_quic_namespace_t   *ns;
+    ngx_quic_connection_t  *qc;
+    static ngx_str_t        initial_token = ngx_null_string;
+    static u_char           src[NGX_QUIC_DEFAULT_MAX_PACKET_SIZE];
+    static u_char           dst[NGX_QUIC_DEFAULT_MAX_PACKET_SIZE];
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, "ngx_quic_send_frames");
+
+    q = ngx_queue_head(frames);
+    start = ngx_queue_data(q, ngx_quic_frame_t, queue);
+
+    ns = &c->quic->ns[ngx_quic_ns(start->level)];
+
+    ngx_memzero(&pkt, sizeof(ngx_quic_header_t));
+
+    p = src;
+    out.data = src;
+
+    for (q = ngx_queue_head(frames);
+         q != ngx_queue_sentinel(frames);
+         q = ngx_queue_next(q))
+    {
+        f = ngx_queue_data(q, ngx_quic_frame_t, queue);
+
+        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, "frame: %s", f->info);
+
+        len = ngx_quic_create_frame(p, f);
+        if (len == -1) {
+            return NGX_ERROR;
+        }
+
+        if (f->need_ack) {
+            pkt.need_ack = 1;
+        }
+
+        p += len;
+        f->pnum = ns->pnum;
+    }
+
+    if (start->level == ssl_encryption_initial) {
+        /* ack will not be sent in initial packets due to initial keys being
+         * discarded when handshake start.
+         * Thus consider initial packets as non-ack-eliciting
+         */
+        pkt.need_ack = 0;
+    }
+
+    out.len = p - out.data;
+
+    while (out.len < 4) {
+        *p++ = NGX_QUIC_FT_PADDING;
+        out.len++;
+    }
+
+    ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "packet ready: %ui bytes at level %d need_ack: %ui",
+                   out.len, start->level, pkt.need_ack);
+
+    qc = c->quic;
+
+    keys = &c->quic->keys[start->level];
+
+    pkt.secret = &keys->server;
+
+    if (start->level == ssl_encryption_initial) {
+        pkt.flags = NGX_QUIC_PKT_INITIAL;
+        pkt.token = initial_token;
+
+    } else if (start->level == ssl_encryption_handshake) {
+        pkt.flags = NGX_QUIC_PKT_HANDSHAKE;
+
+    } else {
+        // TODO: macro, set FIXED bit
+        pkt.flags = 0x40 | (c->quic->key_phase ? NGX_QUIC_PKT_KPHASE : 0);
+    }
+
+    ngx_quic_set_packet_number(&pkt, ns);
+
+    pkt.log = c->log;
+    pkt.level = start->level;
+    pkt.dcid = qc->dcid;
+    pkt.scid = qc->scid;
+    pkt.payload = out;
+
+    res.data = dst;
+
+    if (ngx_quic_encrypt(&pkt, c->ssl->connection, &res) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    ngx_quic_hexdump0(c->log, "packet to send", res.data, res.len);
+
+    len = c->send(c, res.data, res.len);
+    if (len == NGX_ERROR || (size_t) len != res.len) {
+        return NGX_ERROR;
+    }
+
+    /* len == NGX_OK || NGX_AGAIN */
+    ns->pnum++;
+
+    now = ngx_current_msec;
+    start->last = now;
+
+    return pkt.need_ack ? NGX_OK : NGX_DONE;
+}
+
+
+static void
+ngx_quic_set_packet_number(ngx_quic_header_t *pkt, ngx_quic_namespace_t *ns)
+{
+    uint64_t  delta;
+
+    delta = ns->pnum - ns->largest;
+    pkt->number = ns->pnum;
+
+    if (delta <= 0x7F) {
+        pkt->num_len = 1;
+        pkt->trunc = ns->pnum & 0xff;
+
+    } else if (delta <= 0x7FFF) {
+        pkt->num_len = 2;
+        pkt->flags |= 0x1;
+        pkt->trunc = ns->pnum & 0xffff;
+
+    } else if (delta <= 0x7FFFFF) {
+        pkt->num_len = 3;
+        pkt->flags |= 0x2;
+        pkt->trunc = ns->pnum & 0xffffff;
+
+    } else {
+        pkt->num_len = 4;
+        pkt->flags |= 0x3;
+        pkt->trunc = ns->pnum & 0xffffffff;
+    }
+}
+
+
+static void
+ngx_quic_retransmit_handler(ngx_event_t *ev)
+{
+    ngx_uint_t              i;
+    ngx_msec_t              wait, nswait;
+    ngx_connection_t       *c;
+    ngx_quic_connection_t  *qc;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, ev->log, 0,
+                   "retransmit timer");
+
+    c = ev->data;
+    qc = c->quic;
+
+    wait = 0;
+
+    for (i = 0; i < NGX_QUIC_NAMESPACE_LAST; i++) {
+        if (ngx_quic_retransmit_ns(c, &qc->ns[i], &nswait) != NGX_OK) {
+            ngx_quic_close_connection(c);
+            return;
+        }
+
+        if (i == 0) {
+            wait = nswait;
+
+        } else if (nswait > 0 && nswait < wait) {
+            wait = nswait;
+        }
+    }
+
+    if (wait > 0) {
+        ngx_add_timer(&qc->retry, wait);
+    }
+}
+
+
+static void
+ngx_quic_push_handler(ngx_event_t *ev)
+{
+    ngx_connection_t       *c;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, ev->log, 0, "push timer");
+
+    c = ev->data;
+
+    if (ngx_quic_output(c) != NGX_OK) {
+        ngx_quic_close_connection(c);
+        return;
+    }
+}
+
+
+static ngx_int_t
+ngx_quic_retransmit_ns(ngx_connection_t *c, ngx_quic_namespace_t *ns,
+    ngx_msec_t *waitp)
+{
+    uint64_t                pn;
+    ngx_msec_t              now, wait;
+    ngx_queue_t            *q, range;
+    ngx_quic_frame_t       *f, *start;
+    ngx_quic_connection_t  *qc;
+
+    qc = c->quic;
+
+    now = ngx_current_msec;
+    wait = 0;
+
+    if (ngx_queue_empty(&ns->sent)) {
+        *waitp = 0;
+        return NGX_OK;
+    }
+
+    q = ngx_queue_head(&ns->sent);
+    start = ngx_queue_data(q, ngx_quic_frame_t, queue);
+    pn = start->pnum;
+    f = start;
+
+    do {
+        ngx_queue_init(&range);
+
+        /* send frames with same packet number to the wire */
+        do {
+            f = ngx_queue_data(q, ngx_quic_frame_t, queue);
+
+            if (start->first + qc->tp.max_idle_timeout < now) {
+                ngx_log_error(NGX_LOG_ERR, c->log, 0,
+                              "retransmission timeout");
+                return NGX_DECLINED;
+            }
+
+            if (f->pnum != pn) {
+                break;
+            }
+
+            q = ngx_queue_next(q);
+
+            ngx_queue_remove(&f->queue);
+            ngx_queue_insert_tail(&range, &f->queue);
+
+        } while (q != ngx_queue_sentinel(&ns->sent));
+
+        wait = start->last + qc->tp.max_ack_delay - now;
+
+        if ((ngx_msec_int_t) wait > 0) {
+            break;
+        }
+
+        /* NGX_DONE is impossible here, such frames don't get into this queue */
+        if (ngx_quic_send_frames(c, &range) != NGX_OK) {
+            return NGX_ERROR;
+        }
+
+        /* move frames group to the end of queue */
+        ngx_queue_add(&ns->sent, &range);
+
+    } while (q != ngx_queue_sentinel(&ns->sent));
+
+    *waitp = wait;
+
+    return NGX_OK;
+}
+
+
+ngx_connection_t *
+ngx_quic_create_uni_stream(ngx_connection_t *c)
+{
+    ngx_uint_t              id;
+    ngx_quic_stream_t      *qs, *sn;
+    ngx_quic_connection_t  *qc;
+
+    qs = c->qs;
+    qc = qs->parent->quic;
+
+    id = (qc->streams.id_counter << 2)
+         | NGX_QUIC_STREAM_SERVER_INITIATED
+         | NGX_QUIC_STREAM_UNIDIRECTIONAL;
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "creating server uni stream #%ui id %ui",
+                   qc->streams.id_counter, id);
+
+    qc->streams.id_counter++;
+
+    sn = ngx_quic_create_stream(qs->parent, id, 0);
+    if (sn == NULL) {
+        return NULL;
+    }
+
+    return sn->c;
+}
+
+
+static void
+ngx_quic_rbtree_insert_stream(ngx_rbtree_node_t *temp,
+    ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel)
+{
+    ngx_rbtree_node_t  **p;
+    ngx_quic_stream_t   *qn, *qnt;
+
+    for ( ;; ) {
+        qn = (ngx_quic_stream_t *) node;
+        qnt = (ngx_quic_stream_t *) temp;
+
+        p = (qn->id < qnt->id) ? &temp->left : &temp->right;
+
+        if (*p == sentinel) {
+            break;
+        }
+
+        temp = *p;
+    }
+
+    *p = node;
+    node->parent = temp;
+    node->left = sentinel;
+    node->right = sentinel;
+    ngx_rbt_red(node);
+}
+
+
+static ngx_quic_stream_t *
+ngx_quic_find_stream(ngx_rbtree_t *rbtree, uint64_t id)
+{
+    ngx_rbtree_node_t  *node, *sentinel;
+    ngx_quic_stream_t  *qn;
+
+    node = rbtree->root;
+    sentinel = rbtree->sentinel;
+
+    while (node != sentinel) {
+        qn = (ngx_quic_stream_t *) node;
+
+        if (id == qn->id) {
+            return qn;
+        }
+
+        node = (id < qn->id) ? node->left : node->right;
+    }
+
+    return NULL;
+}
+
+
+static ngx_quic_stream_t *
+ngx_quic_create_stream(ngx_connection_t *c, uint64_t id, size_t rcvbuf_size)
+{
+    ngx_log_t           *log;
+    ngx_pool_t          *pool;
+    ngx_quic_stream_t   *sn;
+    ngx_pool_cleanup_t  *cln;
+
+    pool = ngx_create_pool(NGX_DEFAULT_POOL_SIZE, c->log);
+    if (pool == NULL) {
+        return NULL;
+    }
+
+    sn = ngx_pcalloc(pool, sizeof(ngx_quic_stream_t));
+    if (sn == NULL) {
+        ngx_destroy_pool(pool);
+        return NULL;
+    }
+
+    sn->node.key = id;
+    sn->parent = c;
+    sn->id = id;
+
+    sn->b = ngx_create_temp_buf(pool, rcvbuf_size);
+    if (sn->b == NULL) {
+        ngx_destroy_pool(pool);
+        return NULL;
+    }
+
+    log = ngx_palloc(pool, sizeof(ngx_log_t));
+    if (log == NULL) {
+        ngx_destroy_pool(pool);
+        return NULL;
+    }
+
+    *log = *c->log;
+    pool->log = log;
+
+    sn->c = ngx_get_connection(-1, log);
+    if (sn->c == NULL) {
+        ngx_destroy_pool(pool);
+        return NULL;
+    }
+
+    sn->c->qs = sn;
+    sn->c->pool = pool;
+    sn->c->ssl = c->ssl;
+    sn->c->sockaddr = c->sockaddr;
+    sn->c->listening = c->listening;
+    sn->c->addr_text = c->addr_text;
+    sn->c->local_sockaddr = c->local_sockaddr;
+    sn->c->number = ngx_atomic_fetch_add(ngx_connection_counter, 1);
+
+    sn->c->recv = ngx_quic_stream_recv;
+    sn->c->send = ngx_quic_stream_send;
+    sn->c->send_chain = ngx_quic_stream_send_chain;
+
+    sn->c->read->log = c->log;
+    sn->c->write->log = c->log;
+
+    cln = ngx_pool_cleanup_add(pool, 0);
+    if (cln == NULL) {
+        ngx_close_connection(sn->c);
+        ngx_destroy_pool(pool);
+        return NULL;
+    }
+
+    cln->handler = ngx_quic_stream_cleanup_handler;
+    cln->data = sn->c;
+
+    ngx_rbtree_insert(&c->quic->streams.tree, &sn->node);
+
+    return sn;
+}
+
+
+static ssize_t
+ngx_quic_stream_recv(ngx_connection_t *c, u_char *buf, size_t size)
+{
+    ssize_t             len;
+    ngx_buf_t          *b;
+    ngx_event_t        *rev;
+    ngx_quic_stream_t  *qs;
+
+    qs = c->qs;
+    b = qs->b;
+    rev = c->read;
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic recv: eof:%d, avail:%z",
+                   rev->pending_eof, b->last - b->pos);
+
+    if (b->pos == b->last) {
+        rev->ready = 0;
+
+        if (rev->pending_eof) {
+            rev->eof = 1;
+            return 0;
+        }
+
+        ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, "quic recv() not ready");
+        return NGX_AGAIN;
+    }
+
+    len = ngx_min(b->last - b->pos, (ssize_t) size);
+
+    ngx_memcpy(buf, b->pos, len);
+
+    b->pos += len;
+
+    if (b->pos == b->last) {
+        b->pos = b->start;
+        b->last = b->start;
+        rev->ready = rev->pending_eof;
+    }
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic recv: %z of %uz", len, size);
+
+    return len;
+}
+
+
+static ssize_t
+ngx_quic_stream_send(ngx_connection_t *c, u_char *buf, size_t size)
+{
+    ngx_connection_t       *pc;
+    ngx_quic_frame_t       *frame;
+    ngx_quic_stream_t      *qs;
+    ngx_quic_connection_t  *qc;
+
+    qs = c->qs;
+    pc = qs->parent;
+    qc = pc->quic;
+
+    if (qc->closing) {
+        return NGX_ERROR;
+    }
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, "quic send: %uz", size);
+
+    frame = ngx_quic_alloc_frame(pc, size);
+    if (frame == NULL) {
+        return 0;
+    }
+
+    ngx_memcpy(frame->data, buf, size);
+
+    frame->level = ssl_encryption_application;
+    frame->type = NGX_QUIC_FT_STREAM6; /* OFF=1 LEN=1 FIN=0 */
+    frame->u.stream.off = 1;
+    frame->u.stream.len = 1;
+    frame->u.stream.fin = 0;
+
+    frame->u.stream.type = frame->type;
+    frame->u.stream.stream_id = qs->id;
+    frame->u.stream.offset = c->sent;
+    frame->u.stream.length = size;
+    frame->u.stream.data = frame->data;
+
+    c->sent += size;
+
+    ngx_sprintf(frame->info, "stream %xi len=%ui level=%d",
+                qs->id, size, frame->level);
+
+    ngx_quic_queue_frame(qc, frame);
+
+    return size;
+}
+
+
+static void
+ngx_quic_stream_cleanup_handler(void *data)
+{
+    ngx_connection_t *c = data;
+
+    ngx_connection_t       *pc;
+    ngx_quic_frame_t       *frame;
+    ngx_quic_stream_t      *qs;
+    ngx_quic_connection_t  *qc;
+
+    qs = c->qs;
+    pc = qs->parent;
+    qc = pc->quic;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, "quic stream cleanup");
+
+    ngx_rbtree_delete(&qc->streams.tree, &qs->node);
+
+    if (qc->closing) {
+        ngx_post_event(pc->read, &ngx_posted_events);
+        return;
+    }
+
+    if ((qs->id & 0x03) == NGX_QUIC_STREAM_UNIDIRECTIONAL) {
+        /* do not send fin for client unidirectional streams */
+        return;
+    }
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, "quic send fin");
+
+    frame = ngx_quic_alloc_frame(pc, 0);
+    if (frame == NULL) {
+        return;
+    }
+
+    frame->level = ssl_encryption_application;
+    frame->type = NGX_QUIC_FT_STREAM7; /* OFF=1 LEN=1 FIN=1 */
+    frame->u.stream.off = 1;
+    frame->u.stream.len = 1;
+    frame->u.stream.fin = 1;
+
+    frame->u.stream.type = frame->type;
+    frame->u.stream.stream_id = qs->id;
+    frame->u.stream.offset = c->sent;
+    frame->u.stream.length = 0;
+    frame->u.stream.data = NULL;
+
+    ngx_sprintf(frame->info, "stream %xi fin=1 level=%d", qs->id, frame->level);
+
+    ngx_quic_queue_frame(qc, frame);
+
+    (void) ngx_quic_output(pc);
+}
+
+
+static ngx_chain_t *
+ngx_quic_stream_send_chain(ngx_connection_t *c, ngx_chain_t *in,
+    off_t limit)
+{
+    size_t      len;
+    ssize_t     n;
+    ngx_buf_t  *b;
+
+    for ( /* void */; in; in = in->next) {
+        b = in->buf;
+
+        if (!ngx_buf_in_memory(b)) {
+            continue;
+        }
+
+        if (ngx_buf_size(b) == 0) {
+            continue;
+        }
+
+        len = b->last - b->pos;
+
+        n = ngx_quic_stream_send(c, b->pos, len);
+
+        if (n == NGX_ERROR) {
+            return NGX_CHAIN_ERROR;
+        }
+
+        if (n == NGX_AGAIN) {
+            return in;
+        }
+
+        if (n != (ssize_t) len) {
+            b->pos += n;
+            return in;
+        }
+    }
+
+    return NULL;
+}
+
+
+static ngx_quic_frame_t *
+ngx_quic_alloc_frame(ngx_connection_t *c, size_t size)
+{
+    u_char                 *p;
+    ngx_queue_t            *q;
+    ngx_quic_frame_t       *frame;
+    ngx_quic_connection_t  *qc;
+
+    if (size) {
+        p = ngx_alloc(size, c->log);
+        if (p == NULL) {
+            return NULL;
+        }
+
+    } else {
+        p = NULL;
+    }
+
+    qc = c->quic;
+
+    if (!ngx_queue_empty(&qc->free_frames)) {
+
+        q = ngx_queue_head(&qc->free_frames);
+        frame = ngx_queue_data(q, ngx_quic_frame_t, queue);
+
+        ngx_queue_remove(&frame->queue);
+
+        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "reuse quic frame n:%ui", qc->nframes);
+
+    } else {
+        frame = ngx_pcalloc(c->pool, sizeof(ngx_quic_frame_t));
+        if (frame == NULL) {
+            ngx_free(p);
+            return NULL;
+        }
+
+#if (NGX_DEBUG)
+        ++qc->nframes;
+#endif
+
+        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "alloc quic frame n:%ui", qc->nframes);
+    }
+
+    ngx_memzero(frame, sizeof(ngx_quic_frame_t));
+
+    frame->data = p;
+
+    return frame;
+}
+
+
+static void
+ngx_quic_free_frame(ngx_connection_t *c, ngx_quic_frame_t *frame)
+{
+    ngx_quic_connection_t  *qc;
+
+    qc = c->quic;
+
+    if (frame->data) {
+        ngx_free(frame->data);
+    }
+
+    ngx_queue_insert_head(&qc->free_frames, &frame->queue);
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "free quic frame n:%ui", qc->nframes);
+}
new file mode 100644
--- /dev/null
+++ b/src/event/ngx_event_quic.h
@@ -0,0 +1,95 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_EVENT_QUIC_H_INCLUDED_
+#define _NGX_EVENT_QUIC_H_INCLUDED_
+
+
+#include <ngx_event_openssl.h>
+
+
+#define NGX_QUIC_DRAFT_VERSION               27
+#define NGX_QUIC_VERSION  (0xff000000 + NGX_QUIC_DRAFT_VERSION)
+
+#define NGX_QUIC_MAX_SHORT_HEADER            25
+#define NGX_QUIC_MAX_LONG_HEADER             346
+
+#define NGX_QUIC_DEFAULT_MAX_PACKET_SIZE     65527
+#define NGX_QUIC_DEFAULT_ACK_DELAY_EXPONENT  3
+#define NGX_QUIC_DEFAULT_MAX_ACK_DELAY       25
+
+#define NGX_QUIC_STREAM_SERVER_INITIATED     0x01
+#define NGX_QUIC_STREAM_UNIDIRECTIONAL       0x02
+
+#define NGX_QUIC_STREAM_BUFSIZE              16384
+
+
+typedef struct {
+    /* configurable */
+    ngx_msec_t          max_idle_timeout;
+    ngx_msec_t          max_ack_delay;
+
+    ngx_uint_t          max_packet_size;
+    ngx_uint_t          initial_max_data;
+    ngx_uint_t          initial_max_stream_data_bidi_local;
+    ngx_uint_t          initial_max_stream_data_bidi_remote;
+    ngx_uint_t          initial_max_stream_data_uni;
+    ngx_uint_t          initial_max_streams_bidi;
+    ngx_uint_t          initial_max_streams_uni;
+    ngx_uint_t          ack_delay_exponent;
+    ngx_uint_t          disable_active_migration;
+    ngx_uint_t          active_connection_id_limit;
+
+    /* TODO */
+    ngx_uint_t          original_connection_id;
+    u_char              stateless_reset_token[16];
+    void               *preferred_address;
+} ngx_quic_tp_t;
+
+
+struct ngx_quic_stream_s {
+    ngx_rbtree_node_t   node;
+    ngx_connection_t   *parent;
+    ngx_connection_t   *c;
+    uint64_t            id;
+    ngx_buf_t          *b;
+};
+
+
+void ngx_quic_run(ngx_connection_t *c, ngx_ssl_t *ssl, ngx_quic_tp_t *tp,
+    ngx_connection_handler_pt handler);
+ngx_connection_t *ngx_quic_create_uni_stream(ngx_connection_t *c);
+
+
+/********************************* DEBUG *************************************/
+
+#if (NGX_DEBUG)
+
+#define ngx_quic_hexdump(log, fmt, data, len, ...)                            \
+do {                                                                          \
+    ngx_int_t  m;                                                             \
+    u_char     buf[2048];                                                     \
+                                                                              \
+    if (log->log_level & NGX_LOG_DEBUG_EVENT) {                               \
+        m = ngx_hex_dump(buf, (u_char *) data, ngx_min(len, 1024)) - buf;     \
+        ngx_log_debug(NGX_LOG_DEBUG_EVENT, log, 0,                            \
+                   "%s: " fmt " %*s%s, len: %uz",                             \
+                   __FUNCTION__,  __VA_ARGS__, m, buf,                        \
+                   len < 2048 ? "" : "...", len);                             \
+    }                                                                         \
+} while (0)
+
+#else
+
+#define ngx_quic_hexdump(log, fmt, data, len, ...)
+
+#endif
+
+#define ngx_quic_hexdump0(log, fmt, data, len)                                \
+    ngx_quic_hexdump(log, fmt "%s", data, len, "")                            \
+
+
+#endif /* _NGX_EVENT_QUIC_H_INCLUDED_ */
new file mode 100644
--- /dev/null
+++ b/src/event/ngx_event_quic_protection.c
@@ -0,0 +1,1016 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_event.h>
+
+
+#define NGX_QUIC_IV_LEN               12
+
+#define NGX_AES_128_GCM_SHA256        0x1301
+#define NGX_AES_256_GCM_SHA384        0x1302
+#define NGX_CHACHA20_POLY1305_SHA256  0x1303
+
+
+#ifdef OPENSSL_IS_BORINGSSL
+#define ngx_quic_cipher_t             EVP_AEAD
+#else
+#define ngx_quic_cipher_t             EVP_CIPHER
+#endif
+
+typedef struct {
+    const ngx_quic_cipher_t  *c;
+    const EVP_CIPHER         *hp;
+    const EVP_MD             *d;
+} ngx_quic_ciphers_t;
+
+
+static ngx_int_t ngx_hkdf_expand(u_char *out_key, size_t out_len,
+    const EVP_MD *digest, const u_char *prk, size_t prk_len,
+    const u_char *info, size_t info_len);
+static ngx_int_t ngx_hkdf_extract(u_char *out_key, size_t *out_len,
+    const EVP_MD *digest, const u_char *secret, size_t secret_len,
+    const u_char *salt, size_t salt_len);
+
+static uint64_t ngx_quic_parse_pn(u_char **pos, ngx_int_t len, u_char *mask);
+static void ngx_quic_compute_nonce(u_char *nonce, size_t len, uint64_t pn);
+static ngx_int_t ngx_quic_ciphers(ngx_ssl_conn_t *ssl_conn,
+    ngx_quic_ciphers_t *ciphers, enum ssl_encryption_level_t level);
+
+static ngx_int_t ngx_quic_tls_open(const ngx_quic_cipher_t *cipher,
+    ngx_quic_secret_t *s, ngx_str_t *out, u_char *nonce, ngx_str_t *in,
+    ngx_str_t *ad, ngx_log_t *log);
+static ngx_int_t ngx_quic_tls_seal(const ngx_quic_cipher_t *cipher,
+    ngx_quic_secret_t *s, ngx_str_t *out, u_char *nonce, ngx_str_t *in,
+    ngx_str_t *ad, ngx_log_t *log);
+static ngx_int_t ngx_quic_tls_hp(ngx_log_t *log, const EVP_CIPHER *cipher,
+    ngx_quic_secret_t *s, u_char *out, u_char *in);
+static ngx_int_t ngx_quic_hkdf_expand(ngx_pool_t *pool, const EVP_MD *digest,
+    ngx_str_t *out, ngx_str_t *label, const uint8_t *prk, size_t prk_len);
+
+static ssize_t ngx_quic_create_long_packet(ngx_quic_header_t *pkt,
+    ngx_ssl_conn_t *ssl_conn, ngx_str_t *res);
+static ssize_t ngx_quic_create_short_packet(ngx_quic_header_t *pkt,
+    ngx_ssl_conn_t *ssl_conn, ngx_str_t *res);
+
+
+static ngx_int_t
+ngx_quic_ciphers(ngx_ssl_conn_t *ssl_conn, ngx_quic_ciphers_t *ciphers,
+    enum ssl_encryption_level_t level)
+{
+    ngx_int_t          id, len;
+    const SSL_CIPHER  *cipher;
+
+    if (level == ssl_encryption_initial) {
+        id = NGX_AES_128_GCM_SHA256;
+
+    } else {
+        cipher = SSL_get_current_cipher(ssl_conn);
+        if (cipher == NULL) {
+            return NGX_ERROR;
+        }
+
+        id = SSL_CIPHER_get_id(cipher) & 0xffff;
+    }
+
+    switch (id) {
+
+    case NGX_AES_128_GCM_SHA256:
+#ifdef OPENSSL_IS_BORINGSSL
+        ciphers->c = EVP_aead_aes_128_gcm();
+#else
+        ciphers->c = EVP_aes_128_gcm();
+#endif
+        ciphers->hp = EVP_aes_128_ctr();
+        ciphers->d = EVP_sha256();
+        len = 16;
+        break;
+
+    case NGX_AES_256_GCM_SHA384:
+#ifdef OPENSSL_IS_BORINGSSL
+        ciphers->c = EVP_aead_aes_256_gcm();
+#else
+        ciphers->c = EVP_aes_256_gcm();
+#endif
+        ciphers->hp = EVP_aes_256_ctr();
+        ciphers->d = EVP_sha384();
+        len = 32;
+        break;
+
+    case NGX_CHACHA20_POLY1305_SHA256:
+#ifdef OPENSSL_IS_BORINGSSL
+        ciphers->c = EVP_aead_chacha20_poly1305();
+#else
+        ciphers->c = EVP_chacha20_poly1305();
+#endif
+#ifdef OPENSSL_IS_BORINGSSL
+        ciphers->hp = (const EVP_CIPHER *) EVP_aead_chacha20_poly1305();
+#else
+        ciphers->hp = EVP_chacha20();
+#endif
+        ciphers->d = EVP_sha256();
+        len = 32;
+        break;
+
+    default:
+        return NGX_ERROR;
+    }
+
+    return len;
+}
+
+
+ngx_int_t
+ngx_quic_set_initial_secret(ngx_pool_t *pool, ngx_quic_secret_t *client,
+    ngx_quic_secret_t *server, ngx_str_t *secret)
+{
+    size_t             is_len;
+    uint8_t            is[SHA256_DIGEST_LENGTH];
+    ngx_uint_t         i;
+    const EVP_MD      *digest;
+    const EVP_CIPHER  *cipher;
+
+    static const uint8_t salt[20] =
+        "\xc3\xee\xf7\x12\xc7\x2e\xbb\x5a\x11\xa7"
+        "\xd2\x43\x2b\xb4\x63\x65\xbe\xf9\xf5\x02";
+
+    /* AEAD_AES_128_GCM prior to handshake, quic-tls-23#section-5.3 */
+
+    cipher = EVP_aes_128_gcm();
+    digest = EVP_sha256();
+
+    if (ngx_hkdf_extract(is, &is_len, digest, secret->data, secret->len,
+                         salt, sizeof(salt))
+        != NGX_OK)
+    {
+        return NGX_ERROR;
+    }
+
+    ngx_str_t iss = {
+        .data = is,
+        .len = is_len
+    };
+
+    ngx_quic_hexdump0(pool->log, "salt", salt, sizeof(salt));
+    ngx_quic_hexdump0(pool->log, "initial secret", is, is_len);
+
+    /* draft-ietf-quic-tls-23#section-5.2 */
+    client->secret.len = SHA256_DIGEST_LENGTH;
+    server->secret.len = SHA256_DIGEST_LENGTH;
+
+    client->key.len = EVP_CIPHER_key_length(cipher);
+    server->key.len = EVP_CIPHER_key_length(cipher);
+
+    client->hp.len = EVP_CIPHER_key_length(cipher);
+    server->hp.len = EVP_CIPHER_key_length(cipher);
+
+    client->iv.len = EVP_CIPHER_iv_length(cipher);
+    server->iv.len = EVP_CIPHER_iv_length(cipher);
+
+    struct {
+        ngx_str_t   label;
+        ngx_str_t  *key;
+        ngx_str_t  *prk;
+    } seq[] = {
+
+        /* draft-ietf-quic-tls-23#section-5.2 */
+        { ngx_string("tls13 client in"), &client->secret, &iss },
+        {
+            ngx_string("tls13 quic key"),
+            &client->key,
+            &client->secret,
+        },
+        {
+            ngx_string("tls13 quic iv"),
+            &client->iv,
+            &client->secret,
+        },
+        {
+            /* AEAD_AES_128_GCM prior to handshake, quic-tls-23#section-5.4.1 */
+            ngx_string("tls13 quic hp"),
+            &client->hp,
+            &client->secret,
+        },
+        { ngx_string("tls13 server in"), &server->secret, &iss },
+        {
+            /* AEAD_AES_128_GCM prior to handshake, quic-tls-23#section-5.3 */
+            ngx_string("tls13 quic key"),
+            &server->key,
+            &server->secret,
+        },
+        {
+            ngx_string("tls13 quic iv"),
+            &server->iv,
+            &server->secret,
+        },
+        {
+           /* AEAD_AES_128_GCM prior to handshake, quic-tls-23#section-5.4.1 */
+            ngx_string("tls13 quic hp"),
+            &server->hp,
+            &server->secret,
+        },
+
+    };
+
+    for (i = 0; i < (sizeof(seq) / sizeof(seq[0])); i++) {
+
+        if (ngx_quic_hkdf_expand(pool, digest, seq[i].key, &seq[i].label,
+                                 seq[i].prk->data, seq[i].prk->len)
+            != NGX_OK)
+        {
+            return NGX_ERROR;
+        }
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_hkdf_expand(ngx_pool_t *pool, const EVP_MD *digest, ngx_str_t *out,
+    ngx_str_t *label, const uint8_t *prk, size_t prk_len)
+{
+    size_t    info_len;
+    uint8_t  *p;
+    uint8_t   info[20];
+
+    if (out->data == NULL) {
+        out->data = ngx_pnalloc(pool, out->len);
+        if (out->data == NULL) {
+            return NGX_ERROR;
+        }
+    }
+
+    info_len = 2 + 1 + label->len + 1;
+
+    info[0] = 0;
+    info[1] = out->len;
+    info[2] = label->len;
+    p = ngx_cpymem(&info[3], label->data, label->len);
+    *p = '\0';
+
+    if (ngx_hkdf_expand(out->data, out->len, digest,
+                        prk, prk_len, info, info_len)
+        != NGX_OK)
+    {
+        ngx_ssl_error(NGX_LOG_INFO, pool->log, 0,
+                      "ngx_hkdf_expand(%V) failed", label);
+        return NGX_ERROR;
+    }
+
+    ngx_quic_hexdump(pool->log, "%V info", info, info_len, label);
+    ngx_quic_hexdump(pool->log, "%V key", out->data, out->len, label);
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_hkdf_expand(u_char *out_key, size_t out_len, const EVP_MD *digest,
+    const uint8_t *prk, size_t prk_len, const u_char *info, size_t info_len)
+{
+#ifdef OPENSSL_IS_BORINGSSL
+    if (HKDF_expand(out_key, out_len, digest, prk, prk_len, info, info_len)
+        == 0)
+    {
+        return NGX_ERROR;
+    }
+#else
+
+    EVP_PKEY_CTX  *pctx;
+
+    pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, NULL);
+
+    if (EVP_PKEY_derive_init(pctx) <= 0) {
+        return NGX_ERROR;
+    }
+
+    if (EVP_PKEY_CTX_hkdf_mode(pctx, EVP_PKEY_HKDEF_MODE_EXPAND_ONLY) <= 0) {
+        return NGX_ERROR;
+    }
+
+    if (EVP_PKEY_CTX_set_hkdf_md(pctx, digest) <= 0) {
+        return NGX_ERROR;
+    }
+
+    if (EVP_PKEY_CTX_set1_hkdf_key(pctx, prk, prk_len) <= 0) {
+        return NGX_ERROR;
+    }
+
+    if (EVP_PKEY_CTX_add1_hkdf_info(pctx, info, info_len) <= 0) {
+        return NGX_ERROR;
+    }
+
+    if (EVP_PKEY_derive(pctx, out_key, &out_len) <= 0) {
+        return NGX_ERROR;
+    }
+
+#endif
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_hkdf_extract(u_char *out_key, size_t *out_len, const EVP_MD *digest,
+    const u_char *secret, size_t secret_len, const u_char *salt,
+    size_t salt_len)
+{
+#ifdef OPENSSL_IS_BORINGSSL
+    if (HKDF_extract(out_key, out_len, digest, secret, secret_len, salt,
+                     salt_len)
+        == 0)
+    {
+        return NGX_ERROR;
+    }
+#else
+
+    EVP_PKEY_CTX  *pctx;
+
+    pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, NULL);
+
+    if (EVP_PKEY_derive_init(pctx) <= 0) {
+        return NGX_ERROR;
+    }
+
+    if (EVP_PKEY_CTX_hkdf_mode(pctx, EVP_PKEY_HKDEF_MODE_EXTRACT_ONLY) <= 0) {
+        return NGX_ERROR;
+    }
+
+    if (EVP_PKEY_CTX_set_hkdf_md(pctx, digest) <= 0) {
+        return NGX_ERROR;
+    }
+
+    if (EVP_PKEY_CTX_set1_hkdf_key(pctx, secret, secret_len) <= 0) {
+        return NGX_ERROR;
+    }
+
+    if (EVP_PKEY_CTX_set1_hkdf_salt(pctx, salt, salt_len) <= 0) {
+        return NGX_ERROR;
+    }
+
+    if (EVP_PKEY_derive(pctx, out_key, out_len) <= 0) {
+        return NGX_ERROR;
+    }
+
+#endif
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_tls_open(const ngx_quic_cipher_t *cipher, ngx_quic_secret_t *s,
+    ngx_str_t *out, u_char *nonce, ngx_str_t *in, ngx_str_t *ad,
+    ngx_log_t *log)
+{
+
+#ifdef OPENSSL_IS_BORINGSSL
+    EVP_AEAD_CTX  *ctx;
+
+    ctx = EVP_AEAD_CTX_new(cipher, s->key.data, s->key.len,
+                           EVP_AEAD_DEFAULT_TAG_LENGTH);
+    if (ctx == NULL) {
+        ngx_ssl_error(NGX_LOG_INFO, log, 0, "EVP_AEAD_CTX_new() failed");
+        return NGX_ERROR;
+    }
+
+    if (EVP_AEAD_CTX_open(ctx, out->data, &out->len, out->len, nonce, s->iv.len,
+                          in->data, in->len, ad->data, ad->len)
+        != 1)
+    {
+        EVP_AEAD_CTX_free(ctx);
+        ngx_ssl_error(NGX_LOG_INFO, log, 0, "EVP_AEAD_CTX_open() failed");
+        return NGX_ERROR;
+    }
+
+    EVP_AEAD_CTX_free(ctx);
+#else
+    int              len;
+    u_char          *tag;
+    EVP_CIPHER_CTX  *ctx;
+
+    ctx = EVP_CIPHER_CTX_new();
+    if (ctx == NULL) {
+        ngx_ssl_error(NGX_LOG_INFO, log, 0, "EVP_CIPHER_CTX_new() failed");
+        return NGX_ERROR;
+    }
+
+    if (EVP_DecryptInit_ex(ctx, cipher, NULL, NULL, NULL) != 1) {
+        EVP_CIPHER_CTX_free(ctx);
+        ngx_ssl_error(NGX_LOG_INFO, log, 0, "EVP_DecryptInit_ex() failed");
+        return NGX_ERROR;
+    }
+
+    if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, s->iv.len, NULL)
+        == 0)
+    {
+        EVP_CIPHER_CTX_free(ctx);
+        ngx_ssl_error(NGX_LOG_INFO, log, 0,
+                      "EVP_CIPHER_CTX_ctrl(EVP_CTRL_GCM_SET_IVLEN) failed");
+        return NGX_ERROR;
+    }
+
+    if (EVP_DecryptInit_ex(ctx, NULL, NULL, s->key.data, nonce) != 1) {
+        EVP_CIPHER_CTX_free(ctx);
+        ngx_ssl_error(NGX_LOG_INFO, log, 0, "EVP_DecryptInit_ex() failed");
+        return NGX_ERROR;
+    }
+
+    if (EVP_DecryptUpdate(ctx, NULL, &len, ad->data, ad->len) != 1) {
+        EVP_CIPHER_CTX_free(ctx);
+        ngx_ssl_error(NGX_LOG_INFO, log, 0, "EVP_DecryptUpdate() failed");
+        return NGX_ERROR;
+    }
+
+    if (EVP_DecryptUpdate(ctx, out->data, &len, in->data,
+                          in->len - EVP_GCM_TLS_TAG_LEN)
+        != 1)
+    {
+        EVP_CIPHER_CTX_free(ctx);
+        ngx_ssl_error(NGX_LOG_INFO, log, 0, "EVP_DecryptUpdate() failed");
+        return NGX_ERROR;
+    }
+
+    out->len = len;
+    tag = in->data + in->len - EVP_GCM_TLS_TAG_LEN;
+
+    if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, EVP_GCM_TLS_TAG_LEN, tag)
+        == 0)
+    {
+        EVP_CIPHER_CTX_free(ctx);
+        ngx_ssl_error(NGX_LOG_INFO, log, 0,
+                      "EVP_CIPHER_CTX_ctrl(EVP_CTRL_GCM_SET_TAG) failed");
+        return NGX_ERROR;
+    }
+
+    if (EVP_DecryptFinal_ex(ctx, out->data + len, &len) <= 0) {
+        EVP_CIPHER_CTX_free(ctx);
+        ngx_ssl_error(NGX_LOG_INFO, log, 0, "EVP_DecryptFinal_ex failed");
+        return NGX_ERROR;
+    }
+
+    out->len += len;
+
+    EVP_CIPHER_CTX_free(ctx);
+#endif
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_tls_seal(const ngx_quic_cipher_t *cipher, ngx_quic_secret_t *s,
+    ngx_str_t *out, u_char *nonce, ngx_str_t *in, ngx_str_t *ad, ngx_log_t *log)
+{
+
+#ifdef OPENSSL_IS_BORINGSSL
+    EVP_AEAD_CTX  *ctx;
+
+    ctx = EVP_AEAD_CTX_new(cipher, s->key.data, s->key.len,
+                           EVP_AEAD_DEFAULT_TAG_LENGTH);
+    if (ctx == NULL) {
+        ngx_ssl_error(NGX_LOG_INFO, log, 0, "EVP_AEAD_CTX_new() failed");
+        return NGX_ERROR;
+    }
+
+    if (EVP_AEAD_CTX_seal(ctx, out->data, &out->len, out->len, nonce, s->iv.len,
+                          in->data, in->len, ad->data, ad->len)
+        != 1)
+    {
+        EVP_AEAD_CTX_free(ctx);
+        ngx_ssl_error(NGX_LOG_INFO, log, 0, "EVP_AEAD_CTX_seal() failed");
+        return NGX_ERROR;
+    }
+
+    EVP_AEAD_CTX_free(ctx);
+#else
+    int              len;
+    EVP_CIPHER_CTX  *ctx;
+
+    ctx = EVP_CIPHER_CTX_new();
+    if (ctx == NULL) {
+        ngx_ssl_error(NGX_LOG_INFO, log, 0, "EVP_CIPHER_CTX_new() failed");
+        return NGX_ERROR;
+    }
+
+    if (EVP_EncryptInit_ex(ctx, cipher, NULL, NULL, NULL) != 1) {
+        EVP_CIPHER_CTX_free(ctx);
+        ngx_ssl_error(NGX_LOG_INFO, log, 0, "EVP_EncryptInit_ex() failed");
+        return NGX_ERROR;
+    }
+
+    if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, s->iv.len, NULL)
+        == 0)
+    {
+        EVP_CIPHER_CTX_free(ctx);
+        ngx_ssl_error(NGX_LOG_INFO, log, 0,
+                      "EVP_CIPHER_CTX_ctrl(EVP_CTRL_GCM_SET_IVLEN) failed");
+        return NGX_ERROR;
+    }
+
+    if (EVP_EncryptInit_ex(ctx, NULL, NULL, s->key.data, nonce) != 1) {
+        EVP_CIPHER_CTX_free(ctx);
+        ngx_ssl_error(NGX_LOG_INFO, log, 0, "EVP_EncryptInit_ex() failed");
+        return NGX_ERROR;
+    }
+
+    if (EVP_EncryptUpdate(ctx, NULL, &len, ad->data, ad->len) != 1) {
+        EVP_CIPHER_CTX_free(ctx);
+        ngx_ssl_error(NGX_LOG_INFO, log, 0, "EVP_EncryptUpdate() failed");
+        return NGX_ERROR;
+    }
+
+    if (EVP_EncryptUpdate(ctx, out->data, &len, in->data, in->len) != 1) {
+        EVP_CIPHER_CTX_free(ctx);
+        ngx_ssl_error(NGX_LOG_INFO, log, 0, "EVP_EncryptUpdate() failed");
+        return NGX_ERROR;
+    }
+
+    out->len = len;
+
+    if (EVP_EncryptFinal_ex(ctx, out->data + out->len, &len) <= 0) {
+        EVP_CIPHER_CTX_free(ctx);
+        ngx_ssl_error(NGX_LOG_INFO, log, 0, "EVP_EncryptFinal_ex failed");
+        return NGX_ERROR;
+    }
+
+    out->len += len;
+
+    if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, EVP_GCM_TLS_TAG_LEN,
+                            out->data + in->len)
+        == 0)
+    {
+        EVP_CIPHER_CTX_free(ctx);
+        ngx_ssl_error(NGX_LOG_INFO, log, 0,
+                      "EVP_CIPHER_CTX_ctrl(EVP_CTRL_GCM_GET_TAG) failed");
+        return NGX_ERROR;
+    }
+
+    EVP_CIPHER_CTX_free(ctx);
+
+    out->len += EVP_GCM_TLS_TAG_LEN;
+#endif
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_tls_hp(ngx_log_t *log, const EVP_CIPHER *cipher,
+    ngx_quic_secret_t *s, u_char *out, u_char *in)
+{
+    int              outlen;
+    EVP_CIPHER_CTX  *ctx;
+    u_char           zero[5] = {0};
+
+#ifdef OPENSSL_IS_BORINGSSL
+    uint32_t counter;
+
+    ngx_memcpy(&counter, in, sizeof(uint32_t));
+
+    if (cipher == (const EVP_CIPHER *) EVP_aead_chacha20_poly1305()) {
+        CRYPTO_chacha_20(out, zero, 5, s->hp.data, &in[4], counter);
+        return NGX_OK;
+    }
+#endif
+
+    ctx = EVP_CIPHER_CTX_new();
+    if (ctx == NULL) {
+        return NGX_ERROR;
+    }
+
+    if (EVP_EncryptInit_ex(ctx, cipher, NULL, s->hp.data, in) != 1) {
+        ngx_ssl_error(NGX_LOG_INFO, log, 0, "EVP_EncryptInit_ex() failed");
+        goto failed;
+    }
+
+    if (!EVP_EncryptUpdate(ctx, out, &outlen, zero, 5)) {
+        ngx_ssl_error(NGX_LOG_INFO, log, 0, "EVP_EncryptUpdate() failed");
+        goto failed;
+    }
+
+    if (!EVP_EncryptFinal_ex(ctx, out + 5, &outlen)) {
+        ngx_ssl_error(NGX_LOG_INFO, log, 0, "EVP_EncryptFinal_Ex() failed");
+        goto failed;
+    }
+
+    EVP_CIPHER_CTX_free(ctx);
+
+    return NGX_OK;
+
+failed:
+
+    EVP_CIPHER_CTX_free(ctx);
+
+    return NGX_ERROR;
+}
+
+
+int
+ngx_quic_set_encryption_secret(ngx_pool_t *pool, ngx_ssl_conn_t *ssl_conn,
+    enum ssl_encryption_level_t level, const uint8_t *secret,
+    size_t secret_len, ngx_quic_secret_t *peer_secret)
+{
+    ngx_int_t           key_len;
+    ngx_uint_t          i;
+    ngx_quic_ciphers_t  ciphers;
+
+    key_len = ngx_quic_ciphers(ssl_conn, &ciphers, level);
+
+    if (key_len == NGX_ERROR) {
+        ngx_ssl_error(NGX_LOG_INFO, pool->log, 0, "unexpected cipher");
+        return 0;
+    }
+
+    if (level == ssl_encryption_initial) {
+        return 0;
+    }
+
+    peer_secret->secret.data = ngx_pnalloc(pool, secret_len);
+    if (peer_secret->secret.data == NULL) {
+        return NGX_ERROR;
+    }
+
+    peer_secret->secret.len = secret_len;
+    ngx_memcpy(peer_secret->secret.data, secret, secret_len);
+
+    peer_secret->key.len = key_len;
+    peer_secret->iv.len = NGX_QUIC_IV_LEN;
+    peer_secret->hp.len = key_len;
+
+    struct {
+        ngx_str_t       label;
+        ngx_str_t      *key;
+        const uint8_t  *secret;
+    } seq[] = {
+        { ngx_string("tls13 quic key"), &peer_secret->key, secret },
+        { ngx_string("tls13 quic iv"),  &peer_secret->iv,  secret },
+        { ngx_string("tls13 quic hp"),  &peer_secret->hp,  secret },
+    };
+
+    for (i = 0; i < (sizeof(seq) / sizeof(seq[0])); i++) {
+
+        if (ngx_quic_hkdf_expand(pool, ciphers.d, seq[i].key, &seq[i].label,
+                                 seq[i].secret, secret_len)
+            != NGX_OK)
+        {
+            return 0;
+        }
+    }
+
+    return 1;
+}
+
+
+ngx_int_t
+ngx_quic_key_update(ngx_connection_t *c, ngx_quic_secrets_t *current,
+    ngx_quic_secrets_t *next)
+{
+    ngx_uint_t          i;
+    ngx_quic_ciphers_t  ciphers;
+
+    ngx_log_debug(NGX_LOG_DEBUG_EVENT, c->log, 0, "quic key update");
+
+    if (ngx_quic_ciphers(c->ssl->connection, &ciphers,
+                         ssl_encryption_application)
+        == NGX_ERROR)
+    {
+        return NGX_ERROR;
+    }
+
+    next->client.secret.len = current->client.secret.len;
+    next->client.key.len = current->client.key.len;
+    next->client.iv.len = current->client.iv.len;
+    next->client.hp = current->client.hp;
+
+    next->server.secret.len = current->server.secret.len;
+    next->server.key.len = current->server.key.len;
+    next->server.iv.len = current->server.iv.len;
+    next->server.hp = current->server.hp;
+
+    struct {
+        ngx_str_t   label;
+        ngx_str_t  *key;
+        ngx_str_t  *secret;
+    } seq[] = {
+        {
+            ngx_string("tls13 quic ku"),
+            &next->client.secret,
+            &current->client.secret,
+        },
+        {
+            ngx_string("tls13 quic key"),
+            &next->client.key,
+            &next->client.secret,
+        },
+        {
+            ngx_string("tls13 quic iv"),
+            &next->client.iv,
+            &next->client.secret,
+        },
+        {
+            ngx_string("tls13 quic ku"),
+            &next->server.secret,
+            &current->server.secret,
+        },
+        {
+            ngx_string("tls13 quic key"),
+            &next->server.key,
+            &next->server.secret,
+        },
+        {
+            ngx_string("tls13 quic iv"),
+            &next->server.iv,
+            &next->server.secret,
+        },
+    };
+
+    for (i = 0; i < (sizeof(seq) / sizeof(seq[0])); i++) {
+
+        if (ngx_quic_hkdf_expand(c->pool, ciphers.d, seq[i].key, &seq[i].label,
+                                 seq[i].secret->data, seq[i].secret->len)
+            != NGX_OK)
+        {
+            return NGX_ERROR;
+        }
+    }
+
+    return NGX_OK;
+}
+
+
+static ssize_t
+ngx_quic_create_long_packet(ngx_quic_header_t *pkt, ngx_ssl_conn_t *ssl_conn,
+    ngx_str_t *res)
+{
+    u_char              *pnp, *sample;
+    ngx_str_t            ad, out;
+    ngx_uint_t           i;
+    ngx_quic_ciphers_t   ciphers;
+    u_char               nonce[12], mask[16];
+
+    out.len = pkt->payload.len + EVP_GCM_TLS_TAG_LEN;
+
+    ad.data = res->data;
+    ad.len = ngx_quic_create_long_header(pkt, ad.data, out.len, &pnp);
+
+    out.data = res->data + ad.len;
+
+    ngx_quic_hexdump0(pkt->log, "ad", ad.data, ad.len);
+
+    if (ngx_quic_ciphers(ssl_conn, &ciphers, pkt->level) == NGX_ERROR) {
+        return NGX_ERROR;
+    }
+
+    ngx_log_debug3(NGX_LOG_DEBUG_EVENT, pkt->log, 0,
+                   "ngx_quic_create_long_packet: number %L, encoded %d:0x%xD",
+                   pkt->number, (int) pkt->num_len, pkt->trunc);
+
+    ngx_memcpy(nonce, pkt->secret->iv.data, pkt->secret->iv.len);
+    ngx_quic_compute_nonce(nonce, sizeof(nonce), pkt->number);
+
+    ngx_quic_hexdump0(pkt->log, "server_iv", pkt->secret->iv.data, 12);
+    ngx_quic_hexdump0(pkt->log, "nonce", nonce, 12);
+
+    if (ngx_quic_tls_seal(ciphers.c, pkt->secret, &out,
+                          nonce, &pkt->payload, &ad, pkt->log)
+        != NGX_OK)
+    {
+        return NGX_ERROR;
+    }
+
+    sample = &out.data[4 - pkt->num_len];
+    if (ngx_quic_tls_hp(pkt->log, ciphers.hp, pkt->secret, mask, sample)
+        != NGX_OK)
+    {
+        return NGX_ERROR;
+    }
+
+    ngx_quic_hexdump0(pkt->log, "sample", sample, 16);
+    ngx_quic_hexdump0(pkt->log, "mask", mask, 5);
+
+    /* quic-tls: 5.4.1.  Header Protection Application */
+    ad.data[0] ^= mask[0] & 0x0f;
+
+    for (i = 0; i < pkt->num_len; i++) {
+        pnp[i] ^= mask[i + 1];
+    }
+
+    res->len = ad.len + out.len;
+
+    return NGX_OK;
+}
+
+
+static ssize_t
+ngx_quic_create_short_packet(ngx_quic_header_t *pkt, ngx_ssl_conn_t *ssl_conn,
+    ngx_str_t *res)
+{
+    u_char              *pnp, *sample;
+    ngx_str_t            ad, out;
+    ngx_uint_t           i;
+    ngx_quic_ciphers_t   ciphers;
+    u_char               nonce[12], mask[16];
+
+    out.len = pkt->payload.len + EVP_GCM_TLS_TAG_LEN;
+
+    ad.data = res->data;
+    ad.len = ngx_quic_create_short_header(pkt, ad.data, out.len, &pnp);
+
+    out.data = res->data + ad.len;
+
+    ngx_quic_hexdump0(pkt->log, "ad", ad.data, ad.len);
+
+    if (ngx_quic_ciphers(ssl_conn, &ciphers, pkt->level) == NGX_ERROR) {
+        return NGX_ERROR;
+    }
+
+    ngx_log_debug3(NGX_LOG_DEBUG_EVENT, pkt->log, 0,
+                   "ngx_quic_create_short_packet: number %L, encoded %d:0x%xD",
+                   pkt->number, (int) pkt->num_len, pkt->trunc);
+
+    ngx_memcpy(nonce, pkt->secret->iv.data, pkt->secret->iv.len);
+    ngx_quic_compute_nonce(nonce, sizeof(nonce), pkt->number);
+
+    ngx_quic_hexdump0(pkt->log, "server_iv", pkt->secret->iv.data, 12);
+    ngx_quic_hexdump0(pkt->log, "nonce", nonce, 12);
+
+    if (ngx_quic_tls_seal(ciphers.c, pkt->secret, &out,
+                          nonce, &pkt->payload, &ad, pkt->log)
+        != NGX_OK)
+    {
+        return NGX_ERROR;
+    }
+
+    sample = &out.data[4 - pkt->num_len];
+    if (ngx_quic_tls_hp(pkt->log, ciphers.hp, pkt->secret, mask, sample)
+        != NGX_OK)
+    {
+        return NGX_ERROR;
+    }
+
+    ngx_quic_hexdump0(pkt->log, "sample", sample, 16);
+    ngx_quic_hexdump0(pkt->log, "mask", mask, 5);
+
+    /* quic-tls: 5.4.1.  Header Protection Application */
+    ad.data[0] ^= mask[0] & 0x1f;
+
+    for (i = 0; i < pkt->num_len; i++) {
+        pnp[i] ^= mask[i + 1];
+    }
+
+    res->len = ad.len + out.len;
+
+    return NGX_OK;
+}
+
+
+static uint64_t
+ngx_quic_parse_pn(u_char **pos, ngx_int_t len, u_char *mask)
+{
+    u_char    *p;
+    uint64_t   value;
+
+    p = *pos;
+    value = *p++ ^ *mask++;
+
+    while (--len) {
+        value = (value << 8) + (*p++ ^ *mask++);
+    }
+
+    *pos = p;
+    return value;
+}
+
+
+static void
+ngx_quic_compute_nonce(u_char *nonce, size_t len, uint64_t pn)
+{
+    nonce[len - 4] ^= (pn & 0xff000000) >> 24;
+    nonce[len - 3] ^= (pn & 0x00ff0000) >> 16;
+    nonce[len - 2] ^= (pn & 0x0000ff00) >> 8;
+    nonce[len - 1] ^= (pn & 0x000000ff);
+}
+
+
+ssize_t
+ngx_quic_encrypt(ngx_quic_header_t *pkt, ngx_ssl_conn_t *ssl_conn,
+    ngx_str_t *res)
+{
+    if (pkt->level == ssl_encryption_application) {
+        return ngx_quic_create_short_packet(pkt, ssl_conn, res);
+    }
+
+    return ngx_quic_create_long_packet(pkt, ssl_conn, res);
+}
+
+
+ngx_int_t
+ngx_quic_decrypt(ngx_quic_header_t *pkt, ngx_ssl_conn_t *ssl_conn)
+{
+    u_char               clearflags, *p, *sample;
+    uint64_t             pn;
+    ngx_int_t            pnl, rc, key_phase;
+    ngx_str_t            in, ad;
+    ngx_quic_secret_t   *secret;
+    ngx_quic_ciphers_t   ciphers;
+    uint8_t              mask[16], nonce[12];
+
+    if (ngx_quic_ciphers(ssl_conn, &ciphers, pkt->level) == NGX_ERROR) {
+        return NGX_ERROR;
+    }
+
+    secret = pkt->secret;
+
+    p = pkt->raw->pos;
+
+    /* draft-ietf-quic-tls-23#section-5.4.2:
+     * the Packet Number field is assumed to be 4 bytes long
+     * draft-ietf-quic-tls-23#section-5.4.[34]:
+     * AES-Based and ChaCha20-Based header protections sample 16 bytes
+     */
+
+    sample = p + 4;
+
+    ngx_quic_hexdump0(pkt->log, "sample", sample, 16);
+
+    /* header protection */
+
+    if (ngx_quic_tls_hp(pkt->log, ciphers.hp, secret, mask, sample)
+        != NGX_OK)
+    {
+        return NGX_ERROR;
+    }
+
+    if (ngx_quic_long_pkt(pkt->flags)) {
+        clearflags = pkt->flags ^ (mask[0] & 0x0f);
+
+    } else {
+        clearflags = pkt->flags ^ (mask[0] & 0x1f);
+        key_phase = (clearflags & NGX_QUIC_PKT_KPHASE) != 0;
+
+        if (key_phase != pkt->key_phase) {
+            secret = pkt->next;
+            pkt->key_update = 1;
+        }
+    }
+
+    pnl = (clearflags & 0x03) + 1;
+    pn = ngx_quic_parse_pn(&p, pnl, &mask[1]);
+
+    pkt->pn = pn;
+
+    ngx_quic_hexdump0(pkt->log, "mask", mask, 5);
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, pkt->log, 0,
+                   "quic clear flags: %xi", clearflags);
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, pkt->log, 0,
+                   "quic packet number: %uL, len: %xi", pn, pnl);
+
+    /* packet protection */
+
+    in.data = p;
+
+    if (ngx_quic_long_pkt(pkt->flags)) {
+        in.len = pkt->len - pnl;
+
+    } else {
+        in.len = pkt->data + pkt->len - p;
+    }
+
+    ad.len = p - pkt->data;
+    ad.data = pkt->plaintext;
+
+    ngx_memcpy(ad.data, pkt->data, ad.len);
+    ad.data[0] = clearflags;
+
+    do {
+        ad.data[ad.len - pnl] = pn >> (8 * (pnl - 1)) % 256;
+    } while (--pnl);
+
+    ngx_memcpy(nonce, secret->iv.data, secret->iv.len);
+    ngx_quic_compute_nonce(nonce, sizeof(nonce), pn);
+
+    ngx_quic_hexdump0(pkt->log, "nonce", nonce, 12);
+    ngx_quic_hexdump0(pkt->log, "ad", ad.data, ad.len);
+
+    pkt->payload.len = in.len - EVP_GCM_TLS_TAG_LEN;
+
+    if (NGX_QUIC_DEFAULT_MAX_PACKET_SIZE - ad.len < pkt->payload.len) {
+        return NGX_ERROR;
+    }
+
+    pkt->payload.data = pkt->plaintext + ad.len;
+
+    rc = ngx_quic_tls_open(ciphers.c, secret, &pkt->payload,
+                           nonce, &in, &ad, pkt->log);
+
+    ngx_quic_hexdump0(pkt->log, "packet payload",
+                      pkt->payload.data, pkt->payload.len);
+
+    return rc;
+}
+
new file mode 100644
--- /dev/null
+++ b/src/event/ngx_event_quic_protection.h
@@ -0,0 +1,45 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_EVENT_QUIC_PROTECTION_H_INCLUDED_
+#define _NGX_EVENT_QUIC_PROTECTION_H_INCLUDED_
+
+
+#define NGX_QUIC_ENCRYPTION_LAST  ((ssl_encryption_application) + 1)
+
+
+typedef struct ngx_quic_secret_s {
+    ngx_str_t                 secret;
+    ngx_str_t                 key;
+    ngx_str_t                 iv;
+    ngx_str_t                 hp;
+} ngx_quic_secret_t;
+
+
+typedef struct {
+    ngx_quic_secret_t         client;
+    ngx_quic_secret_t         server;
+} ngx_quic_secrets_t;
+
+
+ngx_int_t ngx_quic_set_initial_secret(ngx_pool_t *pool,
+    ngx_quic_secret_t *client, ngx_quic_secret_t *server,
+    ngx_str_t *secret);
+
+int ngx_quic_set_encryption_secret(ngx_pool_t *pool, ngx_ssl_conn_t *ssl_conn,
+    enum ssl_encryption_level_t level, const uint8_t *secret, size_t secret_len,
+    ngx_quic_secret_t *peer_secret);
+
+ngx_int_t ngx_quic_key_update(ngx_connection_t *c,
+    ngx_quic_secrets_t *current, ngx_quic_secrets_t *next);
+
+ssize_t ngx_quic_encrypt(ngx_quic_header_t *pkt, ngx_ssl_conn_t *ssl_conn,
+     ngx_str_t *res);
+
+ngx_int_t ngx_quic_decrypt(ngx_quic_header_t *pkt, ngx_ssl_conn_t *ssl_conn);
+
+
+#endif /* _NGX_EVENT_QUIC_PROTECTION_H_INCLUDED_ */
new file mode 100644
--- /dev/null
+++ b/src/event/ngx_event_quic_transport.c
@@ -0,0 +1,1749 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_event.h>
+
+
+#if (NGX_HAVE_NONALIGNED)
+
+#define ngx_quic_parse_uint16(p)  ntohs(*(uint16_t *) (p))
+#define ngx_quic_parse_uint32(p)  ntohl(*(uint32_t *) (p))
+
+#define ngx_quic_write_uint16  ngx_quic_write_uint16_aligned
+#define ngx_quic_write_uint32  ngx_quic_write_uint32_aligned
+
+#else
+
+#define ngx_quic_parse_uint16(p)  ((p)[0] << 8 | (p)[1])
+#define ngx_quic_parse_uint32(p)                                              \
+    ((uint32_t) (p)[0] << 24 | (p)[1] << 16 | (p)[2] << 8 | (p)[3])
+
+#define ngx_quic_write_uint16(p, s)                                           \
+    ((p)[0] = (u_char) ((s) >> 8),                                            \
+     (p)[1] = (u_char)  (s),                                                  \
+     (p) + sizeof(uint16_t))
+
+#define ngx_quic_write_uint32(p, s)                                           \
+    ((p)[0] = (u_char) ((s) >> 24),                                           \
+     (p)[1] = (u_char) ((s) >> 16),                                           \
+     (p)[2] = (u_char) ((s) >> 8),                                            \
+     (p)[3] = (u_char)  (s),                                                  \
+     (p) + sizeof(uint32_t))
+
+#endif
+
+#define ngx_quic_write_uint24(p, s)                                           \
+    ((p)[0] = (u_char) ((s) >> 16),                                           \
+     (p)[1] = (u_char) ((s) >> 8),                                            \
+     (p)[2] = (u_char)  (s),                                                  \
+     (p) + 3)
+
+#define ngx_quic_write_uint16_aligned(p, s)                                   \
+    (*(uint16_t *) (p) = htons((uint16_t) (s)), (p) + sizeof(uint16_t))
+
+#define ngx_quic_write_uint32_aligned(p, s)                                   \
+    (*(uint32_t *) (p) = htonl((uint32_t) (s)), (p) + sizeof(uint32_t))
+
+#define ngx_quic_varint_len(value)                                            \
+     ((value) <= 63 ? 1                                                       \
+     : ((uint32_t) value) <= 16383 ? 2                                        \
+     : ((uint64_t) value) <= 1073741823 ?  4                                  \
+     : 8)
+
+
+static u_char *ngx_quic_parse_int(u_char *pos, u_char *end, uint64_t *out);
+static u_char *ngx_quic_parse_int_multi(u_char *pos, u_char *end, ...);
+static void ngx_quic_build_int(u_char **pos, uint64_t value);
+
+static u_char *ngx_quic_read_uint8(u_char *pos, u_char *end, uint8_t *value);
+/*static*/ u_char *ngx_quic_read_uint16(u_char *pos, u_char *end, uint16_t *value); // usage depends on NGX_QUIC_VERSION
+static u_char *ngx_quic_read_uint32(u_char *pos, u_char *end, uint32_t *value);
+static u_char *ngx_quic_read_bytes(u_char *pos, u_char *end, size_t len,
+    u_char **out);
+static u_char *ngx_quic_copy_bytes(u_char *pos, u_char *end, size_t len,
+    u_char *dst);
+
+static size_t ngx_quic_create_ack(u_char *p, ngx_quic_ack_frame_t *ack);
+static size_t ngx_quic_create_crypto(u_char *p,
+    ngx_quic_crypto_frame_t *crypto);
+static size_t ngx_quic_create_hs_done(u_char *p);
+static size_t ngx_quic_create_stream(u_char *p, ngx_quic_stream_frame_t *sf);
+static size_t ngx_quic_create_max_streams(u_char *p,
+    ngx_quic_max_streams_frame_t *ms);
+static size_t ngx_quic_create_max_stream_data(u_char *p,
+    ngx_quic_max_stream_data_frame_t *ms);
+static size_t ngx_quic_create_close(u_char *p, ngx_quic_close_frame_t *cl);
+
+static ngx_int_t ngx_quic_parse_transport_param(u_char *p, u_char *end,
+    uint16_t id, ngx_quic_tp_t *dst);
+
+
+/* literal errors indexed by corresponding value */
+static char *ngx_quic_errors[] = {
+    "NO_ERROR",
+    "INTERNAL_ERROR",
+    "SERVER_BUSY",
+    "FLOW_CONTROL_ERROR",
+    "STREAM_LIMIT_ERROR",
+    "STREAM_STATE_ERROR",
+    "FINAL_SIZE_ERROR",
+    "FRAME_ENCODING_ERROR",
+    "TRANSPORT_PARAMETER_ERROR",
+    "CONNECTION_ID_LIMIT_ERROR",
+    "PROTOCOL_VIOLATION",
+    "INVALID_TOKEN",
+    "",
+    "CRYPTO_BUFFER_EXCEEDED",
+    "",
+    "CRYPTO_ERROR",
+};
+
+
+static ngx_inline u_char *
+ngx_quic_parse_int(u_char *pos, u_char *end, uint64_t *out)
+{
+    u_char      *p;
+    uint64_t     value;
+    ngx_uint_t   len;
+
+    if (pos >= end) {
+        printf("OOPS >=\n");
+        return NULL;
+    }
+
+    p = pos;
+    len = 1 << ((*p & 0xc0) >> 6);
+
+    value = *p++ & 0x3f;
+
+    if ((size_t)(end - p) < (len - 1)) {
+        printf("LEN TOO BIG: need %ld have %ld\n", len, end - p);
+        return NULL;
+    }
+
+    while (--len) {
+        value = (value << 8) + *p++;
+    }
+
+    *out = value;
+
+    return p;
+}
+
+
+static ngx_inline u_char *
+ngx_quic_parse_int_multi(u_char *pos, u_char *end, ...)
+{
+    u_char    *p;
+    va_list    ap;
+    uint64_t  *item;
+
+    p = pos;
+
+    va_start(ap, end);
+
+    do {
+        item = va_arg(ap, uint64_t *);
+        if (item == NULL) {
+            break;
+        }
+
+        p = ngx_quic_parse_int(p, end, item);
+        if (p == NULL) {
+            return NULL;
+        }
+
+    } while (1);
+
+    va_end(ap);
+
+    return p;
+}
+
+
+static ngx_inline u_char *
+ngx_quic_read_uint8(u_char *pos, u_char *end, uint8_t *value)
+{
+    if ((size_t)(end - pos) < 1) {
+        return NULL;
+    }
+
+    *value = *pos;
+
+    return pos + 1;
+}
+
+
+/*static*/ ngx_inline u_char *
+ngx_quic_read_uint16(u_char *pos, u_char *end, uint16_t *value)
+{
+    if ((size_t)(end - pos) < sizeof(uint16_t)) {
+        return NULL;
+    }
+
+    *value = ngx_quic_parse_uint16(pos);
+
+    return pos + sizeof(uint16_t);
+}
+
+
+static ngx_inline u_char *
+ngx_quic_read_uint32(u_char *pos, u_char *end, uint32_t *value)
+{
+    if ((size_t)(end - pos) < sizeof(uint32_t)) {
+        return NULL;
+    }
+
+    *value = ngx_quic_parse_uint32(pos);
+
+    return pos + sizeof(uint32_t);
+}
+
+
+static ngx_inline u_char *
+ngx_quic_read_bytes(u_char *pos, u_char *end, size_t len, u_char **out)
+{
+    if ((size_t)(end - pos) < len) {
+        return NULL;
+    }
+
+    *out = pos;
+
+    return pos + len;
+}
+
+
+static u_char *
+ngx_quic_copy_bytes(u_char *pos, u_char *end, size_t len, u_char *dst)
+{
+    if ((size_t)(end - pos) < len) {
+        return NULL;
+    }
+
+    ngx_memcpy(dst, pos, len);
+
+    return pos + len;
+}
+
+
+static void
+ngx_quic_build_int(u_char **pos, uint64_t value)
+{
+    u_char      *p;
+    ngx_uint_t   len;//, len2;
+
+    p = *pos;
+    len = 0;
+
+    while (value >> ((1 << len) * 8 - 2)) {
+        len++;
+    }
+
+    *p = len << 6;
+
+//    len2 =
+    len = (1 << len);
+    len--;
+    *p |= value >> (len * 8);
+    p++;
+
+    while (len) {
+        *p++ = value >> ((len-- - 1) * 8);
+    }
+
+    *pos = p;
+//    return len2;
+}
+
+
+u_char *
+ngx_quic_error_text(uint64_t error_code)
+{
+    return (u_char *) ngx_quic_errors[error_code];
+}
+
+
+ngx_int_t
+ngx_quic_parse_long_header(ngx_quic_header_t *pkt)
+{
+    u_char  *p, *end;
+    uint8_t  idlen;
+
+    p = pkt->data;
+    end = pkt->data + pkt->len;
+
+    ngx_quic_hexdump0(pkt->log, "long input", pkt->data, pkt->len);
+
+    p = ngx_quic_read_uint8(p, end, &pkt->flags);
+    if (p == NULL) {
+        ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                      "packet is too small to read flags");
+        return NGX_ERROR;
+    }
+
+    if (!ngx_quic_long_pkt(pkt->flags)) {
+        ngx_log_error(NGX_LOG_INFO, pkt->log, 0, "not a long packet");
+        return NGX_ERROR;
+    }
+
+    p = ngx_quic_read_uint32(p, end, &pkt->version);
+    if (p == NULL) {
+        ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                      "packet is too small to read version");
+        return NGX_ERROR;
+    }
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, pkt->log, 0,
+                   "quic flags:%xi version:%xD", pkt->flags, pkt->version);
+
+    if (pkt->version != NGX_QUIC_VERSION) {
+        ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                      "unsupported quic version: 0x%xi", pkt->version);
+        return NGX_ERROR;
+    }
+
+    p = ngx_quic_read_uint8(p, end, &idlen);
+    if (p == NULL) {
+        ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                      "packet is too small to read dcid len");
+        return NGX_ERROR;
+    }
+
+    pkt->dcid.len = idlen;
+
+    p = ngx_quic_read_bytes(p, end, idlen, &pkt->dcid.data);
+    if (p == NULL) {
+        ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                      "packet is too small to read dcid");
+        return NGX_ERROR;
+    }
+
+    p = ngx_quic_read_uint8(p, end, &idlen);
+    if (p == NULL) {
+        ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                      "packet is too small to read scid len");
+        return NGX_ERROR;
+    }
+
+    pkt->scid.len = idlen;
+
+    p = ngx_quic_read_bytes(p, end, idlen, &pkt->scid.data);
+    if (p == NULL) {
+        ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                      "packet is too small to read scid");
+        return NGX_ERROR;
+    }
+
+    pkt->raw->pos = p;
+
+    return NGX_OK;
+}
+
+
+size_t
+ngx_quic_create_long_header(ngx_quic_header_t *pkt, u_char *out,
+    size_t pkt_len, u_char **pnp)
+{
+    u_char  *p, *start;
+
+    p = start = out;
+
+    *p++ = pkt->flags;
+
+    p = ngx_quic_write_uint32(p, NGX_QUIC_VERSION);
+
+    *p++ = pkt->scid.len;
+    p = ngx_cpymem(p, pkt->scid.data, pkt->scid.len);
+
+    *p++ = pkt->dcid.len;
+    p = ngx_cpymem(p, pkt->dcid.data, pkt->dcid.len);
+
+    if (pkt->level == ssl_encryption_initial) {
+        ngx_quic_build_int(&p, pkt->token.len);
+    }
+
+    ngx_quic_build_int(&p, pkt_len + pkt->num_len);
+
+    *pnp = p;
+
+    switch (pkt->num_len) {
+    case 1:
+        *p++ = pkt->trunc;
+        break;
+    case 2:
+        p = ngx_quic_write_uint16(p, pkt->trunc);
+        break;
+    case 3:
+        p = ngx_quic_write_uint24(p, pkt->trunc);
+        break;
+    case 4:
+        p = ngx_quic_write_uint32(p, pkt->trunc);
+        break;
+    }
+
+    return p - start;
+}
+
+
+size_t
+ngx_quic_create_short_header(ngx_quic_header_t *pkt, u_char *out,
+    size_t pkt_len, u_char **pnp)
+{
+    u_char  *p, *start;
+
+    p = start = out;
+
+    *p++ = pkt->flags;
+
+    p = ngx_cpymem(p, pkt->scid.data, pkt->scid.len);
+
+    *pnp = p;
+
+    switch (pkt->num_len) {
+    case 1:
+        *p++ = pkt->trunc;
+        break;
+    case 2:
+        p = ngx_quic_write_uint16(p, pkt->trunc);
+        break;
+    case 3:
+        p = ngx_quic_write_uint24(p, pkt->trunc);
+        break;
+    case 4:
+        p = ngx_quic_write_uint32(p, pkt->trunc);
+        break;
+    }
+
+    return p - start;
+}
+
+
+ngx_int_t
+ngx_quic_parse_short_header(ngx_quic_header_t *pkt, ngx_str_t *dcid)
+{
+    u_char  *p, *end;
+
+    p = pkt->data;
+    end = pkt->data + pkt->len;
+
+    ngx_quic_hexdump0(pkt->log, "short input", pkt->data, pkt->len);
+
+    p = ngx_quic_read_uint8(p, end, &pkt->flags);
+    if (p == NULL) {
+        ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                      "packet is too small to read flags");
+        return NGX_ERROR;
+    }
+
+    if (!ngx_quic_short_pkt(pkt->flags)) {
+        ngx_log_error(NGX_LOG_INFO, pkt->log, 0, "not a short packet");
+        return NGX_ERROR;
+    }
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, pkt->log, 0,
+                   "quic flags:%xi", pkt->flags);
+
+    if (ngx_memcmp(p, dcid->data, dcid->len) != 0) {
+        ngx_log_error(NGX_LOG_INFO, pkt->log, 0, "unexpected quic dcid");
+        return NGX_ERROR;
+    }
+
+    pkt->dcid.len = dcid->len;
+
+    p = ngx_quic_read_bytes(p, end, dcid->len, &pkt->dcid.data);
+    if (p == NULL) {
+        ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                      "packet is too small to read dcid");
+        return NGX_ERROR;
+    }
+
+    pkt->raw->pos = p;
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_quic_parse_initial_header(ngx_quic_header_t *pkt)
+{
+    u_char    *p, *end;
+    uint64_t   varint;
+
+    p = pkt->raw->pos;
+
+    end = pkt->raw->last;
+
+    pkt->log->action = "parsing quic initial header";
+
+    p = ngx_quic_parse_int(p, end, &varint);
+    if (p == NULL) {
+        ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                      "failed to parse token length");
+        return NGX_ERROR;
+    }
+
+    pkt->token.len = varint;
+
+    p = ngx_quic_read_bytes(p, end, pkt->token.len, &pkt->token.data);
+    if (p == NULL) {
+        ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                      "packet too small to read token data");
+        return NGX_ERROR;
+    }
+
+    p = ngx_quic_parse_int(p, end, &varint);
+    if (p == NULL) {
+        ngx_log_error(NGX_LOG_INFO, pkt->log, 0, "bad packet length");
+        return NGX_ERROR;
+    }
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, pkt->log, 0,
+                   "quic packet length: %uL", varint);
+
+    if (varint > (uint64_t) ((pkt->data + pkt->len) - p)) {
+        ngx_log_error(NGX_LOG_INFO, pkt->log, 0, "truncated initial packet");
+        return NGX_ERROR;
+    }
+
+    pkt->raw->pos = p;
+    pkt->len = varint;
+
+    ngx_quic_hexdump0(pkt->log, "DCID", pkt->dcid.data, pkt->dcid.len);
+    ngx_quic_hexdump0(pkt->log, "SCID", pkt->scid.data, pkt->scid.len);
+    ngx_quic_hexdump0(pkt->log, "token", pkt->token.data, pkt->token.len);
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_quic_parse_handshake_header(ngx_quic_header_t *pkt)
+{
+    u_char    *p, *end;
+    uint64_t   plen;
+
+    p = pkt->raw->pos;
+    end = pkt->raw->last;
+
+    pkt->log->action = "parsing quic handshake header";
+
+    p = ngx_quic_parse_int(p, end, &plen);
+    if (p == NULL) {
+        ngx_log_error(NGX_LOG_INFO, pkt->log, 0, "bad packet length");
+        return NGX_ERROR;
+    }
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, pkt->log, 0,
+                   "quic packet length: %uL", plen);
+
+    if (plen > (uint64_t)((pkt->data + pkt->len) - p)) {
+        ngx_log_error(NGX_LOG_INFO, pkt->log, 0, "truncated handshake packet");
+        return NGX_ERROR;
+    }
+
+    pkt->raw->pos = p;
+    pkt->len = plen;
+
+    return NGX_OK;
+}
+
+
+#define ngx_quic_stream_bit_off(val)  (((val) & 0x04) ? 1 : 0)
+#define ngx_quic_stream_bit_len(val)  (((val) & 0x02) ? 1 : 0)
+#define ngx_quic_stream_bit_fin(val)  (((val) & 0x01) ? 1 : 0)
+
+ssize_t
+ngx_quic_parse_frame(ngx_quic_header_t *pkt, u_char *start, u_char *end,
+    ngx_quic_frame_t *f)
+{
+    u_char      *p;
+    uint8_t      flags;
+    uint64_t     varint;
+    ngx_uint_t   i;
+
+    flags = pkt->flags;
+    p = start;
+
+    p = ngx_quic_parse_int(p, end, &varint);
+    if (p == NULL) {
+        ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                     "failed to obtain quic frame type");
+        return NGX_ERROR;
+    }
+
+    f->type = varint;
+
+    switch (f->type) {
+
+    case NGX_QUIC_FT_CRYPTO:
+
+        if (ngx_quic_pkt_zrtt(flags)) {
+            goto not_allowed;
+        }
+
+        p = ngx_quic_parse_int(p, end, &f->u.crypto.offset);
+        if (p == NULL) {
+            ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                          "failed to parse crypto frame offset");
+            return NGX_ERROR;
+        }
+
+        p = ngx_quic_parse_int(p, end, &f->u.crypto.len);
+        if (p == NULL) {
+            ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                          "failed to parse crypto frame len");
+            return NGX_ERROR;
+        }
+
+        p = ngx_quic_read_bytes(p, end, f->u.crypto.len, &f->u.crypto.data);
+        if (p == NULL) {
+            ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                          "failed to parse crypto frame data");
+            return NGX_ERROR;
+        }
+
+        ngx_log_debug3(NGX_LOG_DEBUG_EVENT, pkt->log, 0,
+                       "quic CRYPTO frame length: %uL off:%uL pp:%p",
+                       f->u.crypto.len, f->u.crypto.offset,
+                       f->u.crypto.data);
+
+        ngx_quic_hexdump0(pkt->log, "CRYPTO frame contents",
+                          f->u.crypto.data, f->u.crypto.len);
+        break;
+
+    case NGX_QUIC_FT_PADDING:
+
+        /* allowed in any packet type */
+
+        while (p < end && *p == NGX_QUIC_FT_PADDING) {
+            p++;
+        }
+
+        break;
+
+    case NGX_QUIC_FT_ACK:
+    case NGX_QUIC_FT_ACK_ECN:
+
+        if (ngx_quic_pkt_zrtt(flags)) {
+            goto not_allowed;
+        }
+
+        p = ngx_quic_parse_int_multi(p, end, &f->u.ack.largest,
+                                     &f->u.ack.delay, &f->u.ack.range_count,
+                                     &f->u.ack.first_range, NULL);
+        if (p == NULL) {
+            ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                          "failed to parse ack frame");
+            return NGX_ERROR;
+        }
+
+        f->u.ack.ranges_start = p;
+
+        /* process all ranges to get bounds, values are ignored */
+        for (i = 0; i < f->u.ack.range_count; i++) {
+            p = ngx_quic_parse_int_multi(p, end, &varint, &varint, NULL);
+            if (p == NULL) {
+                ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                              "failed to parse ack frame range %ui", i);
+                return NGX_ERROR;
+            }
+        }
+
+        f->u.ack.ranges_end = p;
+
+        ngx_log_debug4(NGX_LOG_DEBUG_EVENT, pkt->log, 0,
+                       "ACK: { largest=%ui delay=%ui count=%ui first=%ui}",
+                       f->u.ack.largest,
+                       f->u.ack.delay,
+                       f->u.ack.range_count,
+                       f->u.ack.first_range);
+
+        if (f->type == NGX_QUIC_FT_ACK_ECN) {
+
+            p = ngx_quic_parse_int_multi(p, end, &f->u.ack.ect0,
+                                         &f->u.ack.ect1, &f->u.ack.ce, NULL);
+            if (p == NULL) {
+                ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                              "failed to parse ack frame ECT counts", i);
+                return NGX_ERROR;
+            }
+
+            ngx_log_debug3(NGX_LOG_DEBUG_EVENT, pkt->log, 0,
+                           "ACK ECN counters: %ui %ui %ui",
+                           f->u.ack.ect0, f->u.ack.ect1, f->u.ack.ce);
+        }
+
+        break;
+
+    case NGX_QUIC_FT_PING:
+
+        /* allowed in any packet type */
+
+        break;
+
+    case NGX_QUIC_FT_NEW_CONNECTION_ID:
+
+        if (!(ngx_quic_short_pkt(flags) || ngx_quic_pkt_zrtt(flags))) {
+            goto not_allowed;
+        }
+
+        p = ngx_quic_parse_int_multi(p, end, &f->u.ncid.seqnum,
+                                     &f->u.ncid.retire, NULL);
+        if (p == NULL) {
+            ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                          "failed to parse new connection id frame");
+            return NGX_ERROR;
+        }
+
+        p = ngx_quic_read_uint8(p, end, &f->u.ncid.len);
+        if (p == NULL) {
+            ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                          "failed to parse new connection id length");
+            return NGX_ERROR;
+        }
+
+        p = ngx_quic_copy_bytes(p, end, f->u.ncid.len, f->u.ncid.cid);
+        if (p == NULL) {
+            ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                          "failed to parse new connection id cid");
+            return NGX_ERROR;
+        }
+
+        p = ngx_quic_copy_bytes(p, end, 16, f->u.ncid.srt);
+        if (p == NULL) {
+            ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                          "failed to parse new connection id srt");
+            return NGX_ERROR;
+        }
+
+        ngx_log_debug3(NGX_LOG_DEBUG_EVENT, pkt->log, 0,
+                       "NCID: { seq=%ui retire=%ui len=%ui}",
+                       f->u.ncid.seqnum, f->u.ncid.retire, f->u.ncid.len);
+        break;
+
+    case NGX_QUIC_FT_CONNECTION_CLOSE2:
+
+        if (!ngx_quic_short_pkt(flags)) {
+            goto not_allowed;
+        }
+
+        /* fall through */
+
+    case NGX_QUIC_FT_CONNECTION_CLOSE:
+
+        if (ngx_quic_pkt_zrtt(flags)) {
+            goto not_allowed;
+        }
+
+        p = ngx_quic_parse_int(p, end, &f->u.close.error_code);
+        if (p == NULL) {
+            ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                          "failed to parse close connection frame error code");
+            return NGX_ERROR;
+        }
+
+        if (f->type == NGX_QUIC_FT_CONNECTION_CLOSE) {
+            p = ngx_quic_parse_int(p, end, &f->u.close.frame_type);
+            if (p == NULL) {
+                ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                              "failed to parse close connection frame type");
+                return NGX_ERROR;
+            }
+        }
+
+        p = ngx_quic_parse_int(p, end, &varint);
+        if (p == NULL) {
+            ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                          "failed to parse close reason length");
+            return NGX_ERROR;
+        }
+
+        f->u.close.reason.len = varint;
+
+        p = ngx_quic_read_bytes(p, end, f->u.close.reason.len,
+                                &f->u.close.reason.data);
+        if (p == NULL) {
+            ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                          "failed to parse close reason");
+            return NGX_ERROR;
+        }
+
+        if (f->type == NGX_QUIC_FT_CONNECTION_CLOSE) {
+
+            if (f->u.close.error_code >= NGX_QUIC_ERR_LAST) {
+                ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                              "unkown error code: %ui, truncated",
+                              f->u.close.error_code);
+                f->u.close.error_code = NGX_QUIC_ERR_LAST - 1;
+            }
+
+            ngx_log_debug4(NGX_LOG_DEBUG_EVENT, pkt->log, 0,
+                          "CONN.CLOSE: { %s (0x%xi) type=0x%xi reason='%V'}",
+                           ngx_quic_error_text(f->u.close.error_code),
+                           f->u.close.error_code, f->u.close.frame_type,
+                           &f->u.close.reason);
+        } else {
+
+            ngx_log_debug2(NGX_LOG_DEBUG_EVENT, pkt->log, 0,
+                          "CONN.CLOSE2: { (0x%xi) reason '%V'}",
+                           f->u.close.error_code, &f->u.close.reason);
+        }
+
+        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:
+
+        if (!(ngx_quic_short_pkt(flags) || ngx_quic_pkt_zrtt(flags))) {
+            goto not_allowed;
+        }
+
+        f->u.stream.type = f->type;
+
+        f->u.stream.off = ngx_quic_stream_bit_off(f->type);
+        f->u.stream.len = ngx_quic_stream_bit_len(f->type);
+        f->u.stream.fin = ngx_quic_stream_bit_fin(f->type);
+
+        p = ngx_quic_parse_int(p, end, &f->u.stream.stream_id);
+        if (p == NULL) {
+            ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                          "failed to parse stream frame id");
+            return NGX_ERROR;
+        }
+
+        if (f->type & 0x04) {
+            p = ngx_quic_parse_int(p, end, &f->u.stream.offset);
+            if (p == NULL) {
+                 ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                               "failed to parse stream frame offset");
+                return NGX_ERROR;
+            }
+
+        } else {
+            f->u.stream.offset = 0;
+        }
+
+        if (f->type & 0x02) {
+            p = ngx_quic_parse_int(p, end, &f->u.stream.length);
+            if (p == NULL) {
+                ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                              "failed to parse stream frame length");
+                return NGX_ERROR;
+            }
+
+        } else {
+            f->u.stream.length = end - p; /* up to packet end */
+        }
+
+        p = ngx_quic_read_bytes(p, end, f->u.stream.length,
+                                &f->u.stream.data);
+        if (p == NULL) {
+            ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                          "failed to parse stream frame data len=%ui "
+                          "offset=%ui", f->u.stream.length, f->u.stream.offset);
+            return NGX_ERROR;
+        }
+
+        ngx_log_debug7(NGX_LOG_DEBUG_EVENT, pkt->log, 0,
+                       "STREAM frame { 0x%xi id 0x%xi offset 0x%xi "
+                       "len 0x%xi bits:off=%d len=%d fin=%d }",
+                       f->type, f->u.stream.stream_id, f->u.stream.offset,
+                       f->u.stream.length, f->u.stream.off, f->u.stream.len,
+                       f->u.stream.fin);
+
+            ngx_quic_hexdump0(pkt->log, "STREAM frame contents",
+                              f->u.stream.data, f->u.stream.length);
+        break;
+
+    case NGX_QUIC_FT_MAX_DATA:
+
+        if (!(ngx_quic_short_pkt(flags) || ngx_quic_pkt_zrtt(flags))) {
+            goto not_allowed;
+        }
+
+        p = ngx_quic_parse_int(p, end, &f->u.max_data.max_data);
+        if (p == NULL) {
+            ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                          "failed to parse max data frame");
+            return NGX_ERROR;
+        }
+
+        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, pkt->log, 0,
+                       "MAX_DATA frame { Maximum Data %ui }",
+                       f->u.max_data.max_data);
+        break;
+
+    case NGX_QUIC_FT_RESET_STREAM:
+
+        if (!(ngx_quic_short_pkt(flags) || ngx_quic_pkt_zrtt(flags))) {
+            goto not_allowed;
+        }
+
+        p = ngx_quic_parse_int_multi(p, end, &f->u.reset_stream.id,
+                                     &f->u.reset_stream.error_code,
+                                     &f->u.reset_stream.final_size, NULL);
+        if (p == NULL) {
+            ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                          "failed to parse reset stream frame");
+            return NGX_ERROR;
+        }
+
+        ngx_log_debug3(NGX_LOG_DEBUG_EVENT, pkt->log, 0,
+                       "RESET STREAM frame"
+                       " { id 0x%xi error_code 0x%xi final_size 0x%xi }",
+                       f->u.reset_stream.id, f->u.reset_stream.error_code,
+                       f->u.reset_stream.final_size);
+        break;
+
+    case NGX_QUIC_FT_STOP_SENDING:
+
+        if (!(ngx_quic_short_pkt(flags) || ngx_quic_pkt_zrtt(flags))) {
+            goto not_allowed;
+        }
+
+        p = ngx_quic_parse_int_multi(p, end, &f->u.stop_sending.id,
+                                     &f->u.stop_sending.error_code, NULL);
+        if (p == NULL) {
+            ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                          "failed to parse stop sending frame");
+            return NGX_ERROR;
+        }
+
+        ngx_log_debug2(NGX_LOG_DEBUG_EVENT, pkt->log, 0,
+                       "STOP SENDING frame { id 0x%xi error_code 0x%xi}",
+                       f->u.stop_sending.id, f->u.stop_sending.error_code);
+
+        break;
+
+    case NGX_QUIC_FT_STREAMS_BLOCKED:
+    case NGX_QUIC_FT_STREAMS_BLOCKED2:
+
+        if (!(ngx_quic_short_pkt(flags) || ngx_quic_pkt_zrtt(flags))) {
+            goto not_allowed;
+        }
+
+        p = ngx_quic_parse_int(p, end, &f->u.streams_blocked.limit);
+        if (p == NULL) {
+            ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                          "failed to parse streams blocked frame limit");
+            return NGX_ERROR;
+        }
+
+        f->u.streams_blocked.bidi =
+                              (f->type == NGX_QUIC_FT_STREAMS_BLOCKED) ? 1 : 0;
+
+        ngx_log_debug2(NGX_LOG_DEBUG_EVENT, pkt->log, 0,
+                       "STREAMS BLOCKED frame { limit %ui bidi: %d }",
+                       f->u.streams_blocked.limit,
+                       f->u.streams_blocked.bidi);
+
+        break;
+
+    case NGX_QUIC_FT_HANDSHAKE_DONE:
+        /* only sent by server, not by client */
+        goto not_allowed;
+
+    case NGX_QUIC_FT_NEW_TOKEN:
+
+        if (!ngx_quic_short_pkt(flags)) {
+            goto not_allowed;
+        }
+
+        /* TODO: implement */
+
+        ngx_log_error(NGX_LOG_ALERT, pkt->log, 0,
+                      "unimplemented frame type 0x%xi in packet", f->type);
+
+        break;
+
+    case NGX_QUIC_FT_MAX_STREAMS:
+    case NGX_QUIC_FT_MAX_STREAMS2:
+
+        if (!(ngx_quic_short_pkt(flags) || ngx_quic_pkt_zrtt(flags))) {
+            goto not_allowed;
+        }
+
+        p = ngx_quic_parse_int(p, end, &f->u.max_streams.limit);
+        if (p == NULL) {
+            ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                          "failed to parse max streams frame limit");
+            return NGX_ERROR;
+        }
+
+        f->u.max_streams.bidi = (f->type == NGX_QUIC_FT_MAX_STREAMS) ? 1 : 0;
+
+        ngx_log_debug2(NGX_LOG_DEBUG_EVENT, pkt->log, 0,
+                       "MAX STREAMS frame { limit %ui bidi: %d }",
+                       f->u.max_streams.limit,
+                       f->u.max_streams.bidi);
+        break;
+
+    case NGX_QUIC_FT_MAX_STREAM_DATA:
+
+        if (!(ngx_quic_short_pkt(flags) || ngx_quic_pkt_zrtt(flags))) {
+            goto not_allowed;
+        }
+
+        p = ngx_quic_parse_int_multi(p, end, &f->u.max_stream_data.id,
+                                     &f->u.max_stream_data.limit, NULL);
+        if (p == NULL) {
+            ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                          "failed to parse max stream data frame");
+            return NGX_ERROR;
+        }
+
+        ngx_log_debug2(NGX_LOG_DEBUG_EVENT, pkt->log, 0,
+                       "MAX STREAM DATA frame { id: %ui limit: %ui }",
+                       f->u.max_stream_data.id,
+                       f->u.max_stream_data.limit);
+        break;
+
+    case NGX_QUIC_FT_DATA_BLOCKED:
+
+        if (!(ngx_quic_short_pkt(flags) || ngx_quic_pkt_zrtt(flags))) {
+            goto not_allowed;
+        }
+
+        p = ngx_quic_parse_int(p, end, &f->u.data_blocked.limit);
+        if (p == NULL) {
+            ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                          "failed to parse data blocked frame limit");
+            return NGX_ERROR;
+        }
+
+        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, pkt->log, 0,
+                       "DATA BLOCKED frame { limit %ui }",
+                       f->u.data_blocked.limit);
+        break;
+
+    case NGX_QUIC_FT_STREAM_DATA_BLOCKED:
+
+        if (!(ngx_quic_short_pkt(flags) || ngx_quic_pkt_zrtt(flags))) {
+            goto not_allowed;
+        }
+
+        p = ngx_quic_parse_int_multi(p, end, &f->u.stream_data_blocked.id,
+                                     &f->u.stream_data_blocked.limit, NULL);
+        if (p == NULL) {
+            ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                          "failed to parse tream data blocked frame");
+            return NGX_ERROR;
+        }
+
+        ngx_log_debug2(NGX_LOG_DEBUG_EVENT, pkt->log, 0,
+                       "STREAM DATA BLOCKED frame { id: %ui limit: %ui }",
+                       f->u.stream_data_blocked.id,
+                       f->u.stream_data_blocked.limit);
+        break;
+
+    case NGX_QUIC_FT_RETIRE_CONNECTION_ID:
+
+        if (!(ngx_quic_short_pkt(flags) || ngx_quic_pkt_zrtt(flags))) {
+            goto not_allowed;
+        }
+
+        p = ngx_quic_parse_int(p, end, &f->u.retire_cid.sequence_number);
+        if (p == NULL) {
+            ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                          "failed to parse retire connection id"
+                          " frame sequence number");
+            return NGX_ERROR;
+        }
+
+        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, pkt->log, 0,
+                       "RETIRE CONNECTION ID frame { sequence_number %ui }",
+                       f->u.retire_cid.sequence_number);
+        break;
+
+    case NGX_QUIC_FT_PATH_CHALLENGE:
+
+        if (!(ngx_quic_short_pkt(flags) || ngx_quic_pkt_zrtt(flags))) {
+            goto not_allowed;
+        }
+
+        p = ngx_quic_copy_bytes(p, end, 8, f->u.path_challenge.data);
+        if (p == NULL) {
+            ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                          "failed to get path challenge frame data");
+            return NGX_ERROR;
+        }
+
+        ngx_log_debug0(NGX_LOG_DEBUG_EVENT, pkt->log, 0,
+                       "PATH CHALLENGE frame");
+
+        ngx_quic_hexdump0(pkt->log, "path challenge data",
+                          f->u.path_challenge.data, 8);
+        break;
+
+    case NGX_QUIC_FT_PATH_RESPONSE:
+
+        if (!(ngx_quic_short_pkt(flags) || ngx_quic_pkt_zrtt(flags))) {
+            goto not_allowed;
+        }
+
+        p = ngx_quic_copy_bytes(p, end, 8, f->u.path_response.data);
+        if (p == NULL) {
+            ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                          "failed to get path response frame data");
+            return NGX_ERROR;
+        }
+
+        ngx_log_debug0(NGX_LOG_DEBUG_EVENT, pkt->log, 0,
+                       "PATH RESPONSE frame");
+
+        ngx_quic_hexdump0(pkt->log, "path response data",
+                          f->u.path_response.data, 8);
+        break;
+
+    default:
+        ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                      "unknown frame type 0x%xi in packet", f->type);
+
+        return NGX_ERROR;
+    }
+
+    return p - start;
+
+not_allowed:
+
+    ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                  "frame type 0x%xi is not allowed in packet with flags 0x%xi",
+                  f->type, pkt->flags);
+
+    return NGX_DECLINED;
+}
+
+
+ssize_t
+ngx_quic_parse_ack_range(ngx_quic_header_t *pkt, u_char *start, u_char *end,
+    uint64_t *gap, uint64_t *range)
+{
+    u_char  *p;
+
+    p = start;
+
+    p = ngx_quic_parse_int(p, end, gap);
+    if (p == NULL) {
+        ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                      "failed to parse ack frame gap");
+        return NGX_ERROR;
+    }
+
+    p = ngx_quic_parse_int(p, end, range);
+    if (p == NULL) {
+        ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                      "failed to parse ack frame range");
+        return NGX_ERROR;
+    }
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, pkt->log, 0,
+                   "ACK range: gap %ui range %ui", *gap, *range);
+
+    return p - start;
+}
+
+
+ssize_t
+ngx_quic_create_frame(u_char *p, ngx_quic_frame_t *f)
+{
+    /*
+     *  QUIC-recovery, section 2:
+     *
+     *  Ack-eliciting Frames:  All frames other than ACK, PADDING, and
+     *  CONNECTION_CLOSE are considered ack-eliciting.
+     */
+    f->need_ack = 1;
+
+    switch (f->type) {
+    case NGX_QUIC_FT_ACK:
+        f->need_ack = 0;
+        return ngx_quic_create_ack(p, &f->u.ack);
+
+    case NGX_QUIC_FT_CRYPTO:
+        return ngx_quic_create_crypto(p, &f->u.crypto);
+
+    case NGX_QUIC_FT_HANDSHAKE_DONE:
+        return ngx_quic_create_hs_done(p);
+
+    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:
+        return ngx_quic_create_stream(p, &f->u.stream);
+
+    case NGX_QUIC_FT_CONNECTION_CLOSE:
+        f->need_ack = 0;
+        return ngx_quic_create_close(p, &f->u.close);
+
+    case NGX_QUIC_FT_MAX_STREAMS:
+        return ngx_quic_create_max_streams(p, &f->u.max_streams);
+
+    case NGX_QUIC_FT_MAX_STREAM_DATA:
+        return ngx_quic_create_max_stream_data(p, &f->u.max_stream_data);
+
+    default:
+        /* BUG: unsupported frame type generated */
+        return NGX_ERROR;
+    }
+}
+
+
+static size_t
+ngx_quic_create_ack(u_char *p, ngx_quic_ack_frame_t *ack)
+{
+    size_t   len;
+    u_char  *start;
+
+    /* minimal ACK packet */
+
+    if (p == NULL) {
+        len = ngx_quic_varint_len(NGX_QUIC_FT_ACK);
+        len += ngx_quic_varint_len(ack->largest);
+        len += ngx_quic_varint_len(0);
+        len += ngx_quic_varint_len(0);
+        len += ngx_quic_varint_len(ack->first_range);
+
+        return len;
+    }
+
+    start = p;
+
+    ngx_quic_build_int(&p, NGX_QUIC_FT_ACK);
+    ngx_quic_build_int(&p, ack->largest);
+    ngx_quic_build_int(&p, 0);
+    ngx_quic_build_int(&p, 0);
+    ngx_quic_build_int(&p, ack->first_range);
+
+    return p - start;
+}
+
+
+static size_t
+ngx_quic_create_crypto(u_char *p, ngx_quic_crypto_frame_t *crypto)
+{
+    size_t   len;
+    u_char  *start;
+
+    if (p == NULL) {
+        len = ngx_quic_varint_len(NGX_QUIC_FT_CRYPTO);
+        len += ngx_quic_varint_len(crypto->offset);
+        len += ngx_quic_varint_len(crypto->len);
+        len += crypto->len;
+
+        return len;
+    }
+
+    start = p;
+
+    ngx_quic_build_int(&p, NGX_QUIC_FT_CRYPTO);
+    ngx_quic_build_int(&p, crypto->offset);
+    ngx_quic_build_int(&p, crypto->len);
+    p = ngx_cpymem(p, crypto->data, crypto->len);
+
+    return p - start;
+}
+
+
+static size_t
+ngx_quic_create_hs_done(u_char *p)
+{
+    u_char  *start;
+
+    if (p == NULL) {
+        return ngx_quic_varint_len(NGX_QUIC_FT_HANDSHAKE_DONE);
+    }
+
+    start = p;
+
+    ngx_quic_build_int(&p, NGX_QUIC_FT_HANDSHAKE_DONE);
+
+    return p - start;
+}
+
+
+static size_t
+ngx_quic_create_stream(u_char *p, ngx_quic_stream_frame_t *sf)
+{
+    size_t   len;
+    u_char  *start;
+
+    if (!sf->len) {
+#if 0
+        ngx_log_error(NGX_LOG_INFO, log, 0,
+                      "attempt to generate a stream frame without length");
+#endif
+        // XXX: handle error in caller
+        return NGX_ERROR;
+    }
+
+    if (p == NULL) {
+        len = ngx_quic_varint_len(sf->type);
+
+        if (sf->off) {
+            len += ngx_quic_varint_len(sf->offset);
+        }
+
+        len += ngx_quic_varint_len(sf->stream_id);
+
+        /* length is always present in generated frames */
+        len += ngx_quic_varint_len(sf->length);
+
+        len += sf->length;
+
+        return len;
+    }
+
+    start = p;
+
+    ngx_quic_build_int(&p, sf->type);
+    ngx_quic_build_int(&p, sf->stream_id);
+
+    if (sf->off) {
+        ngx_quic_build_int(&p, sf->offset);
+    }
+
+    /* length is always present in generated frames */
+    ngx_quic_build_int(&p, sf->length);
+
+    p = ngx_cpymem(p, sf->data, sf->length);
+
+    return p - start;
+}
+
+
+static size_t
+ngx_quic_create_max_streams(u_char *p, ngx_quic_max_streams_frame_t *ms)
+{
+    size_t       len;
+    u_char      *start;
+    ngx_uint_t   type;
+
+    type = ms->bidi ?  NGX_QUIC_FT_MAX_STREAMS : NGX_QUIC_FT_MAX_STREAMS2;
+
+    if (p == NULL) {
+        len = ngx_quic_varint_len(type);
+        len += ngx_quic_varint_len(ms->limit);
+        return len;
+    }
+
+    start = p;
+
+    ngx_quic_build_int(&p, type);
+    ngx_quic_build_int(&p, ms->limit);
+
+    return p - start;
+}
+
+
+static ngx_int_t
+ngx_quic_parse_transport_param(u_char *p, u_char *end, uint16_t id,
+    ngx_quic_tp_t *dst)
+{
+    uint64_t   varint;
+
+    switch (id) {
+    case NGX_QUIC_TP_ORIGINAL_CONNECTION_ID:
+    case NGX_QUIC_TP_STATELESS_RESET_TOKEN:
+    case NGX_QUIC_TP_PREFERRED_ADDRESS:
+        // TODO
+        return NGX_DECLINED;
+    }
+
+    switch (id) {
+
+    case NGX_QUIC_TP_DISABLE_ACTIVE_MIGRATION:
+        /* zero-length option */
+        if (end - p != 0) {
+            return NGX_ERROR;
+        }
+        dst->disable_active_migration = 1;
+        return NGX_OK;
+
+    case NGX_QUIC_TP_MAX_IDLE_TIMEOUT:
+    case NGX_QUIC_TP_MAX_PACKET_SIZE:
+    case NGX_QUIC_TP_INITIAL_MAX_DATA:
+    case NGX_QUIC_TP_INITIAL_MAX_STREAM_DATA_BIDI_LOCAL:
+    case NGX_QUIC_TP_INITIAL_MAX_STREAM_DATA_BIDI_REMOTE:
+    case NGX_QUIC_TP_INITIAL_MAX_STREAM_DATA_UNI:
+    case NGX_QUIC_TP_INITIAL_MAX_STREAMS_BIDI:
+    case NGX_QUIC_TP_INITIAL_MAX_STREAMS_UNI:
+    case NGX_QUIC_TP_ACK_DELAY_EXPONENT:
+    case NGX_QUIC_TP_MAX_ACK_DELAY:
+    case NGX_QUIC_TP_ACTIVE_CONNECTION_ID_LIMIT:
+
+        p = ngx_quic_parse_int(p, end, &varint);
+        if (p == NULL) {
+            return NGX_ERROR;
+        }
+        break;
+
+    default:
+        return NGX_DECLINED;
+    }
+
+    switch (id) {
+
+    case NGX_QUIC_TP_MAX_IDLE_TIMEOUT:
+        dst->max_idle_timeout = varint;
+        break;
+
+    case NGX_QUIC_TP_MAX_PACKET_SIZE:
+        dst->max_packet_size = varint;
+        break;
+
+    case NGX_QUIC_TP_INITIAL_MAX_DATA:
+        dst->initial_max_data = varint;
+        break;
+
+    case NGX_QUIC_TP_INITIAL_MAX_STREAM_DATA_BIDI_LOCAL:
+        dst->initial_max_stream_data_bidi_local = varint;
+        break;
+
+    case NGX_QUIC_TP_INITIAL_MAX_STREAM_DATA_BIDI_REMOTE:
+        dst->initial_max_stream_data_bidi_remote = varint;
+        break;
+
+    case NGX_QUIC_TP_INITIAL_MAX_STREAM_DATA_UNI:
+        dst->initial_max_stream_data_uni = varint;
+        break;
+
+    case NGX_QUIC_TP_INITIAL_MAX_STREAMS_BIDI:
+        dst->initial_max_streams_bidi = varint;
+        break;
+
+    case NGX_QUIC_TP_INITIAL_MAX_STREAMS_UNI:
+        dst->initial_max_streams_uni = varint;
+        break;
+
+    case NGX_QUIC_TP_ACK_DELAY_EXPONENT:
+        dst->ack_delay_exponent = varint;
+        break;
+
+    case NGX_QUIC_TP_MAX_ACK_DELAY:
+        dst->max_ack_delay = varint;
+        break;
+
+    case NGX_QUIC_TP_ACTIVE_CONNECTION_ID_LIMIT:
+        dst->active_connection_id_limit = varint;
+        break;
+
+    default:
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_quic_parse_transport_params(u_char *p, u_char *end, ngx_quic_tp_t *tp,
+    ngx_log_t *log)
+{
+    ngx_int_t  rc;
+
+#if (NGX_QUIC_DRAFT_VERSION < 27)
+
+    uint16_t  id, len, tp_len;
+
+    p = ngx_quic_read_uint16(p, end, &tp_len);
+    if (p == NULL) {
+        ngx_log_error(NGX_LOG_INFO, log, 0,
+                      "failed to parse total transport params length");
+        return NGX_ERROR;
+    }
+
+    while (p < end) {
+
+        p = ngx_quic_read_uint16(p, end, &id);
+        if (p == NULL) {
+            ngx_log_error(NGX_LOG_INFO, log, 0,
+                          "failed to parse transport param id");
+            return NGX_ERROR;
+        }
+
+        p = ngx_quic_read_uint16(p, end, &len);
+        if (p == NULL) {
+            ngx_log_error(NGX_LOG_INFO, log, 0,
+                         "failed to parse transport param id 0x%xi length", id);
+            return NGX_ERROR;
+        }
+
+        rc = ngx_quic_parse_transport_param(p, p + len, id, tp);
+
+        if (rc == NGX_ERROR) {
+            ngx_log_error(NGX_LOG_INFO, log, 0,
+                          "failed to parse transport param id 0x%xi data", id);
+            return NGX_ERROR;
+        }
+
+        if (rc == NGX_DECLINED) {
+            ngx_log_error(NGX_LOG_INFO, log, 0,
+                          "unknown transport param id 0x%xi, skipped", id);
+        }
+
+        p += len;
+    };
+
+#else
+
+    uint64_t  id, len;
+
+    while (p < end) {
+        p = ngx_quic_parse_int(p, end, &id);
+        if (p == NULL) {
+            ngx_log_error(NGX_LOG_INFO, log, 0,
+                          "failed to parse transport param id");
+            return NGX_ERROR;
+        }
+
+        p = ngx_quic_parse_int(p, end, &len);
+        if (p == NULL) {
+            ngx_log_error(NGX_LOG_INFO, log, 0,
+                         "failed to parse transport param id 0x%xi length", id);
+            return NGX_ERROR;
+        }
+
+        rc = ngx_quic_parse_transport_param(p, p + len, id, tp);
+
+        if (rc == NGX_ERROR) {
+            ngx_log_error(NGX_LOG_INFO, log, 0,
+                          "failed to parse transport param id 0x%xi data", id);
+            return NGX_ERROR;
+        }
+
+        if (rc == NGX_DECLINED) {
+            ngx_log_error(NGX_LOG_INFO, log, 0,
+                          "unknown transport param id 0x%xi,skipped", id);
+        }
+
+        p += len;
+
+    }
+
+#endif
+
+    if (p != end) {
+        ngx_log_error(NGX_LOG_INFO, log, 0,
+                      "trailing garbage in transport parameters: %ui bytes",
+                      end - p);
+        return NGX_ERROR;
+    }
+
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, log, 0,
+                   "client transport parameters parsed successfully");
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, log, 0,
+                   "disable active migration: %ui",
+                   tp->disable_active_migration);
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, log, 0, "idle timeout: %ui",
+                   tp->max_idle_timeout);
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, log, 0, "max packet size: %ui",
+                   tp->max_packet_size);
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, log, 0, "max data: %ui",
+                   tp->initial_max_data);
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, log, 0,
+                   "max stream data bidi local: %ui",
+                   tp->initial_max_stream_data_bidi_local);
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, log, 0,
+                   "max stream data bidi remote: %ui",
+                   tp->initial_max_stream_data_bidi_remote);
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, log, 0, "max stream data uni: %ui",
+                   tp->initial_max_stream_data_uni);
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, log, 0,
+                   "initial max streams bidi: %ui",
+                   tp->initial_max_streams_bidi);
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, log, 0, "initial max streams uni: %ui",
+                   tp->initial_max_streams_uni);
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, log, 0, "ack delay exponent: %ui",
+                   tp->ack_delay_exponent);
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, log, 0, "max ack delay: %ui",
+                   tp->max_ack_delay);
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, log, 0,
+                   "active connection id limit: %ui",
+                   tp->active_connection_id_limit);
+
+    return NGX_OK;
+}
+
+
+static size_t
+ngx_quic_create_max_stream_data(u_char *p, ngx_quic_max_stream_data_frame_t *ms)
+{
+    size_t   len;
+    u_char  *start;
+
+    if (p == NULL) {
+        len = ngx_quic_varint_len(NGX_QUIC_FT_MAX_STREAM_DATA);
+        len += ngx_quic_varint_len(ms->id);
+        len += ngx_quic_varint_len(ms->limit);
+        return len;
+    }
+
+    start = p;
+
+    ngx_quic_build_int(&p, NGX_QUIC_FT_MAX_STREAM_DATA);
+    ngx_quic_build_int(&p, ms->id);
+    ngx_quic_build_int(&p, ms->limit);
+
+    return p - start;
+}
+
+
+ssize_t
+ngx_quic_create_transport_params(u_char *pos, u_char *end, ngx_quic_tp_t *tp)
+{
+    u_char  *p;
+    size_t   len;
+
+#if (NGX_QUIC_DRAFT_VERSION < 27)
+
+/* older drafts with static transport parameters encoding */
+
+#define ngx_quic_tp_len(id, value)                                            \
+    4 + ngx_quic_varint_len(value)
+
+#define ngx_quic_tp_vint(id, value)                                           \
+    do {                                                                      \
+        p = ngx_quic_write_uint16(p, id);                                     \
+        p = ngx_quic_write_uint16(p, ngx_quic_varint_len(value));             \
+        ngx_quic_build_int(&p, value);                                        \
+    } while (0)
+
+#else
+
+/* recent drafts with variable integer transport parameters encoding */
+
+#define ngx_quic_tp_len(id, value)                                            \
+    ngx_quic_varint_len(id)                                                   \
+    + ngx_quic_varint_len(value)                                              \
+    + ngx_quic_varint_len(ngx_quic_varint_len(value))
+
+#define ngx_quic_tp_vint(id, value)                                           \
+    do {                                                                      \
+        ngx_quic_build_int(&p, id);                                           \
+        ngx_quic_build_int(&p, ngx_quic_varint_len(value));                   \
+        ngx_quic_build_int(&p, value);                                        \
+    } while (0)
+
+#endif
+
+    p = pos;
+
+    len = ngx_quic_tp_len(NGX_QUIC_TP_ACTIVE_CONNECTION_ID_LIMIT,
+                          tp->active_connection_id_limit);
+
+    len += ngx_quic_tp_len(NGX_QUIC_TP_INITIAL_MAX_DATA,tp->initial_max_data);
+
+    len += ngx_quic_tp_len(NGX_QUIC_TP_INITIAL_MAX_STREAMS_UNI,
+                           tp->initial_max_streams_uni);
+
+    len += ngx_quic_tp_len(NGX_QUIC_TP_INITIAL_MAX_STREAMS_BIDI,
+                           tp->initial_max_streams_bidi);
+
+    len += ngx_quic_tp_len(NGX_QUIC_TP_INITIAL_MAX_STREAM_DATA_BIDI_LOCAL,
+                           tp->initial_max_stream_data_bidi_local);
+
+    len += ngx_quic_tp_len(NGX_QUIC_TP_INITIAL_MAX_STREAM_DATA_BIDI_REMOTE,
+                           tp->initial_max_stream_data_bidi_remote);
+
+    len += ngx_quic_tp_len(NGX_QUIC_TP_INITIAL_MAX_STREAM_DATA_UNI,
+                           tp->initial_max_stream_data_uni);
+
+    len += ngx_quic_tp_len(NGX_QUIC_TP_MAX_IDLE_TIMEOUT,
+                           tp->max_idle_timeout);
+
+    if (pos == NULL) {
+#if (NGX_QUIC_DRAFT_VERSION < 27)
+        len += 2;
+#endif
+        return len;
+    }
+
+#if (NGX_QUIC_DRAFT_VERSION < 27)
+    /* TLS extension length */
+    p = ngx_quic_write_uint16(p, len);
+#endif
+
+    ngx_quic_tp_vint(NGX_QUIC_TP_ACTIVE_CONNECTION_ID_LIMIT,
+                     tp->active_connection_id_limit);
+
+    ngx_quic_tp_vint(NGX_QUIC_TP_INITIAL_MAX_DATA,
+                     tp->initial_max_data);
+
+    ngx_quic_tp_vint(NGX_QUIC_TP_INITIAL_MAX_STREAMS_UNI,
+                     tp->initial_max_streams_uni);
+
+    ngx_quic_tp_vint(NGX_QUIC_TP_INITIAL_MAX_STREAMS_BIDI,
+                     tp->initial_max_streams_bidi);
+
+    ngx_quic_tp_vint(NGX_QUIC_TP_INITIAL_MAX_STREAM_DATA_BIDI_LOCAL,
+                     tp->initial_max_stream_data_bidi_local);
+
+    ngx_quic_tp_vint(NGX_QUIC_TP_INITIAL_MAX_STREAM_DATA_BIDI_REMOTE,
+                     tp->initial_max_stream_data_bidi_remote);
+
+    ngx_quic_tp_vint(NGX_QUIC_TP_INITIAL_MAX_STREAM_DATA_UNI,
+                     tp->initial_max_stream_data_uni);
+
+    ngx_quic_tp_vint(NGX_QUIC_TP_MAX_IDLE_TIMEOUT,
+                     tp->max_idle_timeout);
+
+    ngx_quic_hexdump0(ngx_cycle->log, "transport parameters", pos, p - pos);
+
+    return p - pos;
+}
+
+
+static size_t
+ngx_quic_create_close(u_char *p, ngx_quic_close_frame_t *cl)
+{
+    size_t   len;
+    u_char  *start;
+
+    if (p == NULL) {
+        len = ngx_quic_varint_len(NGX_QUIC_FT_CONNECTION_CLOSE);
+        len += ngx_quic_varint_len(cl->error_code);
+        len += ngx_quic_varint_len(cl->frame_type);
+        len += ngx_quic_varint_len(cl->reason.len);
+        len += cl->reason.len;
+
+        return len;
+    }
+
+    start = p;
+
+    ngx_quic_build_int(&p, NGX_QUIC_FT_CONNECTION_CLOSE);
+    ngx_quic_build_int(&p, cl->error_code);
+    ngx_quic_build_int(&p, cl->frame_type);
+    ngx_quic_build_int(&p, cl->reason.len);
+    p = ngx_cpymem(p, cl->reason.data, cl->reason.len);
+
+    return p - start;
+}
new file mode 100644
--- /dev/null
+++ b/src/event/ngx_event_quic_transport.h
@@ -0,0 +1,298 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_EVENT_QUIC_WIRE_H_INCLUDED_
+#define _NGX_EVENT_QUIC_WIRE_H_INCLUDED_
+
+
+#include <ngx_event_openssl.h>
+
+
+#define ngx_quic_long_pkt(flags)  ((flags) & 0x80)            /* 17.2   */
+#define ngx_quic_short_pkt(flags) (((flags) & 0x80) == 0)     /* 17.3   */
+
+/* Long packet types */
+#define NGX_QUIC_PKT_INITIAL                             0xC0 /* 17.2.2 */
+#define NGX_QUIC_PKT_ZRTT                                0xD0 /* 17.2.3 */
+#define NGX_QUIC_PKT_HANDSHAKE                           0xE0 /* 17.2.4 */
+#define NGX_QUIC_PKT_RETRY                               0xF0 /* 17.2.5 */
+#define NGX_QUIC_PKT_KPHASE                              0x04 /* 17.3   */
+
+#define ngx_quic_pkt_in(flags)     (((flags) & 0xF0) == NGX_QUIC_PKT_INITIAL)
+#define ngx_quic_pkt_zrtt(flags)   (((flags) & 0xF0) == NGX_QUIC_PKT_ZRTT)
+#define ngx_quic_pkt_hs(flags)     (((flags) & 0xF0) == NGX_QUIC_PKT_HANDSHAKE)
+#define ngx_quic_pkt_retry(flags)  (((flags) & 0xF0) == NGX_QUIC_PKT_RETRY)
+
+/* 12.4.  Frames and Frame Types */
+#define NGX_QUIC_FT_PADDING                              0x00
+#define NGX_QUIC_FT_PING                                 0x01
+#define NGX_QUIC_FT_ACK                                  0x02
+#define NGX_QUIC_FT_ACK_ECN                              0x03
+#define NGX_QUIC_FT_RESET_STREAM                         0x04
+#define NGX_QUIC_FT_STOP_SENDING                         0x05
+#define NGX_QUIC_FT_CRYPTO                               0x06
+#define NGX_QUIC_FT_NEW_TOKEN                            0x07
+#define NGX_QUIC_FT_STREAM0                              0x08
+#define NGX_QUIC_FT_STREAM1                              0x09
+#define NGX_QUIC_FT_STREAM2                              0x0A
+#define NGX_QUIC_FT_STREAM3                              0x0B
+#define NGX_QUIC_FT_STREAM4                              0x0C
+#define NGX_QUIC_FT_STREAM5                              0x0D
+#define NGX_QUIC_FT_STREAM6                              0x0E
+#define NGX_QUIC_FT_STREAM7                              0x0F
+#define NGX_QUIC_FT_MAX_DATA                             0x10
+#define NGX_QUIC_FT_MAX_STREAM_DATA                      0x11
+#define NGX_QUIC_FT_MAX_STREAMS                          0x12
+#define NGX_QUIC_FT_MAX_STREAMS2                         0x13
+#define NGX_QUIC_FT_DATA_BLOCKED                         0x14
+#define NGX_QUIC_FT_STREAM_DATA_BLOCKED                  0x15
+#define NGX_QUIC_FT_STREAMS_BLOCKED                      0x16
+#define NGX_QUIC_FT_STREAMS_BLOCKED2                     0x17
+#define NGX_QUIC_FT_NEW_CONNECTION_ID                    0x18
+#define NGX_QUIC_FT_RETIRE_CONNECTION_ID                 0x19
+#define NGX_QUIC_FT_PATH_CHALLENGE                       0x1A
+#define NGX_QUIC_FT_PATH_RESPONSE                        0x1B
+#define NGX_QUIC_FT_CONNECTION_CLOSE                     0x1C
+#define NGX_QUIC_FT_CONNECTION_CLOSE2                    0x1D
+#define NGX_QUIC_FT_HANDSHAKE_DONE                       0x1E
+
+/* 22.4.  QUIC Transport Error Codes Registry */
+#define NGX_QUIC_ERR_NO_ERROR                            0x00
+#define NGX_QUIC_ERR_INTERNAL_ERROR                      0x01
+#define NGX_QUIC_ERR_SERVER_BUSY                         0x02
+#define NGX_QUIC_ERR_FLOW_CONTROL_ERROR                  0x03
+#define NGX_QUIC_ERR_STREAM_LIMIT_ERROR                  0x04
+#define NGX_QUIC_ERR_STREAM_STATE_ERROR                  0x05
+#define NGX_QUIC_ERR_FINAL_SIZE_ERROR                    0x06
+#define NGX_QUIC_ERR_FRAME_ENCODING_ERROR                0x07
+#define NGX_QUIC_ERR_TRANSPORT_PARAMETER_ERROR           0x08
+#define NGX_QUIC_ERR_CONNECTION_ID_LIMIT_ERROR           0x09
+#define NGX_QUIC_ERR_PROTOCOL_VIOLATION                  0x0A
+#define NGX_QUIC_ERR_INVALID_TOKEN                       0x0B
+/* 0xC is not defined */
+#define NGX_QUIC_ERR_CRYPTO_BUFFER_EXCEEDED              0x0D
+/* 0xE is not defined */
+#define NGX_QUIC_ERR_CRYPTO_ERROR                        0x10
+
+#define NGX_QUIC_ERR_LAST  NGX_QUIC_ERR_CRYPTO_ERROR
+
+/* Transport parameters */
+#define NGX_QUIC_TP_ORIGINAL_CONNECTION_ID               0x00
+#define NGX_QUIC_TP_MAX_IDLE_TIMEOUT                     0x01
+#define NGX_QUIC_TP_STATELESS_RESET_TOKEN                0x02
+#define NGX_QUIC_TP_MAX_PACKET_SIZE                      0x03
+#define NGX_QUIC_TP_INITIAL_MAX_DATA                     0x04
+#define NGX_QUIC_TP_INITIAL_MAX_STREAM_DATA_BIDI_LOCAL   0x05
+#define NGX_QUIC_TP_INITIAL_MAX_STREAM_DATA_BIDI_REMOTE  0x06
+#define NGX_QUIC_TP_INITIAL_MAX_STREAM_DATA_UNI          0x07
+#define NGX_QUIC_TP_INITIAL_MAX_STREAMS_BIDI             0x08
+#define NGX_QUIC_TP_INITIAL_MAX_STREAMS_UNI              0x09
+#define NGX_QUIC_TP_ACK_DELAY_EXPONENT                   0x0A
+#define NGX_QUIC_TP_MAX_ACK_DELAY                        0x0B
+#define NGX_QUIC_TP_DISABLE_ACTIVE_MIGRATION             0x0C
+#define NGX_QUIC_TP_PREFERRED_ADDRESS                    0x0D
+#define NGX_QUIC_TP_ACTIVE_CONNECTION_ID_LIMIT           0x0E
+
+
+typedef struct {
+    uint64_t                                    largest;
+    uint64_t                                    delay;
+    uint64_t                                    range_count;
+    uint64_t                                    first_range;
+    uint64_t                                    ect0;
+    uint64_t                                    ect1;
+    uint64_t                                    ce;
+    u_char                                      *ranges_start;
+    u_char                                      *ranges_end;
+} ngx_quic_ack_frame_t;
+
+
+typedef struct {
+    uint64_t                                    offset;
+    uint64_t                                    len;
+    u_char                                     *data;
+} ngx_quic_crypto_frame_t;
+
+
+typedef struct {
+    uint64_t                                    seqnum;
+    uint64_t                                    retire;
+    uint8_t                                     len;
+    u_char                                      cid[20];
+    u_char                                      srt[16];
+} ngx_quic_new_conn_id_frame_t;
+
+
+typedef struct {
+    uint8_t                                     type;
+    uint64_t                                    stream_id;
+    uint64_t                                    offset;
+    uint64_t                                    length;
+    unsigned                                    off:1;
+    unsigned                                    len:1;
+    unsigned                                    fin:1;
+    u_char                                     *data;
+} ngx_quic_stream_frame_t;
+
+
+typedef struct {
+    uint64_t                                    max_data;
+} ngx_quic_max_data_frame_t;
+
+
+typedef struct {
+    uint64_t                                    error_code;
+    uint64_t                                    frame_type;
+    ngx_str_t                                   reason;
+} ngx_quic_close_frame_t;
+
+
+typedef struct {
+    uint64_t                                    id;
+    uint64_t                                    error_code;
+    uint64_t                                    final_size;
+} ngx_quic_reset_stream_frame_t;
+
+
+typedef struct {
+    uint64_t                                    id;
+    uint64_t                                    error_code;
+} ngx_quic_stop_sending_frame_t;
+
+
+typedef struct {
+    uint64_t                                    limit;
+    ngx_uint_t                                  bidi;  /* unsigned: bidi:1 */
+} ngx_quic_streams_blocked_frame_t;
+
+
+typedef struct {
+    uint64_t                                    limit;
+    ngx_uint_t                                  bidi;  /* unsigned: bidi:1 */
+} ngx_quic_max_streams_frame_t;
+
+
+typedef struct {
+    uint64_t                                    id;
+    uint64_t                                    limit;
+} ngx_quic_max_stream_data_frame_t;
+
+
+typedef struct {
+    uint64_t                                    limit;
+} ngx_quic_data_blocked_frame_t;
+
+
+typedef struct {
+    uint64_t                                    id;
+    uint64_t                                    limit;
+} ngx_quic_stream_data_blocked_frame_t;
+
+
+typedef struct {
+    uint64_t                                    sequence_number;
+} ngx_quic_retire_cid_frame_t;
+
+
+typedef struct {
+    u_char                                      data[8];
+} ngx_quic_path_challenge_frame_t;
+
+
+typedef struct ngx_quic_frame_s                 ngx_quic_frame_t;
+
+struct ngx_quic_frame_s {
+    ngx_uint_t                                  type;
+    enum ssl_encryption_level_t                 level;
+    ngx_queue_t                                 queue;
+    uint64_t                                    pnum;
+    ngx_msec_t                                  first;
+    ngx_msec_t                                  last;
+    ngx_uint_t                                  need_ack;
+                                                    /* unsigned need_ack:1; */
+
+    u_char                                     *data;
+    union {
+        ngx_quic_ack_frame_t                    ack;
+        ngx_quic_crypto_frame_t                 crypto;
+        ngx_quic_new_conn_id_frame_t            ncid;
+        ngx_quic_stream_frame_t                 stream;
+        ngx_quic_max_data_frame_t               max_data;
+        ngx_quic_close_frame_t                  close;
+        ngx_quic_reset_stream_frame_t           reset_stream;
+        ngx_quic_stop_sending_frame_t           stop_sending;
+        ngx_quic_streams_blocked_frame_t        streams_blocked;
+        ngx_quic_max_streams_frame_t            max_streams;
+        ngx_quic_max_stream_data_frame_t        max_stream_data;
+        ngx_quic_data_blocked_frame_t           data_blocked;
+        ngx_quic_stream_data_blocked_frame_t    stream_data_blocked;
+        ngx_quic_retire_cid_frame_t             retire_cid;
+        ngx_quic_path_challenge_frame_t         path_challenge;
+        ngx_quic_path_challenge_frame_t         path_response;
+    } u;
+    u_char                                      info[128]; // for debug
+};
+
+
+typedef struct {
+    ngx_log_t                                  *log;
+
+    struct ngx_quic_secret_s                   *secret;
+    struct ngx_quic_secret_s                   *next;
+    uint64_t                                    number;
+    uint8_t                                     num_len;
+    uint32_t                                    trunc;
+    uint8_t                                     flags;
+    uint32_t                                    version;
+    ngx_str_t                                   token;
+    enum ssl_encryption_level_t                 level;
+
+    /* filled in by parser */
+    ngx_buf_t                                  *raw;   /* udp datagram */
+
+    u_char                                     *data;  /* quic packet */
+    size_t                                      len;
+
+    /* cleartext fields */
+    ngx_str_t                                   dcid;
+    ngx_str_t                                   scid;
+    uint64_t                                    pn;
+    u_char                                     *plaintext;
+    ngx_str_t                                   payload; /* decrypted data */
+
+    unsigned                                    need_ack:1;
+    unsigned                                    key_phase:1;
+    unsigned                                    key_update:1;
+} ngx_quic_header_t;
+
+
+u_char *ngx_quic_error_text(uint64_t error_code);
+
+ngx_int_t ngx_quic_parse_long_header(ngx_quic_header_t *pkt);
+size_t ngx_quic_create_long_header(ngx_quic_header_t *pkt, u_char *out,
+    size_t pkt_len, u_char **pnp);
+
+ngx_int_t ngx_quic_parse_short_header(ngx_quic_header_t *pkt,
+    ngx_str_t *dcid);
+size_t ngx_quic_create_short_header(ngx_quic_header_t *pkt, u_char *out,
+    size_t pkt_len, u_char **pnp);
+
+ngx_int_t ngx_quic_parse_initial_header(ngx_quic_header_t *pkt);
+ngx_int_t ngx_quic_parse_handshake_header(ngx_quic_header_t *pkt);
+
+ssize_t ngx_quic_parse_frame(ngx_quic_header_t *pkt, u_char *start, u_char *end,
+    ngx_quic_frame_t *frame);
+ssize_t ngx_quic_create_frame(u_char *p, ngx_quic_frame_t *f);
+
+ssize_t ngx_quic_parse_ack_range(ngx_quic_header_t *pkt, u_char *start,
+    u_char *end, uint64_t *gap, uint64_t *range);
+
+ngx_int_t ngx_quic_parse_transport_params(u_char *p, u_char *end,
+    ngx_quic_tp_t *tp, ngx_log_t *log);
+ssize_t ngx_quic_create_transport_params(u_char *p, u_char *end,
+    ngx_quic_tp_t *tp);
+
+#endif /* _NGX_EVENT_QUIC_WIRE_H_INCLUDED_ */
--- a/src/http/modules/ngx_http_chunked_filter_module.c
+++ b/src/http/modules/ngx_http_chunked_filter_module.c
@@ -18,7 +18,7 @@ typedef struct {
 
 static ngx_int_t ngx_http_chunked_filter_init(ngx_conf_t *cf);
 static ngx_chain_t *ngx_http_chunked_create_trailers(ngx_http_request_t *r,
-    ngx_http_chunked_filter_ctx_t *ctx);
+    ngx_http_chunked_filter_ctx_t *ctx, size_t size);
 
 
 static ngx_http_module_t  ngx_http_chunked_filter_module_ctx = {
@@ -106,6 +106,7 @@ ngx_http_chunked_body_filter(ngx_http_re
 {
     u_char                         *chunk;
     off_t                           size;
+    size_t                          n;
     ngx_int_t                       rc;
     ngx_buf_t                      *b;
     ngx_chain_t                    *out, *cl, *tl, **ll;
@@ -161,29 +162,50 @@ ngx_http_chunked_body_filter(ngx_http_re
         chunk = b->start;
 
         if (chunk == NULL) {
-            /* the "0000000000000000" is 64-bit hexadecimal string */
+
+#if (NGX_HTTP_V3)
+            if (r->http_version == NGX_HTTP_VERSION_30) {
+                n = NGX_HTTP_V3_VARLEN_INT_LEN * 2;
 
-            chunk = ngx_palloc(r->pool, sizeof("0000000000000000" CRLF) - 1);
+            } else
+#endif
+            {
+                /* the "0000000000000000" is 64-bit hexadecimal string */
+                n = sizeof("0000000000000000" CRLF) - 1;
+            }
+
+            chunk = ngx_palloc(r->pool, n);
             if (chunk == NULL) {
                 return NGX_ERROR;
             }
 
             b->start = chunk;
-            b->end = chunk + sizeof("0000000000000000" CRLF) - 1;
+            b->end = chunk + n;
         }
 
         b->tag = (ngx_buf_tag_t) &ngx_http_chunked_filter_module;
         b->memory = 0;
         b->temporary = 1;
         b->pos = chunk;
-        b->last = ngx_sprintf(chunk, "%xO" CRLF, size);
+
+#if (NGX_HTTP_V3)
+        if (r->http_version == NGX_HTTP_VERSION_30) {
+            b->last = (u_char *) ngx_http_v3_encode_varlen_int(chunk,
+                                                       NGX_HTTP_V3_FRAME_DATA);
+            b->last = (u_char *) ngx_http_v3_encode_varlen_int(b->last, size);
+
+        } else
+#endif
+        {
+            b->last = ngx_sprintf(chunk, "%xO" CRLF, size);
+        }
 
         tl->next = out;
         out = tl;
     }
 
     if (cl->buf->last_buf) {
-        tl = ngx_http_chunked_create_trailers(r, ctx);
+        tl = ngx_http_chunked_create_trailers(r, ctx, size);
         if (tl == NULL) {
             return NGX_ERROR;
         }
@@ -192,11 +214,12 @@ ngx_http_chunked_body_filter(ngx_http_re
 
         *ll = tl;
 
-        if (size == 0) {
-            tl->buf->pos += 2;
-        }
-
-    } else if (size > 0) {
+    } else if (size > 0
+#if (NGX_HTTP_V3)
+               && r->http_version != NGX_HTTP_VERSION_30
+#endif
+               )
+    {
         tl = ngx_chain_get_free_buf(r->pool, &ctx->free);
         if (tl == NULL) {
             return NGX_ERROR;
@@ -227,7 +250,7 @@ ngx_http_chunked_body_filter(ngx_http_re
 
 static ngx_chain_t *
 ngx_http_chunked_create_trailers(ngx_http_request_t *r,
-    ngx_http_chunked_filter_ctx_t *ctx)
+    ngx_http_chunked_filter_ctx_t *ctx, size_t size)
 {
     size_t            len;
     ngx_buf_t        *b;
@@ -236,6 +259,12 @@ ngx_http_chunked_create_trailers(ngx_htt
     ngx_list_part_t  *part;
     ngx_table_elt_t  *header;
 
+#if (NGX_HTTP_V3)
+    if (r->http_version == NGX_HTTP_VERSION_30) {
+        return ngx_http_v3_create_trailers(r);
+    }
+#endif
+
     len = 0;
 
     part = &r->headers_out.trailers.part;
@@ -288,7 +317,10 @@ ngx_http_chunked_create_trailers(ngx_htt
 
     b->last = b->pos;
 
-    *b->last++ = CR; *b->last++ = LF;
+    if (size > 0) {
+        *b->last++ = CR; *b->last++ = LF;
+    }
+
     *b->last++ = '0';
     *b->last++ = CR; *b->last++ = LF;
 
--- a/src/http/modules/ngx_http_ssl_module.c
+++ b/src/http/modules/ngx_http_ssl_module.c
@@ -371,7 +371,7 @@ ngx_http_ssl_alpn_select(ngx_ssl_conn_t 
 #if (NGX_DEBUG)
     unsigned int            i;
 #endif
-#if (NGX_HTTP_V2)
+#if (NGX_HTTP_V2 || NGX_HTTP_V3)
     ngx_http_connection_t  *hc;
 #endif
 #if (NGX_HTTP_V2 || NGX_DEBUG)
@@ -388,9 +388,11 @@ ngx_http_ssl_alpn_select(ngx_ssl_conn_t 
     }
 #endif
 
-#if (NGX_HTTP_V2)
+#if (NGX_HTTP_V2 || NGX_HTTP_V3)
     hc = c->data;
+#endif
 
+#if (NGX_HTTP_V2)
     if (hc->addr_conf->http2) {
         srv =
            (unsigned char *) NGX_HTTP_V2_ALPN_ADVERTISE NGX_HTTP_NPN_ADVERTISE;
@@ -398,6 +400,12 @@ ngx_http_ssl_alpn_select(ngx_ssl_conn_t 
 
     } else
 #endif
+#if (NGX_HTTP_V3)
+    if (hc->addr_conf->http3) {
+        srv = (unsigned char *) NGX_HTTP_V3_ALPN_ADVERTISE;
+        srvlen = sizeof(NGX_HTTP_V3_ALPN_ADVERTISE) - 1;
+    } else
+#endif
     {
         srv = (unsigned char *) NGX_HTTP_NPN_ADVERTISE;
         srvlen = sizeof(NGX_HTTP_NPN_ADVERTISE) - 1;
@@ -1142,7 +1150,7 @@ ngx_http_ssl_init(ngx_conf_t *cf)
         addr = port[p].addrs.elts;
         for (a = 0; a < port[p].addrs.nelts; a++) {
 
-            if (!addr[a].opt.ssl) {
+            if (!addr[a].opt.ssl && !addr[a].opt.http3) {
                 continue;
             }
 
@@ -1156,6 +1164,14 @@ ngx_http_ssl_init(ngx_conf_t *cf)
                               cscf->file_name, cscf->line);
                 return NGX_ERROR;
             }
+
+            if (addr[a].opt.http3 && !(sscf->protocols & NGX_SSL_TLSv1_3)) {
+                ngx_log_error(NGX_LOG_EMERG, cf->log, 0,
+                              "\"ssl_protocols\" did not enable TLSv1.3 for "
+                              "the \"listen ... http3\" directive in %s:%ui",
+                              cscf->file_name, cscf->line);
+                return NGX_ERROR;
+            }
         }
     }
 
--- a/src/http/ngx_http.c
+++ b/src/http/ngx_http.c
@@ -1163,7 +1163,10 @@ ngx_http_add_listen(ngx_conf_t *cf, ngx_
     port = cmcf->ports->elts;
     for (i = 0; i < cmcf->ports->nelts; i++) {
 
-        if (p != port[i].port || sa->sa_family != port[i].family) {
+        if (p != port[i].port
+            || lsopt->type != port[i].type
+            || sa->sa_family != port[i].family)
+        {
             continue;
         }
 
@@ -1180,6 +1183,7 @@ ngx_http_add_listen(ngx_conf_t *cf, ngx_
     }
 
     port->family = sa->sa_family;
+    port->type = lsopt->type;
     port->port = p;
     port->addrs.elts = NULL;
 
@@ -1199,6 +1203,9 @@ ngx_http_add_addresses(ngx_conf_t *cf, n
 #if (NGX_HTTP_V2)
     ngx_uint_t             http2;
 #endif
+#if (NGX_HTTP_SSL)
+    ngx_uint_t             http3;
+#endif
 
     /*
      * we cannot compare whole sockaddr struct's as kernel
@@ -1234,6 +1241,9 @@ ngx_http_add_addresses(ngx_conf_t *cf, n
 #if (NGX_HTTP_V2)
         http2 = lsopt->http2 || addr[i].opt.http2;
 #endif
+#if (NGX_HTTP_SSL)
+        http3 = lsopt->http3 || addr[i].opt.http3;
+#endif
 
         if (lsopt->set) {
 
@@ -1270,6 +1280,9 @@ ngx_http_add_addresses(ngx_conf_t *cf, n
 #if (NGX_HTTP_V2)
         addr[i].opt.http2 = http2;
 #endif
+#if (NGX_HTTP_SSL)
+        addr[i].opt.http3 = http3;
+#endif
 
         return NGX_OK;
     }
@@ -1313,6 +1326,17 @@ ngx_http_add_address(ngx_conf_t *cf, ngx
 
 #endif
 
+#if (NGX_HTTP_SSL && !defined NGX_OPENSSL_QUIC)
+
+    if (lsopt->http3) {
+        ngx_conf_log_error(NGX_LOG_WARN, cf, 0,
+                           "nginx was built with OpenSSL that lacks QUIC "
+                           "support, HTTP/3 is not enabled for %V",
+                           &lsopt->addr_text);
+    }
+
+#endif
+
     addr = ngx_array_push(&port->addrs);
     if (addr == NULL) {
         return NGX_ERROR;
@@ -1735,6 +1759,7 @@ ngx_http_add_listening(ngx_conf_t *cf, n
     }
 #endif
 
+    ls->type = addr->opt.type;
     ls->backlog = addr->opt.backlog;
     ls->rcvbuf = addr->opt.rcvbuf;
     ls->sndbuf = addr->opt.sndbuf;
@@ -1802,6 +1827,9 @@ ngx_http_add_addrs(ngx_conf_t *cf, ngx_h
 #if (NGX_HTTP_V2)
         addrs[i].conf.http2 = addr[i].opt.http2;
 #endif
+#if (NGX_HTTP_SSL)
+        addrs[i].conf.http3 = addr[i].opt.http3;
+#endif
         addrs[i].conf.proxy_protocol = addr[i].opt.proxy_protocol;
 
         if (addr[i].hash.buckets == NULL
@@ -1867,6 +1895,9 @@ ngx_http_add_addrs6(ngx_conf_t *cf, ngx_
 #if (NGX_HTTP_V2)
         addrs6[i].conf.http2 = addr[i].opt.http2;
 #endif
+#if (NGX_HTTP_SSL)
+        addrs6[i].conf.http3 = addr[i].opt.http3;
+#endif
         addrs6[i].conf.proxy_protocol = addr[i].opt.proxy_protocol;
 
         if (addr[i].hash.buckets == NULL
--- a/src/http/ngx_http.h
+++ b/src/http/ngx_http.h
@@ -38,6 +38,9 @@ typedef u_char *(*ngx_http_log_handler_p
 #if (NGX_HTTP_V2)
 #include <ngx_http_v2.h>
 #endif
+#if (NGX_HTTP_V3)
+#include <ngx_http_v3.h>
+#endif
 #if (NGX_HTTP_CACHE)
 #include <ngx_http_cache.h>
 #endif
@@ -60,6 +63,9 @@ struct ngx_http_chunked_s {
     ngx_uint_t           state;
     off_t                size;
     off_t                length;
+#if (NGX_HTTP_V3)
+    void                *h3_parse;
+#endif
 };
 
 
--- a/src/http/ngx_http_core_module.c
+++ b/src/http/ngx_http_core_module.c
@@ -819,7 +819,7 @@ ngx_http_handler(ngx_http_request_t *r)
     if (!r->internal) {
         switch (r->headers_in.connection_type) {
         case 0:
-            r->keepalive = (r->http_version > NGX_HTTP_VERSION_10);
+            r->keepalive = (r->http_version == NGX_HTTP_VERSION_11);
             break;
 
         case NGX_HTTP_CONNECTION_CLOSE:
@@ -3880,6 +3880,7 @@ ngx_http_core_listen(ngx_conf_t *cf, ngx
     ngx_memzero(&lsopt, sizeof(ngx_http_listen_opt_t));
 
     lsopt.backlog = NGX_LISTEN_BACKLOG;
+    lsopt.type = SOCK_STREAM;
     lsopt.rcvbuf = -1;
     lsopt.sndbuf = -1;
 #if (NGX_HAVE_SETFIB)
@@ -4078,6 +4079,19 @@ ngx_http_core_listen(ngx_conf_t *cf, ngx
 #endif
         }
 
+        if (ngx_strcmp(value[n].data, "http3") == 0) {
+#if (NGX_HTTP_V3)
+            lsopt.http3 = 1;
+            lsopt.type = SOCK_DGRAM;
+            continue;
+#else
+            ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
+                               "the \"http3\" parameter requires "
+                               "ngx_http_v3_module");
+            return NGX_CONF_ERROR;
+#endif
+        }
+
         if (ngx_strcmp(value[n].data, "spdy") == 0) {
             ngx_conf_log_error(NGX_LOG_WARN, cf, 0,
                                "invalid parameter \"spdy\": "
--- a/src/http/ngx_http_core_module.h
+++ b/src/http/ngx_http_core_module.h
@@ -75,6 +75,7 @@ typedef struct {
     unsigned                   wildcard:1;
     unsigned                   ssl:1;
     unsigned                   http2:1;
+    unsigned                   http3:1;
 #if (NGX_HAVE_INET6)
     unsigned                   ipv6only:1;
 #endif
@@ -86,6 +87,7 @@ typedef struct {
     int                        backlog;
     int                        rcvbuf;
     int                        sndbuf;
+    int                        type;
 #if (NGX_HAVE_SETFIB)
     int                        setfib;
 #endif
@@ -237,6 +239,7 @@ struct ngx_http_addr_conf_s {
 
     unsigned                   ssl:1;
     unsigned                   http2:1;
+    unsigned                   http3:1;
     unsigned                   proxy_protocol:1;
 };
 
@@ -266,6 +269,7 @@ typedef struct {
 
 typedef struct {
     ngx_int_t                  family;
+    ngx_int_t                  type;
     in_port_t                  port;
     ngx_array_t                addrs;     /* array of ngx_http_conf_addr_t */
 } ngx_http_conf_port_t;
--- a/src/http/ngx_http_header_filter_module.c
+++ b/src/http/ngx_http_header_filter_module.c
@@ -187,6 +187,29 @@ ngx_http_header_filter(ngx_http_request_
         r->header_only = 1;
     }
 
+    if (r->headers_out.status_line.len == 0) {
+        if (r->headers_out.status == NGX_HTTP_NO_CONTENT
+            || r->headers_out.status == NGX_HTTP_NOT_MODIFIED)
+        {
+            r->header_only = 1;
+        }
+    }
+
+#if (NGX_HTTP_V3)
+
+    if (r->http_version == NGX_HTTP_VERSION_30) {
+        ngx_chain_t  *cl;
+
+        cl = ngx_http_v3_create_header(r);
+        if (cl == NULL) {
+            return NGX_ERROR;
+        }
+
+        return ngx_http_write_filter(r, cl);
+    }
+
+#endif
+
     if (r->headers_out.last_modified_time != -1) {
         if (r->headers_out.status != NGX_HTTP_OK
             && r->headers_out.status != NGX_HTTP_PARTIAL_CONTENT
@@ -220,7 +243,6 @@ ngx_http_header_filter(ngx_http_request_
             /* 2XX */
 
             if (status == NGX_HTTP_NO_CONTENT) {
-                r->header_only = 1;
                 ngx_str_null(&r->headers_out.content_type);
                 r->headers_out.last_modified_time = -1;
                 r->headers_out.last_modified = NULL;
@@ -237,10 +259,6 @@ ngx_http_header_filter(ngx_http_request_
         {
             /* 3XX */
 
-            if (status == NGX_HTTP_NOT_MODIFIED) {
-                r->header_only = 1;
-            }
-
             status = status - NGX_HTTP_MOVED_PERMANENTLY + NGX_HTTP_OFF_3XX;
             status_line = &ngx_http_status_lines[status];
             len += ngx_http_status_lines[status].len;
--- a/src/http/ngx_http_parse.c
+++ b/src/http/ngx_http_parse.c
@@ -144,6 +144,7 @@ ngx_http_parse_request_line(ngx_http_req
         /* HTTP methods: GET, HEAD, POST */
         case sw_start:
             r->request_start = p;
+            r->method_start = p;
 
             if (ch == CR || ch == LF) {
                 break;
@@ -158,7 +159,7 @@ ngx_http_parse_request_line(ngx_http_req
 
         case sw_method:
             if (ch == ' ') {
-                r->method_end = p - 1;
+                r->method_end = p;
                 m = r->request_start;
 
                 switch (p - m) {
@@ -2184,6 +2185,12 @@ ngx_http_parse_chunked(ngx_http_request_
         sw_trailer_header_almost_done
     } state;
 
+#if (NGX_HTTP_V3)
+    if (r->http_version == NGX_HTTP_VERSION_30) {
+        return ngx_http_v3_parse_request_body(r, b, ctx);
+    }
+#endif
+
     state = ctx->state;
 
     if (state == sw_chunk_data && ctx->size == 0) {
@@ -2370,6 +2377,11 @@ ngx_http_parse_chunked(ngx_http_request_
         }
     }
 
+    if (b->last_buf) {
+        /* XXX client prematurely closed connection */
+        return NGX_ERROR;
+    }
+
 data:
 
     ctx->state = state;
--- a/src/http/ngx_http_request.c
+++ b/src/http/ngx_http_request.c
@@ -64,6 +64,9 @@ static void ngx_http_ssl_handshake(ngx_e
 static void ngx_http_ssl_handshake_handler(ngx_connection_t *c);
 #endif
 
+#if (NGX_HTTP_V3)
+static void ngx_http_quic_stream_handler(ngx_connection_t *c);
+#endif
 
 static char *ngx_http_client_errors[] = {
 
@@ -218,7 +221,16 @@ ngx_http_init_connection(ngx_connection_
     ngx_http_in6_addr_t    *addr6;
 #endif
 
-    hc = ngx_pcalloc(c->pool, sizeof(ngx_http_connection_t));
+#if (NGX_HTTP_V3)
+    if (c->type == SOCK_DGRAM) {
+        hc = ngx_pcalloc(c->pool, sizeof(ngx_http_v3_connection_t));
+        hc->quic = 1;
+        hc->ssl = 1;
+
+    } else
+#endif
+        hc = ngx_pcalloc(c->pool, sizeof(ngx_http_connection_t));
+
     if (hc == NULL) {
         ngx_http_close_connection(c);
         return;
@@ -324,6 +336,23 @@ ngx_http_init_connection(ngx_connection_
     rev->handler = ngx_http_wait_request_handler;
     c->write->handler = ngx_http_empty_handler;
 
+    if (c->shared) {
+        rev->ready = 1;
+    }
+
+#if (NGX_HTTP_V3)
+    if (hc->quic) {
+        ngx_http_v3_srv_conf_t   *v3cf;
+        ngx_http_ssl_srv_conf_t  *sscf;
+
+        v3cf = ngx_http_get_module_srv_conf(hc->conf_ctx, ngx_http_v3_module);
+        sscf = ngx_http_get_module_srv_conf(hc->conf_ctx, ngx_http_ssl_module);
+
+        ngx_quic_run(c, &sscf->ssl, &v3cf->quic, ngx_http_quic_stream_handler);
+        return;
+    }
+#endif
+
 #if (NGX_HTTP_V2)
     if (hc->addr_conf->http2) {
         rev->handler = ngx_http_v2_init;
@@ -371,6 +400,64 @@ ngx_http_init_connection(ngx_connection_
 }
 
 
+#if (NGX_HTTP_V3)
+
+static void
+ngx_http_quic_stream_handler(ngx_connection_t *c)
+{
+    ngx_event_t               *rev;
+    ngx_connection_t          *pc;
+    ngx_http_log_ctx_t        *ctx;
+    ngx_http_connection_t     *hc;
+    ngx_http_v3_connection_t  *h3c;
+
+    pc = c->qs->parent;
+    h3c = pc->data;
+
+    if (c->qs->id & NGX_QUIC_STREAM_UNIDIRECTIONAL) {
+        ngx_http_v3_handle_client_uni_stream(c);
+        return;
+    }
+
+    hc = ngx_pcalloc(c->pool, sizeof(ngx_http_connection_t));
+    if (hc == NULL) {
+        ngx_http_close_connection(c);
+        return;
+    }
+
+    ngx_memcpy(hc, h3c, sizeof(ngx_http_connection_t));
+    c->data = hc;
+
+    ctx = ngx_palloc(c->pool, sizeof(ngx_http_log_ctx_t));
+    if (ctx == NULL) {
+        ngx_http_close_connection(c);
+        return;
+    }
+
+    ctx->connection = c;
+    ctx->request = NULL;
+    ctx->current_request = NULL;
+
+    c->log->connection = c->number;
+    c->log->handler = ngx_http_log_error;
+    c->log->data = ctx;
+    c->log->action = "waiting for request";
+
+    c->log_error = NGX_ERROR_INFO;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 new stream id:0x%uXL", c->qs->id);
+
+    rev = c->read;
+    rev->handler = ngx_http_wait_request_handler;
+    c->write->handler = ngx_http_empty_handler;
+
+    rev->handler(rev);
+}
+
+#endif
+
+
 static void
 ngx_http_wait_request_handler(ngx_event_t *rev)
 {
@@ -619,6 +706,13 @@ ngx_http_alloc_request(ngx_connection_t 
     r->method = NGX_HTTP_UNKNOWN;
     r->http_version = NGX_HTTP_VERSION_10;
 
+#if (NGX_HTTP_V3)
+    if (hc->quic) {
+        r->http_version = NGX_HTTP_VERSION_30;
+        r->headers_in.chunked = 1;
+    }
+#endif
+
     r->headers_in.content_length_n = -1;
     r->headers_in.keep_alive_n = -1;
     r->headers_out.content_length_n = -1;
@@ -1068,7 +1162,16 @@ ngx_http_process_request_line(ngx_event_
             }
         }
 
-        rc = ngx_http_parse_request_line(r, r->header_in);
+        switch (r->http_version) {
+#if (NGX_HTTP_V3)
+        case NGX_HTTP_VERSION_30:
+            rc = ngx_http_v3_parse_header(r, r->header_in);
+            break;
+#endif
+
+        default: /* HTTP/1.x */
+            rc = ngx_http_parse_request_line(r, r->header_in);
+        }
 
         if (rc == NGX_OK) {
 
@@ -1076,13 +1179,13 @@ ngx_http_process_request_line(ngx_event_
 
             r->request_line.len = r->request_end - r->request_start;
             r->request_line.data = r->request_start;
-            r->request_length = r->header_in->pos - r->request_start;
+            r->request_length = r->header_in->pos - r->request_start; /* XXX */
 
             ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
                            "http request line: \"%V\"", &r->request_line);
 
-            r->method_name.len = r->method_end - r->request_start + 1;
-            r->method_name.data = r->request_line.data;
+            r->method_name.len = r->method_end - r->method_start;
+            r->method_name.data = r->method_start;
 
             if (r->http_protocol.data) {
                 r->http_protocol.len = r->request_end - r->http_protocol.data;
@@ -1153,6 +1256,15 @@ ngx_http_process_request_line(ngx_event_
             break;
         }
 
+        if (rc == NGX_DONE) {
+            if (ngx_handle_read_event(rev, 0) != NGX_OK) {
+                ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
+                return;
+            }
+
+            break;
+        }
+
         if (rc != NGX_AGAIN) {
 
             /* there was error while a request line parsing */
@@ -1343,7 +1455,7 @@ ngx_http_process_request_headers(ngx_eve
 
     cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module);
 
-    rc = NGX_AGAIN;
+    rc = NGX_OK;
 
     for ( ;; ) {
 
@@ -1397,11 +1509,21 @@ ngx_http_process_request_headers(ngx_eve
         /* the host header could change the server configuration context */
         cscf = ngx_http_get_module_srv_conf(r, ngx_http_core_module);
 
-        rc = ngx_http_parse_header_line(r, r->header_in,
-                                        cscf->underscores_in_headers);
+        switch (r->http_version) {
+#if (NGX_HTTP_V3)
+        case NGX_HTTP_VERSION_30:
+            rc = ngx_http_v3_parse_header(r, r->header_in);
+            break;
+#endif
+
+        default: /* HTTP/1.x */
+            rc = ngx_http_parse_header_line(r, r->header_in,
+                                            cscf->underscores_in_headers);
+        }
 
         if (rc == NGX_OK) {
 
+            /* XXX */
             r->request_length += r->header_in->pos - r->header_name_start;
 
             if (r->invalid_header && cscf->ignore_invalid_headers) {
@@ -1427,11 +1549,17 @@ ngx_http_process_request_headers(ngx_eve
 
             h->key.len = r->header_name_end - r->header_name_start;
             h->key.data = r->header_name_start;
-            h->key.data[h->key.len] = '\0';
+
+            if (h->key.data[h->key.len]) {
+                h->key.data[h->key.len] = '\0';
+            }
 
             h->value.len = r->header_end - r->header_start;
             h->value.data = r->header_start;
-            h->value.data[h->value.len] = '\0';
+
+            if (h->value.data[h->value.len]) {
+                h->value.data[h->value.len] = '\0';
+            }
 
             h->lowcase_key = ngx_pnalloc(r->pool, h->key.len);
             if (h->lowcase_key == NULL) {
@@ -1467,7 +1595,7 @@ ngx_http_process_request_headers(ngx_eve
             ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                            "http header done");
 
-            r->request_length += r->header_in->pos - r->header_name_start;
+            r->request_length += r->header_in->pos - r->header_name_start; /* XXX */
 
             r->http_state = NGX_HTTP_PROCESS_REQUEST_STATE;
 
@@ -1582,7 +1710,7 @@ ngx_http_alloc_large_header_buffer(ngx_h
         return NGX_OK;
     }
 
-    old = request_line ? r->request_start : r->header_name_start;
+    old = request_line ? r->request_start : r->header_name_start; /* XXX */
 
     cscf = ngx_http_get_module_srv_conf(r, ngx_http_core_module);
 
@@ -1661,45 +1789,59 @@ ngx_http_alloc_large_header_buffer(ngx_h
             r->request_end = new + (r->request_end - old);
         }
 
-        r->method_end = new + (r->method_end - old);
-
-        r->uri_start = new + (r->uri_start - old);
-        r->uri_end = new + (r->uri_end - old);
-
-        if (r->schema_start) {
+        if (r->method_start >= old && r->method_start < r->header_in->pos) {
+            r->method_start = new + (r->method_start - old);
+            r->method_end = new + (r->method_end - old);
+        }
+
+        if (r->uri_start >= old && r->uri_start < r->header_in->pos) {
+            r->uri_start = new + (r->uri_start - old);
+            r->uri_end = new + (r->uri_end - old);
+        }
+
+        if (r->schema_start >= old && r->schema_start < r->header_in->pos) {
             r->schema_start = new + (r->schema_start - old);
             r->schema_end = new + (r->schema_end - old);
         }
 
-        if (r->host_start) {
+        if (r->host_start >= old && r->host_start < r->header_in->pos) {
             r->host_start = new + (r->host_start - old);
             if (r->host_end) {
                 r->host_end = new + (r->host_end - old);
             }
         }
 
-        if (r->port_start) {
+        if (r->port_start >= old && r->port_start < r->header_in->pos) {
             r->port_start = new + (r->port_start - old);
             r->port_end = new + (r->port_end - old);
         }
 
-        if (r->uri_ext) {
+        if (r->uri_ext >= old && r->uri_ext < r->header_in->pos) {
             r->uri_ext = new + (r->uri_ext - old);
         }
 
-        if (r->args_start) {
+        if (r->args_start >= old && r->args_start < r->header_in->pos) {
             r->args_start = new + (r->args_start - old);
         }
 
-        if (r->http_protocol.data) {
+        if (r->http_protocol.data >= old
+            && r->http_protocol.data < r->header_in->pos)
+        {
             r->http_protocol.data = new + (r->http_protocol.data - old);
         }
 
     } else {
-        r->header_name_start = new;
-        r->header_name_end = new + (r->header_name_end - old);
-        r->header_start = new + (r->header_start - old);
-        r->header_end = new + (r->header_end - old);
+        if (r->header_name_start >= old
+            && r->header_name_start < r->header_in->pos)
+        {
+            r->header_name_start = new;
+            r->header_name_end = new + (r->header_name_end - old);
+        }
+
+        if (r->header_start >= old && r->header_start < r->header_in->pos) {
+            r->header_start = new + (r->header_start - old);
+            r->header_end = new + (r->header_end - old);
+        }
     }
 
     r->header_in = b;
@@ -1924,7 +2066,7 @@ ngx_http_process_request_header(ngx_http
         return NGX_ERROR;
     }
 
-    if (r->headers_in.host == NULL && r->http_version > NGX_HTTP_VERSION_10) {
+    if (r->headers_in.host == NULL && r->http_version == NGX_HTTP_VERSION_11) {
         ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
                    "client sent HTTP/1.1 request without \"Host\" header");
         ngx_http_finalize_request(r, NGX_HTTP_BAD_REQUEST);
--- a/src/http/ngx_http_request.h
+++ b/src/http/ngx_http_request.h
@@ -24,6 +24,7 @@
 #define NGX_HTTP_VERSION_10                1000
 #define NGX_HTTP_VERSION_11                1001
 #define NGX_HTTP_VERSION_20                2000
+#define NGX_HTTP_VERSION_30                3000
 
 #define NGX_HTTP_UNKNOWN                   0x0001
 #define NGX_HTTP_GET                       0x0002
@@ -323,6 +324,7 @@ typedef struct {
     ngx_chain_t                      *free;
 
     unsigned                          ssl:1;
+    unsigned                          quic:1;
     unsigned                          proxy_protocol:1;
 } ngx_http_connection_t;
 
@@ -583,6 +585,7 @@ struct ngx_http_request_s {
     u_char                           *args_start;
     u_char                           *request_start;
     u_char                           *request_end;
+    u_char                           *method_start;
     u_char                           *method_end;
     u_char                           *schema_start;
     u_char                           *schema_end;
@@ -591,6 +594,10 @@ struct ngx_http_request_s {
     u_char                           *port_start;
     u_char                           *port_end;
 
+#if (NGX_HTTP_V3)
+    void                             *h3_parse;
+#endif
+
     unsigned                          http_minor:16;
     unsigned                          http_major:16;
 };
--- a/src/http/ngx_http_request_body.c
+++ b/src/http/ngx_http_request_body.c
@@ -343,11 +343,10 @@ ngx_http_do_read_client_request_body(ngx
             }
 
             if (n == 0) {
-                ngx_log_error(NGX_LOG_INFO, c->log, 0,
-                              "client prematurely closed connection");
+                rb->buf->last_buf = 1;
             }
 
-            if (n == 0 || n == NGX_ERROR) {
+            if (n == NGX_ERROR) {
                 c->error = 1;
                 return NGX_HTTP_BAD_REQUEST;
             }
@@ -355,7 +354,7 @@ ngx_http_do_read_client_request_body(ngx
             rb->buf->last += n;
             r->request_length += n;
 
-            if (n == rest) {
+            if (n == rest || n == 0) {
                 /* pass buffer to request body filter chain */
 
                 out.buf = rb->buf;
@@ -805,11 +804,7 @@ ngx_http_test_expect(ngx_http_request_t 
 
     if (r->expect_tested
         || r->headers_in.expect == NULL
-        || r->http_version < NGX_HTTP_VERSION_11
-#if (NGX_HTTP_V2)
-        || r->stream != NULL
-#endif
-       )
+        || r->http_version != NGX_HTTP_VERSION_11)
     {
         return NGX_OK;
     }
@@ -914,6 +909,11 @@ ngx_http_request_body_length_filter(ngx_
             b->last_buf = 1;
         }
 
+        if (cl->buf->last_buf && rb->rest > 0) {
+            /* XXX client prematurely closed connection */
+            return NGX_ERROR;
+        }
+
         *ll = tl;
         ll = &tl->next;
     }
@@ -950,7 +950,16 @@ ngx_http_request_body_chunked_filter(ngx
         }
 
         r->headers_in.content_length_n = 0;
-        rb->rest = 3;
+
+#if (NGX_HTTP_V3)
+        if (r->http_version == NGX_HTTP_VERSION_30) {
+            rb->rest = 1;
+
+        } else
+#endif
+        {
+            rb->rest = 3;
+        }
     }
 
     out = NULL;
new file mode 100644
--- /dev/null
+++ b/src/http/v3/ngx_http_v3.c
@@ -0,0 +1,96 @@
+
+/*
+ * Copyright (C) Roman Arutyunyan
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_http.h>
+
+
+uintptr_t
+ngx_http_v3_encode_varlen_int(u_char *p, uint64_t value)
+{
+    if (value <= 63) {
+        if (p == NULL) {
+            return 1;
+        }
+
+        *p++ = value;
+        return (uintptr_t) p;
+    }
+
+    if (value <= 16383) {
+        if (p == NULL) {
+            return 2;
+        }
+
+        *p++ = 0x40 | (value >> 8);
+        *p++ = value;
+        return (uintptr_t) p;
+    }
+
+    if (value <= 1073741823) {
+        if (p == NULL) {
+            return 3;
+        }
+
+        *p++ = 0x80 | (value >> 16);
+        *p++ = (value >> 8);
+        *p++ = value;
+        return (uintptr_t) p;
+
+    }
+
+    if (p == NULL) {
+        return 4;
+    }
+
+    *p++ = 0xc0 | (value >> 24);
+    *p++ = (value >> 16);
+    *p++ = (value >> 8);
+    *p++ = value;
+    return (uintptr_t) p;
+}
+
+
+uintptr_t
+ngx_http_v3_encode_prefix_int(u_char *p, uint64_t value, ngx_uint_t prefix)
+{
+    ngx_uint_t  thresh, n;
+
+    thresh = (1 << prefix) - 1;
+
+    if (value < thresh) {
+        if (p == NULL) {
+            return 1;
+        }
+
+        *p++ |= value;
+        return (uintptr_t) p;
+    }
+
+    value -= thresh;
+
+    for (n = 10; n > 1; n--) {
+        if (value >> (7 * (n - 1))) {
+            break;
+        }
+    }
+
+    if (p == NULL) {
+        return n + 1;
+    }
+
+    *p++ |= thresh;
+
+    for ( /* void */ ; n > 1; n--) {
+        *p++ = 0x80 | (value >> 7 * (n - 1));
+    }
+
+    *p++ = value & 0x7f;
+
+    return (uintptr_t) p;
+}
new file mode 100644
--- /dev/null
+++ b/src/http/v3/ngx_http_v3.h
@@ -0,0 +1,116 @@
+
+/*
+ * Copyright (C) Roman Arutyunyan
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_HTTP_V3_H_INCLUDED_
+#define _NGX_HTTP_V3_H_INCLUDED_
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_http.h>
+#include <ngx_http_v3_parse.h>
+
+
+#define NGX_HTTP_V3_ALPN(s)         NGX_HTTP_V3_ALPN_DRAFT(s)
+#define NGX_HTTP_V3_ALPN_DRAFT(s)   "\x05h3-" #s
+#define NGX_HTTP_V3_ALPN_ADVERTISE  NGX_HTTP_V3_ALPN(NGX_QUIC_DRAFT_VERSION)
+
+#define NGX_HTTP_V3_VARLEN_INT_LEN                 4
+#define NGX_HTTP_V3_PREFIX_INT_LEN                 11
+
+#define NGX_HTTP_V3_STREAM_CONTROL                 0x00
+#define NGX_HTTP_V3_STREAM_PUSH                    0x01
+#define NGX_HTTP_V3_STREAM_ENCODER                 0x02
+#define NGX_HTTP_V3_STREAM_DECODER                 0x03
+
+#define NGX_HTTP_V3_FRAME_DATA                     0x00
+#define NGX_HTTP_V3_FRAME_HEADERS                  0x01
+#define NGX_HTTP_V3_FRAME_CANCEL_PUSH              0x03
+#define NGX_HTTP_V3_FRAME_SETTINGS                 0x04
+#define NGX_HTTP_V3_FRAME_PUSH_PROMISE             0x05
+#define NGX_HTTP_V3_FRAME_GOAWAY                   0x07
+#define NGX_HTTP_V3_FRAME_MAX_PUSH_ID              0x0d
+
+#define NGX_HTTP_V3_PARAM_MAX_TABLE_CAPACITY       0x01
+#define NGX_HTTP_V3_PARAM_MAX_HEADER_LIST_SIZE     0x06
+#define NGX_HTTP_V3_PARAM_BLOCKED_STREAMS          0x07
+
+#define NGX_HTTP_V3_STREAM_CLIENT_CONTROL          0
+#define NGX_HTTP_V3_STREAM_SERVER_CONTROL          1
+#define NGX_HTTP_V3_STREAM_CLIENT_ENCODER          2
+#define NGX_HTTP_V3_STREAM_SERVER_ENCODER          3
+#define NGX_HTTP_V3_STREAM_CLIENT_DECODER          4
+#define NGX_HTTP_V3_STREAM_SERVER_DECODER          5
+#define NGX_HTTP_V3_MAX_KNOWN_STREAM               6
+
+
+typedef struct {
+    ngx_quic_tp_t           quic;
+} ngx_http_v3_srv_conf_t;
+
+
+typedef struct {
+    ngx_http_connection_t   hc;
+
+    ngx_array_t            *dynamic;
+    ngx_connection_t       *known_streams[NGX_HTTP_V3_MAX_KNOWN_STREAM];
+} ngx_http_v3_connection_t;
+
+
+typedef struct {
+    ngx_str_t               name;
+    ngx_str_t               value;
+} ngx_http_v3_header_t;
+
+
+ngx_int_t ngx_http_v3_parse_header(ngx_http_request_t *r, ngx_buf_t *b);
+ngx_int_t ngx_http_v3_parse_request_body(ngx_http_request_t *r, ngx_buf_t *b,
+    ngx_http_chunked_t *ctx);
+ngx_chain_t *ngx_http_v3_create_header(ngx_http_request_t *r);
+ngx_chain_t *ngx_http_v3_create_trailers(ngx_http_request_t *r);
+
+uintptr_t ngx_http_v3_encode_varlen_int(u_char *p, uint64_t value);
+uintptr_t ngx_http_v3_encode_prefix_int(u_char *p, uint64_t value,
+    ngx_uint_t prefix);
+
+void ngx_http_v3_handle_client_uni_stream(ngx_connection_t *c);
+
+ngx_int_t ngx_http_v3_ref_insert(ngx_connection_t *c, ngx_uint_t dynamic,
+    ngx_uint_t index, ngx_str_t *value);
+ngx_int_t ngx_http_v3_insert(ngx_connection_t *c, ngx_str_t *name,
+    ngx_str_t *value);
+ngx_int_t ngx_http_v3_set_capacity(ngx_connection_t *c, ngx_uint_t capacity);
+ngx_int_t ngx_http_v3_duplicate(ngx_connection_t *c, ngx_uint_t index);
+ngx_int_t ngx_http_v3_ack_header(ngx_connection_t *c, ngx_uint_t stream_id);
+ngx_int_t ngx_http_v3_cancel_stream(ngx_connection_t *c, ngx_uint_t stream_id);
+ngx_int_t ngx_http_v3_inc_insert_count(ngx_connection_t *c, ngx_uint_t inc);
+ngx_http_v3_header_t *ngx_http_v3_lookup_table(ngx_connection_t *c,
+    ngx_uint_t dynamic, ngx_uint_t index);
+ngx_int_t ngx_http_v3_check_insert_count(ngx_connection_t *c,
+    ngx_uint_t insert_count);
+ngx_int_t ngx_http_v3_set_param(ngx_connection_t *c, uint64_t id,
+    uint64_t value);
+
+ngx_int_t ngx_http_v3_client_ref_insert(ngx_connection_t *c, ngx_uint_t dynamic,
+    ngx_uint_t index, ngx_str_t *value);
+ngx_int_t ngx_http_v3_client_insert(ngx_connection_t *c, ngx_str_t *name,
+    ngx_str_t *value);
+ngx_int_t ngx_http_v3_client_set_capacity(ngx_connection_t *c,
+    ngx_uint_t capacity);
+ngx_int_t ngx_http_v3_client_duplicate(ngx_connection_t *c, ngx_uint_t index);
+ngx_int_t ngx_http_v3_client_ack_header(ngx_connection_t *c,
+    ngx_uint_t stream_id);
+ngx_int_t ngx_http_v3_client_cancel_stream(ngx_connection_t *c,
+    ngx_uint_t stream_id);
+ngx_int_t ngx_http_v3_client_inc_insert_count(ngx_connection_t *c,
+    ngx_uint_t inc);
+
+
+extern ngx_module_t  ngx_http_v3_module;
+
+
+#endif /* _NGX_HTTP_V3_H_INCLUDED_ */
new file mode 100644
--- /dev/null
+++ b/src/http/v3/ngx_http_v3_module.c
@@ -0,0 +1,302 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ * Copyright (C) Roman Arutyunyan
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_http.h>
+
+
+static ngx_command_t  ngx_http_v3_commands[] = {
+
+    { ngx_string("quic_max_idle_timeout"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1,
+      ngx_conf_set_msec_slot,
+      NGX_HTTP_SRV_CONF_OFFSET,
+      offsetof(ngx_http_v3_srv_conf_t, quic.max_idle_timeout),
+      NULL },
+
+    { ngx_string("quic_max_ack_delay"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1,
+      ngx_conf_set_msec_slot,
+      NGX_HTTP_SRV_CONF_OFFSET,
+      offsetof(ngx_http_v3_srv_conf_t, quic.max_ack_delay),
+      NULL },
+
+    { ngx_string("quic_max_packet_size"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1,
+      ngx_conf_set_num_slot,
+      NGX_HTTP_SRV_CONF_OFFSET,
+      offsetof(ngx_http_v3_srv_conf_t, quic.max_packet_size),
+      NULL },
+
+    { ngx_string("quic_initial_max_data"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1,
+      ngx_conf_set_num_slot,
+      NGX_HTTP_SRV_CONF_OFFSET,
+      offsetof(ngx_http_v3_srv_conf_t, quic.initial_max_data),
+      NULL },
+
+    { ngx_string("quic_initial_max_stream_data_bidi_local"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1,
+      ngx_conf_set_num_slot,
+      NGX_HTTP_SRV_CONF_OFFSET,
+      offsetof(ngx_http_v3_srv_conf_t, quic.initial_max_stream_data_bidi_local),
+      NULL },
+
+    { ngx_string("quic_initial_max_stream_data_bidi_remote"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1,
+      ngx_conf_set_num_slot,
+      NGX_HTTP_SRV_CONF_OFFSET,
+      offsetof(ngx_http_v3_srv_conf_t, quic.initial_max_stream_data_bidi_remote),
+      NULL },
+
+    { ngx_string("quic_initial_max_stream_data_uni"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1,
+      ngx_conf_set_num_slot,
+      NGX_HTTP_SRV_CONF_OFFSET,
+      offsetof(ngx_http_v3_srv_conf_t, quic.initial_max_stream_data_uni),
+      NULL },
+
+    { ngx_string("quic_initial_max_streams_bidi"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1,
+      ngx_conf_set_num_slot,
+      NGX_HTTP_SRV_CONF_OFFSET,
+      offsetof(ngx_http_v3_srv_conf_t, quic.initial_max_streams_bidi),
+      NULL },
+
+    { ngx_string("quic_initial_max_streams_uni"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1,
+      ngx_conf_set_num_slot,
+      NGX_HTTP_SRV_CONF_OFFSET,
+      offsetof(ngx_http_v3_srv_conf_t, quic.initial_max_streams_uni),
+      NULL },
+
+    { ngx_string("quic_ack_delay_exponent"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1,
+      ngx_conf_set_num_slot,
+      NGX_HTTP_SRV_CONF_OFFSET,
+      offsetof(ngx_http_v3_srv_conf_t, quic.ack_delay_exponent),
+      NULL },
+
+    { ngx_string("quic_active_migration"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1,
+      ngx_conf_set_num_slot,
+      NGX_HTTP_SRV_CONF_OFFSET,
+      offsetof(ngx_http_v3_srv_conf_t, quic.disable_active_migration),
+      NULL },
+
+    { ngx_string("quic_active_connection_id_limit"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1,
+      ngx_conf_set_num_slot,
+      NGX_HTTP_SRV_CONF_OFFSET,
+      offsetof(ngx_http_v3_srv_conf_t, quic.active_connection_id_limit),
+      NULL },
+
+      ngx_null_command
+};
+
+
+static ngx_int_t ngx_http_variable_quic(ngx_http_request_t *r,
+    ngx_http_variable_value_t *v, uintptr_t data);
+static ngx_int_t ngx_http_variable_http3(ngx_http_request_t *r,
+    ngx_http_variable_value_t *v, uintptr_t data);
+static ngx_int_t ngx_http_v3_add_variables(ngx_conf_t *cf);
+static void *ngx_http_v3_create_srv_conf(ngx_conf_t *cf);
+static char *ngx_http_v3_merge_srv_conf(ngx_conf_t *cf,
+    void *parent, void *child);
+
+
+static ngx_http_module_t  ngx_http_v3_module_ctx = {
+    ngx_http_v3_add_variables,             /* preconfiguration */
+    NULL,                                  /* postconfiguration */
+
+    NULL,                                  /* create main configuration */
+    NULL,                                  /* init main configuration */
+
+    ngx_http_v3_create_srv_conf,           /* create server configuration */
+    ngx_http_v3_merge_srv_conf,            /* merge server configuration */
+
+    NULL,                                  /* create location configuration */
+    NULL                                   /* merge location configuration */
+};
+
+
+ngx_module_t  ngx_http_v3_module = {
+    NGX_MODULE_V1,
+    &ngx_http_v3_module_ctx,               /* module context */
+    ngx_http_v3_commands,                  /* module directives */
+    NGX_HTTP_MODULE,                       /* module type */
+    NULL,                                  /* init master */
+    NULL,                                  /* init module */
+    NULL,                                  /* init process */
+    NULL,                                  /* init thread */
+    NULL,                                  /* exit thread */
+    NULL,                                  /* exit process */
+    NULL,                                  /* exit master */
+    NGX_MODULE_V1_PADDING
+};
+
+
+static ngx_http_variable_t  ngx_http_v3_vars[] = {
+    { ngx_string("quic"), NULL, ngx_http_variable_quic,
+      0, 0, 0 },
+
+    { ngx_string("http3"), NULL, ngx_http_variable_http3,
+      0, 0, 0 },
+
+      ngx_http_null_variable
+};
+
+
+static ngx_int_t
+ngx_http_variable_quic(ngx_http_request_t *r,
+    ngx_http_variable_value_t *v, uintptr_t data)
+{
+    if (r->connection->qs) {
+
+        v->len = 4;
+        v->valid = 1;
+        v->no_cacheable = 1;
+        v->not_found = 0;
+        v->data = (u_char *) "quic";
+        return NGX_OK;
+    }
+
+    v->not_found = 1;
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_http_variable_http3(ngx_http_request_t *r,
+    ngx_http_variable_value_t *v, uintptr_t data)
+{
+    v->valid = 1;
+    v->no_cacheable = 1;
+    v->not_found = 0;
+
+    v->data = ngx_pnalloc(r->pool, sizeof("h3-xx") - 1);
+    if (v->data == NULL) {
+        return NGX_ERROR;
+    }
+
+    v->len = ngx_sprintf(v->data, "h3-%d", NGX_QUIC_DRAFT_VERSION) - v->data;
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_http_v3_add_variables(ngx_conf_t *cf)
+{
+    ngx_http_variable_t  *var, *v;
+
+    for (v = ngx_http_v3_vars; v->name.len; v++) {
+        var = ngx_http_add_variable(cf, &v->name, v->flags);
+        if (var == NULL) {
+            return NGX_ERROR;
+        }
+
+        var->get_handler = v->get_handler;
+        var->data = v->data;
+    }
+
+    return NGX_OK;
+}
+
+
+
+static void *
+ngx_http_v3_create_srv_conf(ngx_conf_t *cf)
+{
+    ngx_http_v3_srv_conf_t  *v3cf;
+
+    v3cf = ngx_pcalloc(cf->pool, sizeof(ngx_http_v3_srv_conf_t));
+    if (v3cf == NULL) {
+        return NULL;
+    }
+
+    /*
+     * set by ngx_pcalloc():
+     *  v3cf->quic.original_connection_id = 0;
+     *  v3cf->quic.stateless_reset_token = { 0 }
+     *  conf->quic.preferred_address = NULL
+     */
+
+    v3cf->quic.max_idle_timeout = NGX_CONF_UNSET_MSEC;
+    v3cf->quic.max_ack_delay = NGX_CONF_UNSET_MSEC;
+
+    v3cf->quic.max_packet_size = NGX_CONF_UNSET_UINT;
+    v3cf->quic.initial_max_data = NGX_CONF_UNSET_UINT;
+    v3cf->quic.initial_max_stream_data_bidi_local = NGX_CONF_UNSET_UINT;
+    v3cf->quic.initial_max_stream_data_bidi_remote = NGX_CONF_UNSET_UINT;
+    v3cf->quic.initial_max_stream_data_uni = NGX_CONF_UNSET_UINT;
+    v3cf->quic.initial_max_streams_bidi = NGX_CONF_UNSET_UINT;
+    v3cf->quic.initial_max_streams_uni = NGX_CONF_UNSET_UINT;
+    v3cf->quic.ack_delay_exponent = NGX_CONF_UNSET_UINT;
+    v3cf->quic.disable_active_migration = NGX_CONF_UNSET_UINT;
+    v3cf->quic.active_connection_id_limit = NGX_CONF_UNSET_UINT;
+
+    return v3cf;
+}
+
+
+static char *
+ngx_http_v3_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child)
+{
+    ngx_http_v3_srv_conf_t *prev = parent;
+    ngx_http_v3_srv_conf_t *conf = child;
+
+    ngx_conf_merge_msec_value(conf->quic.max_idle_timeout,
+                              prev->quic.max_idle_timeout, 60000);
+
+    // > 2 ^ 14 is invalid
+    ngx_conf_merge_msec_value(conf->quic.max_ack_delay,
+                              prev->quic.max_ack_delay,
+                              NGX_QUIC_DEFAULT_MAX_ACK_DELAY);
+
+    // < 1200 is invalid
+    ngx_conf_merge_uint_value(conf->quic.max_packet_size,
+                              prev->quic.max_packet_size,
+                              NGX_QUIC_DEFAULT_MAX_PACKET_SIZE);
+
+    ngx_conf_merge_uint_value(conf->quic.initial_max_data,
+                              prev->quic.initial_max_data, 10000000);
+
+    ngx_conf_merge_uint_value(conf->quic.initial_max_stream_data_bidi_local,
+                              prev->quic.initial_max_stream_data_bidi_local,
+                              255);
+
+    ngx_conf_merge_uint_value(conf->quic.initial_max_stream_data_bidi_remote,
+                              prev->quic.initial_max_stream_data_bidi_remote,
+                              255);
+
+    ngx_conf_merge_uint_value(conf->quic.initial_max_stream_data_uni,
+                              prev->quic.initial_max_stream_data_uni, 255);
+
+    ngx_conf_merge_uint_value(conf->quic.initial_max_streams_bidi,
+                              prev->quic.initial_max_streams_bidi, 16);
+
+    ngx_conf_merge_uint_value(conf->quic.initial_max_streams_uni,
+                              prev->quic.initial_max_streams_uni, 16);
+
+    // > 20 is invalid
+    ngx_conf_merge_uint_value(conf->quic.ack_delay_exponent,
+                              prev->quic.ack_delay_exponent,
+                              NGX_QUIC_DEFAULT_ACK_DELAY_EXPONENT);
+
+    ngx_conf_merge_uint_value(conf->quic.disable_active_migration,
+                              prev->quic.disable_active_migration, 1);
+
+    // < 2 is invalid
+    ngx_conf_merge_uint_value(conf->quic.active_connection_id_limit,
+                              prev->quic.active_connection_id_limit, 2);
+
+    return NGX_CONF_OK;
+}
+
new file mode 100644
--- /dev/null
+++ b/src/http/v3/ngx_http_v3_parse.c
@@ -0,0 +1,1481 @@
+
+/*
+ * Copyright (C) Roman Arutyunyan
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_http.h>
+
+
+ngx_int_t
+ngx_http_v3_parse_varlen_int(ngx_connection_t *c,
+    ngx_http_v3_parse_varlen_int_t *st, u_char ch)
+{
+    enum {
+        sw_start = 0,
+        sw_length_2,
+        sw_length_3,
+        sw_length_4,
+        sw_length_5,
+        sw_length_6,
+        sw_length_7,
+        sw_length_8
+    };
+
+    switch (st->state) {
+
+    case sw_start:
+
+        st->value = ch;
+        if (st->value & 0xc0) {
+            st->state = sw_length_2;
+            break;
+        }
+
+        goto done;
+
+    case sw_length_2:
+
+        st->value = (st->value << 8) + ch;
+        if ((st->value & 0xc000) == 0x4000) {
+            st->value &= 0x3fff;
+            goto done;
+        }
+
+        st->state = sw_length_3;
+        break;
+
+    case sw_length_4:
+
+        st->value = (st->value << 8) + ch;
+        if ((st->value & 0xc0000000) == 0x80000000) {
+            st->value &= 0x3fffffff;
+            goto done;
+        }
+
+        st->state = sw_length_5;
+        break;
+
+    case sw_length_3:
+    case sw_length_5:
+    case sw_length_6:
+    case sw_length_7:
+
+        st->value = (st->value << 8) + ch;
+        st->state++;
+        break;
+
+    case sw_length_8:
+
+        st->value = (st->value << 8) + ch;
+        st->value &= 0x3fffffffffffffff;
+        goto done;
+    }
+
+    return NGX_AGAIN;
+
+done:
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 parse varlen int %uL", st->value);
+
+    st->state = sw_start;
+    return NGX_DONE;
+}
+
+
+ngx_int_t
+ngx_http_v3_parse_prefix_int(ngx_connection_t *c,
+    ngx_http_v3_parse_prefix_int_t *st, ngx_uint_t prefix, u_char ch)
+{
+    enum {
+        sw_start = 0,
+        sw_value
+    };
+
+    switch (st->state) {
+
+    case sw_start:
+
+        st->mask = (1 << prefix) - 1;
+        st->value = (ch & st->mask);
+
+        if (st->value != st->mask) {
+            goto done;
+        }
+
+        st->value = 0;
+        st->state = sw_value;
+        break;
+
+    case sw_value:
+
+        st->value = (st->value << 7) + (ch & 0x7f);
+        if (ch & 0x80) {
+            break;
+        }
+
+        st->value += st->mask;
+        goto done;
+    }
+
+    return NGX_AGAIN;
+
+done:
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 parse prefix int %uL", st->value);
+
+    st->state = sw_start;
+    return NGX_DONE;
+}
+
+
+ngx_int_t
+ngx_http_v3_parse_headers(ngx_connection_t *c, ngx_http_v3_parse_headers_t *st,
+    u_char ch)
+{
+    ngx_int_t  rc;
+    enum {
+        sw_start = 0,
+        sw_length,
+        sw_prefix,
+        sw_header_rep,
+        sw_done
+    };
+
+    switch (st->state) {
+
+    case sw_start:
+
+        ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 parse headers");
+
+        if (ch != NGX_HTTP_V3_FRAME_HEADERS) {
+            return NGX_ERROR;
+        }
+
+        st->state = sw_length;
+        break;
+
+    case sw_length:
+
+        if (ngx_http_v3_parse_varlen_int(c, &st->vlint, ch) != NGX_DONE) {
+            break;
+        }
+
+        st->length = st->vlint.value;
+
+        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 parse headers len:%ui", st->length);
+
+        st->state = sw_prefix;
+        break;
+
+    case sw_prefix:
+
+        if (st->length-- == 0) {
+            return NGX_ERROR;
+        }
+
+        rc = ngx_http_v3_parse_header_block_prefix(c, &st->prefix, ch);
+
+        if (rc == NGX_ERROR) {
+            return NGX_ERROR;
+        }
+
+        if (rc != NGX_DONE) {
+            break;
+        }
+
+        if (st->length == 0) {
+            return NGX_ERROR;
+        }
+
+        st->state = sw_header_rep;
+        break;
+
+    case sw_header_rep:
+
+        rc = ngx_http_v3_parse_header_rep(c, &st->header_rep, st->prefix.base,
+                                          ch);
+
+        if (rc == NGX_ERROR) {
+            return NGX_ERROR;
+        }
+
+        if (--st->length == 0) {
+            if (rc != NGX_DONE) {
+                return NGX_ERROR;
+            }
+
+            goto done;
+        }
+
+        if (rc == NGX_DONE) {
+            return NGX_OK;
+        }
+
+        break;
+    }
+
+    return NGX_AGAIN;
+
+done:
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 parse headers done");
+
+    st->state = sw_start;
+    return NGX_DONE;
+}
+
+
+ngx_int_t
+ngx_http_v3_parse_header_block_prefix(ngx_connection_t *c,
+    ngx_http_v3_parse_header_block_prefix_t *st, u_char ch)
+{
+    enum {
+        sw_start = 0,
+        sw_req_insert_count,
+        sw_delta_base,
+        sw_read_delta_base
+    };
+
+    switch (st->state) {
+
+    case sw_start:
+
+        ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 parse header block prefix");
+
+        st->state = sw_req_insert_count;
+
+        /* fall through */
+
+    case sw_req_insert_count:
+
+        if (ngx_http_v3_parse_prefix_int(c, &st->pint, 8, ch) != NGX_DONE) {
+            break;
+        }
+
+        st->insert_count = st->pint.value;
+        st->state = sw_delta_base;
+        break;
+
+    case sw_delta_base:
+
+        st->sign = (ch & 0x80) ? 1 : 0;
+        st->state = sw_read_delta_base;
+
+        /* fall through */
+
+    case sw_read_delta_base:
+
+        if (ngx_http_v3_parse_prefix_int(c, &st->pint, 7, ch) != NGX_DONE) {
+            break;
+        }
+
+        st->delta_base = st->pint.value;
+        goto done;
+    }
+
+    return NGX_AGAIN;
+
+done:
+
+    if (st->sign) {
+        st->base = st->insert_count - st->delta_base - 1;
+    } else {
+        st->base = st->insert_count + st->delta_base;
+    }
+
+    ngx_log_debug4(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                  "http3 parse header block prefix done "
+                  "i:%ui, s:%ui, d:%ui, base:%uL",
+                  st->insert_count, st->sign, st->delta_base, st->base);
+
+    st->state = sw_start;
+    return NGX_DONE;
+}
+
+
+ngx_int_t
+ngx_http_v3_parse_header_rep(ngx_connection_t *c,
+    ngx_http_v3_parse_header_rep_t *st, ngx_uint_t base, u_char ch)
+{
+    ngx_int_t  rc;
+    enum {
+        sw_start = 0,
+        sw_header_ri,
+        sw_header_lri,
+        sw_header_l,
+        sw_header_pbi,
+        sw_header_lpbi
+    };
+
+    if (st->state == sw_start) {
+
+        ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 parse header representation");
+
+        ngx_memzero(&st->header, sizeof(ngx_http_v3_parse_header_t));
+
+        st->header.base = base;
+
+        if (ch & 0x80) {
+            /* Indexed Header Field */
+
+            st->state = sw_header_ri;
+
+        } else if (ch & 0x40) {
+            /* Literal Header Field With Name Reference */
+
+            st->state = sw_header_lri;
+
+        } else if (ch & 0x20) {
+            /* Literal Header Field Without Name Reference */
+
+            st->state = sw_header_l;
+
+        } else if (ch & 0x10) {
+            /* Indexed Header Field With Post-Base Index */
+
+            st->state = sw_header_pbi;
+
+        } else {
+            /* Literal Header Field With Post-Base Name Reference */
+
+            st->state = sw_header_lpbi;
+        }
+    }
+
+    switch (st->state) {
+
+    case sw_header_ri:
+        rc = ngx_http_v3_parse_header_ri(c, &st->header, ch);
+        break;
+
+    case sw_header_lri:
+        rc = ngx_http_v3_parse_header_lri(c, &st->header, ch);
+        break;
+
+    case sw_header_l:
+        rc = ngx_http_v3_parse_header_l(c, &st->header, ch);
+        break;
+
+    case sw_header_pbi:
+        rc = ngx_http_v3_parse_header_pbi(c, &st->header, ch);
+        break;
+
+    case sw_header_lpbi:
+        rc = ngx_http_v3_parse_header_lpbi(c, &st->header, ch);
+        break;
+
+    default:
+        rc = NGX_OK;
+    }
+
+    if (rc == NGX_ERROR) {
+        return NGX_ERROR;
+    }
+
+    if (rc == NGX_AGAIN) {
+        return NGX_AGAIN;
+    }
+
+    /* rc == NGX_DONE */
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 parse header representation done");
+
+    st->state = sw_start;
+    return NGX_DONE;
+}
+
+
+ngx_int_t
+ngx_http_v3_parse_literal(ngx_connection_t *c, ngx_http_v3_parse_literal_t *st,
+    u_char ch)
+{
+    ngx_uint_t  n;
+    enum {
+        sw_start = 0,
+        sw_value
+    };
+
+    switch (st->state) {
+
+    case sw_start:
+
+        ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 parse literal huff:%ui, len:%ui",
+                       st->huffman, st->length);
+
+        n = st->length;
+
+        if (st->huffman) {
+            n = n * 8 / 5;
+            st->huffstate = 0;
+        }
+
+        st->last = ngx_pnalloc(c->pool, n + 1);
+        if (st->last == NULL) {
+            return NGX_ERROR;
+        }
+
+        st->value.data = st->last;
+        st->state = sw_value;
+
+        /* fall through */
+
+    case sw_value:
+
+        if (st->huffman) {
+            if (ngx_http_v2_huff_decode(&st->huffstate, &ch, 1, &st->last,
+                                        st->length == 1, c->log)
+                != NGX_OK)
+            {
+                return NGX_ERROR;
+            }
+
+        } else {
+            *st->last++ = ch;
+        }
+
+        if (--st->length) {
+            break;
+        }
+
+        st->value.len = st->last - st->value.data;
+        *st->last = '\0';
+        goto done;
+    }
+
+    return NGX_AGAIN;
+
+done:
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 parse literal done \"%V\"", &st->value);
+
+    st->state = sw_start;
+    return NGX_DONE;
+}
+
+
+ngx_int_t
+ngx_http_v3_parse_header_ri(ngx_connection_t *c, ngx_http_v3_parse_header_t *st,
+    u_char ch)
+{
+    ngx_http_v3_header_t  *h;
+    enum {
+        sw_start = 0,
+        sw_index
+    };
+
+    switch (st->state) {
+
+    case sw_start:
+
+        ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 parse header ri");
+
+        st->dynamic = (ch & 0x40) ? 0 : 1;
+        st->state = sw_index;
+
+        /* fall through */
+
+    case sw_index:
+
+        if (ngx_http_v3_parse_prefix_int(c, &st->pint, 6, ch) != NGX_DONE) {
+            break;
+        }
+
+        st->index = st->pint.value;
+        goto done;
+    }
+
+    return NGX_AGAIN;
+
+done:
+
+    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 parse header ri done %s%ui]",
+                   st->dynamic ? "dynamic[-" : "static[", st->index);
+
+    if (st->dynamic) {
+        st->index = st->base - st->index - 1;
+    }
+
+    h = ngx_http_v3_lookup_table(c, st->dynamic, st->index);
+    if (h == NULL) {
+        return NGX_ERROR;
+    }
+
+    st->name = h->name;
+    st->value = h->value;
+    st->state = sw_start;
+
+    return NGX_DONE;
+}
+
+
+ngx_int_t
+ngx_http_v3_parse_header_lri(ngx_connection_t *c,
+    ngx_http_v3_parse_header_t *st, u_char ch)
+{
+    ngx_int_t              rc;
+    ngx_http_v3_header_t  *h;
+    enum {
+        sw_start = 0,
+        sw_index,
+        sw_value_len,
+        sw_read_value_len,
+        sw_value
+    };
+
+    switch (st->state) {
+
+    case sw_start:
+
+        ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 parse header lri");
+
+        st->dynamic = (ch & 0x10) ? 0 : 1;
+        st->state = sw_index;
+
+        /* fall through */
+
+    case sw_index:
+
+        if (ngx_http_v3_parse_prefix_int(c, &st->pint, 4, ch) != NGX_DONE) {
+            break;
+        }
+
+        st->index = st->pint.value;
+        st->state = sw_value_len;
+        break;
+
+    case sw_value_len:
+
+        st->literal.huffman = (ch & 0x80) ? 1 : 0;
+        st->state = sw_read_value_len;
+
+        /* fall through */
+
+    case sw_read_value_len:
+
+        if (ngx_http_v3_parse_prefix_int(c, &st->pint, 7, ch) != NGX_DONE) {
+            break;
+        }
+
+        st->literal.length = st->pint.value;
+        if (st->literal.length == 0) {
+            goto done;
+        }
+
+        st->state = sw_value;
+        break;
+
+    case sw_value:
+
+        rc = ngx_http_v3_parse_literal(c, &st->literal, ch);
+
+        if (rc == NGX_ERROR) {
+            return NGX_ERROR;
+        }
+
+        if (rc == NGX_DONE) {
+            st->value = st->literal.value;
+            goto done;
+        }
+
+        break;
+    }
+
+    return NGX_AGAIN;
+
+done:
+
+    ngx_log_debug3(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 parse header lri done %s%ui] \"%V\"",
+                   st->dynamic ? "dynamic[-" : "static[",
+                   st->index, &st->value);
+
+    if (st->dynamic) {
+        st->index = st->base - st->index - 1;
+    }
+
+    h = ngx_http_v3_lookup_table(c, st->dynamic, st->index);
+    if (h == NULL) {
+        return NGX_ERROR;
+    }
+
+    st->name = h->name;
+    st->state = sw_start;
+    return NGX_DONE;
+}
+
+
+ngx_int_t
+ngx_http_v3_parse_header_l(ngx_connection_t *c,
+    ngx_http_v3_parse_header_t *st, u_char ch)
+{
+    ngx_int_t  rc;
+    enum {
+        sw_start = 0,
+        sw_name_len,
+        sw_name,
+        sw_value_len,
+        sw_read_value_len,
+        sw_value
+    };
+
+    switch (st->state) {
+
+    case sw_start:
+
+        ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 parse header l");
+
+        st->literal.huffman = (ch & 0x08) ? 1 : 0;
+        st->state = sw_name_len;
+
+        /* fall through */
+
+    case sw_name_len:
+
+        if (ngx_http_v3_parse_prefix_int(c, &st->pint, 3, ch) != NGX_DONE) {
+            break;
+        }
+
+        st->literal.length = st->pint.value;
+        if (st->literal.length == 0) {
+            return NGX_ERROR;
+        }
+
+        st->state = sw_name;
+        break;
+
+    case sw_name:
+
+        rc = ngx_http_v3_parse_literal(c, &st->literal, ch);
+
+        if (rc == NGX_ERROR) {
+            return NGX_ERROR;
+        }
+
+        if (rc == NGX_DONE) {
+            st->name = st->literal.value;
+            st->state = sw_value_len;
+        }
+
+        break;
+
+    case sw_value_len:
+
+        st->literal.huffman = (ch & 0x80) ? 1 : 0;
+        st->state = sw_read_value_len;
+
+        /* fall through */
+
+    case sw_read_value_len:
+
+        if (ngx_http_v3_parse_prefix_int(c, &st->pint, 7, ch) != NGX_DONE) {
+            break;
+        }
+
+        st->literal.length = st->pint.value;
+        if (st->literal.length == 0) {
+            goto done;
+        }
+
+        st->state = sw_value;
+        break;
+
+    case sw_value:
+
+        rc = ngx_http_v3_parse_literal(c, &st->literal, ch);
+
+        if (rc == NGX_ERROR) {
+            return NGX_ERROR;
+        }
+
+        if (rc == NGX_DONE) {
+            st->value = st->literal.value;
+            goto done;
+        }
+
+        break;
+    }
+
+    return NGX_AGAIN;
+
+done:
+
+    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 parse header l done \"%V\" \"%V\"",
+                   &st->name, &st->value);
+
+    st->state = sw_start;
+    return NGX_DONE;
+}
+
+
+ngx_int_t
+ngx_http_v3_parse_header_pbi(ngx_connection_t *c,
+    ngx_http_v3_parse_header_t *st, u_char ch)
+{
+    ngx_http_v3_header_t  *h;
+    enum {
+        sw_start = 0,
+        sw_index
+    };
+
+    switch (st->state) {
+
+    case sw_start:
+
+        ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 parse header pbi");
+
+        st->state = sw_index;
+
+        /* fall through */
+
+    case sw_index:
+
+        if (ngx_http_v3_parse_prefix_int(c, &st->pint, 4, ch) != NGX_DONE) {
+            break;
+        }
+
+        st->index = st->pint.value;
+        goto done;
+    }
+
+    return NGX_AGAIN;
+
+done:
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 parse header pbi done dynamic[+%ui]", st->index);
+
+    h = ngx_http_v3_lookup_table(c, 1, st->base + st->index);
+    if (h == NULL) {
+        return NGX_ERROR;
+    }
+
+    st->name = h->name;
+    st->value = h->value;
+    st->state = sw_start;
+    return NGX_DONE;
+}
+
+
+ngx_int_t
+ngx_http_v3_parse_header_lpbi(ngx_connection_t *c,
+    ngx_http_v3_parse_header_t *st, u_char ch)
+{
+    ngx_int_t              rc;
+    ngx_http_v3_header_t  *h;
+    enum {
+        sw_start = 0,
+        sw_index,
+        sw_value_len,
+        sw_read_value_len,
+        sw_value
+    };
+
+    switch (st->state) {
+
+    case sw_start:
+
+        ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 parse header lpbi");
+
+        st->state = sw_index;
+
+        /* fall through */
+
+    case sw_index:
+
+        if (ngx_http_v3_parse_prefix_int(c, &st->pint, 3, ch) != NGX_DONE) {
+            break;
+        }
+
+        st->index = st->pint.value;
+        st->state = sw_value_len;
+        break;
+
+    case sw_value_len:
+
+        st->literal.huffman = (ch & 0x80) ? 1 : 0;
+        st->state = sw_read_value_len;
+
+        /* fall through */
+
+    case sw_read_value_len:
+
+        if (ngx_http_v3_parse_prefix_int(c, &st->pint, 7, ch) != NGX_DONE) {
+            break;
+        }
+
+        st->literal.length = st->pint.value;
+        if (st->literal.length == 0) {
+            goto done;
+        }
+
+        st->state = sw_value;
+        break;
+
+    case sw_value:
+
+        rc = ngx_http_v3_parse_literal(c, &st->literal, ch);
+
+        if (rc == NGX_ERROR) {
+            return NGX_ERROR;
+        }
+
+        if (rc == NGX_DONE) {
+            st->value = st->literal.value;
+            goto done;
+        }
+
+        break;
+    }
+
+    return NGX_AGAIN;
+
+done:
+
+    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 parse header lpbi done dynamic[+%ui] \"%V\"",
+                   st->index, &st->value);
+
+    h = ngx_http_v3_lookup_table(c, 1, st->base + st->index);
+    if (h == NULL) {
+        return NGX_ERROR;
+    }
+
+    st->name = h->name;
+    st->state = sw_start;
+    return NGX_DONE;
+}
+
+
+ngx_int_t
+ngx_http_v3_parse_control(ngx_connection_t *c, void *data, u_char ch)
+{
+    ngx_http_v3_parse_control_t *st = data;
+
+    ngx_int_t  rc;
+    enum {
+        sw_start = 0,
+        sw_type,
+        sw_length,
+        sw_settings,
+        sw_max_push_id,
+        sw_skip
+    };
+
+    switch (st->state) {
+
+    case sw_start:
+
+        ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 parse control");
+
+        st->state = sw_type;
+
+        /* fall through */
+
+    case sw_type:
+
+        if (ngx_http_v3_parse_varlen_int(c, &st->vlint, ch) != NGX_DONE) {
+            break;
+        }
+
+        st->type = st->vlint.value;
+
+        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 parse frame type:%ui", st->type);
+
+        st->state = sw_length;
+        break;
+
+    case sw_length:
+
+        if (ngx_http_v3_parse_varlen_int(c, &st->vlint, ch) != NGX_DONE) {
+            break;
+        }
+
+        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 parse frame len:%uL", st->vlint.value);
+
+        st->length = st->vlint.value;
+        if (st->length == 0) {
+            st->state = sw_type;
+            break;
+        }
+
+        switch (st->type) {
+
+        case NGX_HTTP_V3_FRAME_SETTINGS:
+            st->state = sw_settings;
+            break;
+
+        case NGX_HTTP_V3_FRAME_MAX_PUSH_ID:
+            st->state = sw_max_push_id;
+            break;
+
+        default:
+            ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                           "http3 parse skip unknown frame");
+            st->state = sw_skip;
+        }
+
+        break;
+
+    case sw_settings:
+
+        rc = ngx_http_v3_parse_settings(c, &st->settings, ch);
+
+        if (rc == NGX_ERROR) {
+            return NGX_ERROR;
+        }
+
+        if (--st->length > 0) {
+            break;
+        }
+
+        if (rc != NGX_DONE) {
+            return NGX_ERROR;
+        }
+
+        st->state = sw_type;
+        break;
+
+    case sw_max_push_id:
+
+        if (ngx_http_v3_parse_varlen_int(c, &st->vlint, ch) != NGX_DONE) {
+            break;
+        }
+
+        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 parse MAX_PUSH_ID:%uL", st->vlint.value);
+
+        st->state = sw_type;
+        break;
+
+    case sw_skip:
+
+        if (--st->length == 0) {
+            st->state = sw_type;
+        }
+
+        break;
+    }
+
+    return NGX_AGAIN;
+}
+
+
+ngx_int_t
+ngx_http_v3_parse_settings(ngx_connection_t *c,
+    ngx_http_v3_parse_settings_t *st, u_char ch)
+{
+    enum {
+        sw_start = 0,
+        sw_id,
+        sw_value
+    };
+
+    switch (st->state) {
+
+    case sw_start:
+
+        ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 parse settings");
+
+        st->state = sw_id;
+
+        /* fall through */
+
+    case sw_id:
+
+        if (ngx_http_v3_parse_varlen_int(c, &st->vlint, ch) != NGX_DONE) {
+            break;
+        }
+
+        st->id = st->vlint.value;
+        st->state = sw_value;
+        break;
+
+    case sw_value:
+
+        if (ngx_http_v3_parse_varlen_int(c, &st->vlint, ch) != NGX_DONE) {
+            break;
+        }
+
+        if (ngx_http_v3_set_param(c, st->id, st->vlint.value) != NGX_OK) {
+            return NGX_ERROR;
+        }
+
+        goto done;
+    }
+
+    return NGX_AGAIN;
+
+done:
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 parse settings done");
+
+    st->state = sw_start;
+    return NGX_DONE;
+}
+
+
+ngx_int_t
+ngx_http_v3_parse_encoder(ngx_connection_t *c, void *data, u_char ch)
+{
+    ngx_http_v3_parse_encoder_t *st = data;
+
+    ngx_int_t  rc;
+    enum {
+        sw_start = 0,
+        sw_inr,
+        sw_iwnr,
+        sw_capacity,
+        sw_duplicate
+    };
+
+    if (st->state == sw_start) {
+
+        ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 parse encoder instruction");
+
+        if (ch & 0x80) {
+            /* Insert With Name Reference */
+
+            st->state = sw_inr;
+
+        } else if (ch & 0x40) {
+            /*  Insert Without Name Reference */
+
+            st->state = sw_iwnr;
+
+        } else if (ch & 0x20) {
+            /*  Set Dynamic Table Capacity */
+
+            st->state = sw_capacity;
+
+        } else {
+            /* Duplicate */
+
+            st->state = sw_duplicate;
+        }
+    }
+
+    switch (st->state) {
+
+    case sw_inr:
+
+        rc = ngx_http_v3_parse_header_inr(c, &st->header, ch);
+
+        if (rc == NGX_ERROR) {
+            return NGX_ERROR;
+        }
+
+        if (rc != NGX_DONE) {
+            break;
+        }
+
+        goto done;
+
+    case sw_iwnr:
+
+        rc = ngx_http_v3_parse_header_iwnr(c, &st->header, ch);
+
+        if (rc == NGX_ERROR) {
+            return NGX_ERROR;
+        }
+
+        if (rc != NGX_DONE) {
+            break;
+        }
+
+        goto done;
+
+    case sw_capacity:
+
+        if (ngx_http_v3_parse_prefix_int(c, &st->pint, 5, ch) != NGX_DONE) {
+            break;
+        }
+
+        if (ngx_http_v3_set_capacity(c, st->pint.value) != NGX_OK) {
+            return NGX_ERROR;
+        }
+
+        goto done;
+
+    case sw_duplicate:
+
+        if (ngx_http_v3_parse_prefix_int(c, &st->pint, 5, ch) != NGX_DONE) {
+            break;
+        }
+
+        if (ngx_http_v3_duplicate(c, st->pint.value) != NGX_OK) {
+            return NGX_ERROR;
+        }
+
+        goto done;
+    }
+
+    return NGX_AGAIN;
+
+done:
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 parse encoder instruction done");
+
+    st->state = sw_start;
+    return NGX_DONE;
+}
+
+
+ngx_int_t
+ngx_http_v3_parse_header_inr(ngx_connection_t *c,
+    ngx_http_v3_parse_header_t *st, u_char ch)
+{
+    ngx_int_t  rc;
+    enum {
+        sw_start = 0,
+        sw_name_index,
+        sw_value_len,
+        sw_read_value_len,
+        sw_value
+    };
+
+    switch (st->state) {
+
+    case sw_start:
+
+        ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 parse header inr");
+
+        st->dynamic = (ch & 0x40) ? 0 : 1;
+        st->state = sw_name_index;
+
+        /* fall through */
+
+    case sw_name_index:
+
+        if (ngx_http_v3_parse_prefix_int(c, &st->pint, 6, ch) != NGX_DONE) {
+            break;
+        }
+
+        st->index = st->pint.value;
+        st->state = sw_value_len;
+        break;
+
+    case sw_value_len:
+
+        st->literal.huffman = (ch & 0x80) ? 1 : 0;
+        st->state = sw_read_value_len;
+
+        /* fall through */
+
+    case sw_read_value_len:
+
+        if (ngx_http_v3_parse_prefix_int(c, &st->pint, 7, ch) != NGX_DONE) {
+            break;
+        }
+
+        st->literal.length = st->pint.value;
+        if (st->literal.length == 0) {
+            goto done;
+        }
+
+        st->state = sw_value;
+        break;
+
+    case sw_value:
+
+        rc = ngx_http_v3_parse_literal(c, &st->literal, ch);
+
+        if (rc == NGX_ERROR) {
+            return NGX_ERROR;
+        }
+
+        if (rc == NGX_DONE) {
+            st->value = st->literal.value;
+            goto done;
+        }
+
+        break;
+    }
+
+    return NGX_AGAIN;
+
+done:
+
+    ngx_log_debug3(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 parse header inr done %s[%ui] \"%V\"",
+                   st->dynamic ? "dynamic" : "static",
+                   st->index, &st->value);
+
+    if (ngx_http_v3_ref_insert(c, st->dynamic, st->index, &st->value) != NGX_OK)
+    {
+        return NGX_ERROR;
+    }
+
+    st->state = sw_start;
+    return NGX_DONE;
+}
+
+
+ngx_int_t
+ngx_http_v3_parse_header_iwnr(ngx_connection_t *c,
+    ngx_http_v3_parse_header_t *st, u_char ch)
+{
+    ngx_int_t  rc;
+    enum {
+        sw_start = 0,
+        sw_name_len,
+        sw_name,
+        sw_value_len,
+        sw_read_value_len,
+        sw_value
+    };
+
+    switch (st->state) {
+
+    case sw_start:
+
+        ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 parse header iwnr");
+
+        st->literal.huffman = (ch & 0x20) ? 1 : 0;
+        st->state = sw_name_len;
+
+        /* fall through */
+
+    case sw_name_len:
+
+        if (ngx_http_v3_parse_prefix_int(c, &st->pint, 5, ch) != NGX_DONE) {
+            break;
+        }
+
+        st->literal.length = st->pint.value;
+        if (st->literal.length == 0) {
+            return NGX_ERROR;
+        }
+
+        st->state = sw_name;
+        break;
+
+    case sw_name:
+
+        rc = ngx_http_v3_parse_literal(c, &st->literal, ch);
+
+        if (rc == NGX_ERROR) {
+            return NGX_ERROR;
+        }
+
+        if (rc == NGX_DONE) {
+            st->name = st->literal.value;
+            st->state = sw_value_len;
+        }
+
+        break;
+
+    case sw_value_len:
+
+        st->literal.huffman = (ch & 0x80) ? 1 : 0;
+        st->state = sw_read_value_len;
+
+        /* fall through */
+
+    case sw_read_value_len:
+
+        if (ngx_http_v3_parse_prefix_int(c, &st->pint, 7, ch) != NGX_DONE) {
+            break;
+        }
+
+        st->literal.length = st->pint.value;
+        if (st->literal.length == 0) {
+            goto done;
+        }
+
+        st->state = sw_value;
+        break;
+
+    case sw_value:
+
+        rc = ngx_http_v3_parse_literal(c, &st->literal, ch);
+
+        if (rc == NGX_ERROR) {
+            return NGX_ERROR;
+        }
+
+        if (rc == NGX_DONE) {
+            st->value = st->literal.value;
+            goto done;
+        }
+
+        break;
+    }
+
+    return NGX_AGAIN;
+
+done:
+
+    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 parse header iwnr done \"%V\":\"%V\"",
+                   &st->name, &st->value);
+
+    if (ngx_http_v3_insert(c, &st->name, &st->value) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    st->state = sw_start;
+    return NGX_DONE;
+}
+
+
+ngx_int_t
+ngx_http_v3_parse_decoder(ngx_connection_t *c, void *data, u_char ch)
+{
+    ngx_http_v3_parse_decoder_t *st = data;
+
+    enum {
+        sw_start = 0,
+        sw_ack_header,
+        sw_cancel_stream,
+        sw_inc_insert_count
+    };
+
+    if (st->state == sw_start) {
+
+        ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 parse decoder instruction");
+
+        if (ch & 0x80) {
+            /* Header Acknowledgement */
+
+            st->state = sw_ack_header;
+
+        } else if (ch & 0x40) {
+            /*  Stream Cancellation */
+
+            st->state = sw_cancel_stream;
+
+        }  else {
+            /*  Insert Count Increment */
+
+            st->state = sw_inc_insert_count;
+        }
+    }
+
+    switch (st->state) {
+
+    case sw_ack_header:
+
+        if (ngx_http_v3_parse_prefix_int(c, &st->pint, 6, ch) != NGX_DONE) {
+            break;
+        }
+
+        if (ngx_http_v3_ack_header(c, st->pint.value) != NGX_OK) {
+            return NGX_ERROR;
+        }
+
+        goto done;
+
+    case sw_cancel_stream:
+
+        if (ngx_http_v3_parse_prefix_int(c, &st->pint, 6, ch) != NGX_DONE) {
+            break;
+        }
+
+        if (ngx_http_v3_cancel_stream(c, st->pint.value) != NGX_OK) {
+            return NGX_ERROR;
+        }
+
+        goto done;
+
+    case sw_inc_insert_count:
+
+        if (ngx_http_v3_parse_prefix_int(c, &st->pint, 6, ch) != NGX_DONE) {
+            break;
+        }
+
+        if (ngx_http_v3_inc_insert_count(c, st->pint.value) != NGX_OK) {
+            return NGX_ERROR;
+        }
+
+        goto done;
+    }
+
+    return NGX_AGAIN;
+
+done:
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 parse decoder instruction done");
+
+    st->state = sw_start;
+    return NGX_DONE;
+}
+
+
+ngx_int_t
+ngx_http_v3_parse_data(ngx_connection_t *c, ngx_http_v3_parse_data_t *st,
+    u_char ch)
+{
+    enum {
+        sw_start = 0,
+        sw_type,
+        sw_length
+    };
+
+    switch (st->state) {
+
+    case sw_start:
+
+        ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 parse data");
+
+        st->state = sw_type;
+
+        /* fall through */
+
+    case sw_type:
+
+        if (ngx_http_v3_parse_varlen_int(c, &st->vlint, ch) != NGX_DONE) {
+            break;
+        }
+
+        if (st->vlint.value != NGX_HTTP_V3_FRAME_DATA) {
+            return NGX_ERROR;
+        }
+
+        st->state = sw_length;
+        break;
+
+    case sw_length:
+
+        if (ngx_http_v3_parse_varlen_int(c, &st->vlint, ch) != NGX_DONE) {
+            break;
+        }
+
+        st->length = st->vlint.value;
+
+        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 parse data frame len:%ui", st->length);
+
+        goto done;
+    }
+
+    return NGX_AGAIN;
+
+done:
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 parse data done");
+
+    st->state = sw_start;
+    return NGX_DONE;
+}
new file mode 100644
--- /dev/null
+++ b/src/http/v3/ngx_http_v3_parse.h
@@ -0,0 +1,155 @@
+
+/*
+ * Copyright (C) Roman Arutyunyan
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_HTTP_V3_PARSE_H_INCLUDED_
+#define _NGX_HTTP_V3_PARSE_H_INCLUDED_
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_http.h>
+
+
+typedef struct {
+    ngx_uint_t                      state;
+    uint64_t                        value;
+} ngx_http_v3_parse_varlen_int_t;
+
+
+typedef struct {
+    ngx_uint_t                      state;
+    ngx_uint_t                      mask;
+    uint64_t                        value;
+} ngx_http_v3_parse_prefix_int_t;
+
+
+typedef struct {
+    ngx_uint_t                      state;
+    uint64_t                        id;
+    ngx_http_v3_parse_varlen_int_t  vlint;
+} ngx_http_v3_parse_settings_t;
+
+
+typedef struct {
+    ngx_uint_t                      state;
+    ngx_uint_t                      insert_count;
+    ngx_uint_t                      delta_base;
+    ngx_uint_t                      sign;
+    ngx_uint_t                      base;
+    ngx_http_v3_parse_prefix_int_t  pint;
+} ngx_http_v3_parse_header_block_prefix_t;
+
+
+typedef struct {
+    ngx_uint_t                      state;
+    ngx_uint_t                      length;
+    ngx_uint_t                      huffman;
+    ngx_str_t                       value;
+    u_char                         *last;
+    u_char                          huffstate;
+} ngx_http_v3_parse_literal_t;
+
+
+typedef struct {
+    ngx_uint_t                      state;
+    ngx_uint_t                      index;
+    ngx_uint_t                      base;
+    ngx_uint_t                      dynamic;
+
+    ngx_str_t                       name;
+    ngx_str_t                       value;
+
+    ngx_http_v3_parse_prefix_int_t  pint;
+    ngx_http_v3_parse_literal_t     literal;
+} ngx_http_v3_parse_header_t;
+
+
+typedef struct {
+    ngx_uint_t                      state;
+    ngx_http_v3_parse_header_t      header;
+} ngx_http_v3_parse_header_rep_t;
+
+
+typedef struct {
+    ngx_uint_t                      state;
+    ngx_uint_t                      length;
+    ngx_http_v3_parse_varlen_int_t  vlint;
+    ngx_http_v3_parse_header_block_prefix_t  prefix;
+    ngx_http_v3_parse_header_rep_t  header_rep;
+} ngx_http_v3_parse_headers_t;
+
+
+typedef struct {
+    ngx_uint_t                      state;
+    ngx_http_v3_parse_header_t      header;
+    ngx_http_v3_parse_prefix_int_t  pint;
+} ngx_http_v3_parse_encoder_t;
+
+
+typedef struct {
+    ngx_uint_t                      state;
+    ngx_http_v3_parse_prefix_int_t  pint;
+} ngx_http_v3_parse_decoder_t;
+
+
+typedef struct {
+    ngx_uint_t                      state;
+    ngx_uint_t                      type;
+    ngx_uint_t                      length;
+    ngx_http_v3_parse_varlen_int_t  vlint;
+    ngx_http_v3_parse_settings_t    settings;
+} ngx_http_v3_parse_control_t;
+
+
+typedef struct {
+    ngx_uint_t                      state;
+    ngx_uint_t                      length;
+    ngx_http_v3_parse_varlen_int_t  vlint;
+} ngx_http_v3_parse_data_t;
+
+
+ngx_int_t ngx_http_v3_parse_varlen_int(ngx_connection_t *c,
+    ngx_http_v3_parse_varlen_int_t *st, u_char ch);
+ngx_int_t ngx_http_v3_parse_prefix_int(ngx_connection_t *c,
+    ngx_http_v3_parse_prefix_int_t *st, ngx_uint_t prefix, u_char ch);
+
+ngx_int_t ngx_http_v3_parse_headers(ngx_connection_t *c,
+    ngx_http_v3_parse_headers_t *st, u_char ch);
+ngx_int_t ngx_http_v3_parse_header_block_prefix(ngx_connection_t *c,
+    ngx_http_v3_parse_header_block_prefix_t *st, u_char ch);
+ngx_int_t ngx_http_v3_parse_header_rep(ngx_connection_t *c,
+    ngx_http_v3_parse_header_rep_t *st, ngx_uint_t base, u_char ch);
+ngx_int_t ngx_http_v3_parse_literal(ngx_connection_t *c,
+    ngx_http_v3_parse_literal_t *st, u_char ch);
+ngx_int_t ngx_http_v3_parse_header_ri(ngx_connection_t *c,
+    ngx_http_v3_parse_header_t *st, u_char ch);
+ngx_int_t ngx_http_v3_parse_header_lri(ngx_connection_t *c,
+    ngx_http_v3_parse_header_t *st, u_char ch);
+ngx_int_t ngx_http_v3_parse_header_l(ngx_connection_t *c,
+    ngx_http_v3_parse_header_t *st, u_char ch);
+ngx_int_t ngx_http_v3_parse_header_pbi(ngx_connection_t *c,
+    ngx_http_v3_parse_header_t *st, u_char ch);
+ngx_int_t ngx_http_v3_parse_header_lpbi(ngx_connection_t *c,
+    ngx_http_v3_parse_header_t *st, u_char ch);
+
+ngx_int_t ngx_http_v3_parse_control(ngx_connection_t *c, void *data, u_char ch);
+ngx_int_t ngx_http_v3_parse_settings(ngx_connection_t *c,
+    ngx_http_v3_parse_settings_t *st, u_char ch);
+
+ngx_int_t ngx_http_v3_parse_encoder(ngx_connection_t *c, void *data, u_char ch);
+ngx_int_t ngx_http_v3_parse_header_inr(ngx_connection_t *c,
+    ngx_http_v3_parse_header_t *st, u_char ch);
+ngx_int_t ngx_http_v3_parse_header_iwnr(ngx_connection_t *c,
+    ngx_http_v3_parse_header_t *st, u_char ch);
+
+ngx_int_t ngx_http_v3_parse_decoder(ngx_connection_t *c, void *data, u_char ch);
+
+ngx_int_t ngx_http_v3_parse_data(ngx_connection_t *c,
+    ngx_http_v3_parse_data_t *st, u_char ch);
+
+
+#endif /* _NGX_HTTP_V3_PARSE_H_INCLUDED_ */
new file mode 100644
--- /dev/null
+++ b/src/http/v3/ngx_http_v3_request.c
@@ -0,0 +1,669 @@
+
+/*
+ * Copyright (C) Roman Arutyunyan
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_http.h>
+
+
+static ngx_int_t ngx_http_v3_process_pseudo_header(ngx_http_request_t *r,
+    ngx_str_t *name, ngx_str_t *value);
+
+
+struct {
+    ngx_str_t   name;
+    ngx_uint_t  method;
+} ngx_http_v3_methods[] = {
+
+    { ngx_string("GET"),       NGX_HTTP_GET },
+    { ngx_string("POST"),      NGX_HTTP_POST },
+    { ngx_string("HEAD"),      NGX_HTTP_HEAD },
+    { ngx_string("OPTIONS"),   NGX_HTTP_OPTIONS },
+    { ngx_string("PROPFIND"),  NGX_HTTP_PROPFIND },
+    { ngx_string("PUT"),       NGX_HTTP_PUT },
+    { ngx_string("MKCOL"),     NGX_HTTP_MKCOL },
+    { ngx_string("DELETE"),    NGX_HTTP_DELETE },
+    { ngx_string("COPY"),      NGX_HTTP_COPY },
+    { ngx_string("MOVE"),      NGX_HTTP_MOVE },
+    { ngx_string("PROPPATCH"), NGX_HTTP_PROPPATCH },
+    { ngx_string("LOCK"),      NGX_HTTP_LOCK },
+    { ngx_string("UNLOCK"),    NGX_HTTP_UNLOCK },
+    { ngx_string("PATCH"),     NGX_HTTP_PATCH },
+    { ngx_string("TRACE"),     NGX_HTTP_TRACE }
+};
+
+
+ngx_int_t
+ngx_http_v3_parse_header(ngx_http_request_t *r, ngx_buf_t *b)
+{
+    size_t                        n;
+    u_char                       *p;
+    ngx_int_t                     rc;
+    ngx_str_t                    *name, *value;
+    ngx_connection_t             *c;
+    ngx_http_v3_parse_headers_t  *st;
+    enum {
+        sw_start = 0,
+        sw_prev,
+        sw_headers,
+        sw_last,
+        sw_done
+    };
+
+    c = r->connection;
+    st = r->h3_parse;
+
+    if (st == NULL) {
+        ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 parse header");
+
+        st = ngx_pcalloc(c->pool, sizeof(ngx_http_v3_parse_headers_t));
+        if (st == NULL) {
+            goto failed;
+        }
+
+        r->h3_parse = st;
+    }
+
+    switch (r->state) {
+
+    case sw_prev:
+        r->state = sw_headers;
+        return NGX_OK;
+
+    case sw_done:
+        goto done;
+
+    case sw_last:
+        r->state = sw_done;
+        return NGX_OK;
+
+    default:
+        break;
+    }
+
+    while (b->pos < b->last) {
+        rc = ngx_http_v3_parse_headers(c, st, *b->pos++);
+
+        if (rc == NGX_ERROR) {
+            goto failed;
+        }
+
+        if (rc == NGX_AGAIN) {
+            continue;
+        }
+
+        name = &st->header_rep.header.name;
+        value = &st->header_rep.header.value;
+
+        if (r->state == sw_start) {
+
+            if (ngx_http_v3_process_pseudo_header(r, name, value) == NGX_OK) {
+                if (rc == NGX_OK) {
+                    continue;
+                }
+
+                r->state = sw_done;
+
+            } else if (rc == NGX_OK) {
+                r->state = sw_prev;
+
+            } else {
+                r->state = sw_last;
+            }
+
+            n = (r->method_end - r->method_start) + 1
+                + (r->uri_end - r->uri_start) + 1
+                + sizeof("HTTP/3") - 1;
+
+            p = ngx_pnalloc(c->pool, n);
+            if (p == NULL) {
+                goto failed;
+            }
+
+            r->request_start = p;
+
+            p = ngx_cpymem(p, r->method_start, r->method_end - r->method_start);
+            *p++ = ' ';
+            p = ngx_cpymem(p, r->uri_start, r->uri_end - r->uri_start);
+            *p++ = ' ';
+            p = ngx_cpymem(p, "HTTP/3", sizeof("HTTP/3") - 1);
+
+            r->request_end = p;
+
+        } else if (rc == NGX_DONE) {
+            r->state = sw_done;
+        }
+
+        r->header_name_start = name->data;
+        r->header_name_end = name->data + name->len;
+        r->header_start = value->data;
+        r->header_end = value->data + value->len;
+        r->header_hash = ngx_hash_key(name->data, name->len);
+
+        /* XXX r->lowcase_index = i; */
+
+        return NGX_OK;
+    }
+
+    return NGX_AGAIN;
+
+failed:
+
+    return r->state == sw_start ? NGX_HTTP_PARSE_INVALID_REQUEST
+                                : NGX_HTTP_PARSE_INVALID_HEADER;
+
+done:
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 parse header done");
+
+    return NGX_HTTP_PARSE_HEADER_DONE;
+}
+
+
+static ngx_int_t
+ngx_http_v3_process_pseudo_header(ngx_http_request_t *r, ngx_str_t *name,
+    ngx_str_t *value)
+{
+    ngx_uint_t         i;
+    ngx_connection_t  *c;
+
+    if (name->len == 0 || name->data[0] != ':') {
+        return NGX_DONE;
+    }
+
+    c = r->connection;
+
+    if (name->len == 7 && ngx_strncmp(name->data, ":method", 7) == 0) {
+        r->method_start = value->data;
+        r->method_end = value->data + value->len;
+
+        for (i = 0; i < sizeof(ngx_http_v3_methods)
+                        / sizeof(ngx_http_v3_methods[0]); i++)
+        {
+            if (value->len == ngx_http_v3_methods[i].name.len
+                && ngx_strncmp(value->data, ngx_http_v3_methods[i].name.data,
+                               value->len) == 0)
+            {
+                r->method = ngx_http_v3_methods[i].method;
+                break;
+            }
+        }
+
+        ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 method \"%V\" %ui", value, r->method);
+        return NGX_OK;
+    }
+
+    if (name->len == 5 && ngx_strncmp(name->data, ":path", 5) == 0) {
+        r->uri_start = value->data;
+        r->uri_end = value->data + value->len;
+
+        if (ngx_http_parse_uri(r) != NGX_OK) {
+            ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                          "client sent invalid :path header: \"%V\"", value);
+            return NGX_ERROR;
+        }
+
+        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 path \"%V\"", value);
+
+        return NGX_OK;
+    }
+
+    if (name->len == 7 && ngx_strncmp(name->data, ":scheme", 7) == 0) {
+        r->schema_start = value->data;
+        r->schema_end = value->data + value->len;
+
+        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 schema \"%V\"", value);
+
+        return NGX_OK;
+    }
+
+    if (name->len == 10 && ngx_strncmp(name->data, ":authority", 10) == 0) {
+        r->host_start = value->data;
+        r->host_end = value->data + value->len;
+
+        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 authority \"%V\"", value);
+
+        return NGX_OK;
+    }
+
+    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 unknown pseudo header \"%V\" \"%V\"", name, value);
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_http_v3_parse_request_body(ngx_http_request_t *r, ngx_buf_t *b,
+    ngx_http_chunked_t *ctx)
+{
+    ngx_int_t                  rc;
+    ngx_connection_t          *c;
+    ngx_http_v3_parse_data_t  *st;
+
+    c = r->connection;
+    st = ctx->h3_parse;
+
+    if (st == NULL) {
+        ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 parse request body");
+
+        st = ngx_pcalloc(c->pool, sizeof(ngx_http_v3_parse_data_t));
+        if (st == NULL) {
+            goto failed;
+        }
+
+        r->h3_parse = st;
+    }
+
+    if (ctx->size) {
+        ctx->length = ctx->size + 1;
+        return (b->pos == b->last) ? NGX_AGAIN : NGX_OK;
+    }
+
+    while (b->pos < b->last) {
+        rc = ngx_http_v3_parse_data(c, st, *b->pos++);
+
+        if (rc == NGX_ERROR) {
+            goto failed;
+        }
+
+        if (rc == NGX_AGAIN) {
+            continue;
+        }
+
+        /* rc == NGX_DONE */
+
+        ctx->size = st->length;
+        return NGX_OK;
+    }
+
+    if (!b->last_buf) {
+        ctx->length = 1;
+        return NGX_AGAIN;
+    }
+
+    if (st->state) {
+        goto failed;
+    }
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 parse header done");
+
+    return NGX_DONE;
+
+failed:
+
+    return NGX_ERROR;
+}
+
+
+ngx_chain_t *
+ngx_http_v3_create_header(ngx_http_request_t *r)
+{
+    u_char                    *p;
+    size_t                     len, n;
+    ngx_buf_t                 *b;
+    ngx_uint_t                 i, j;
+    ngx_chain_t               *hl, *cl, *bl;
+    ngx_list_part_t           *part;
+    ngx_table_elt_t           *header;
+    ngx_connection_t          *c;
+    ngx_http_core_loc_conf_t  *clcf;
+
+    c = r->connection;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 create header");
+
+    len = 2;
+
+    if (r->headers_out.status == NGX_HTTP_OK) {
+        len += ngx_http_v3_encode_prefix_int(NULL, 25, 6);
+
+    } else {
+        len += 3 + ngx_http_v3_encode_prefix_int(NULL, 25, 4)
+                 + ngx_http_v3_encode_prefix_int(NULL, 3, 7);
+    }
+
+    clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
+
+    if (r->headers_out.server == NULL) {
+        if (clcf->server_tokens == NGX_HTTP_SERVER_TOKENS_ON) {
+            n = sizeof(NGINX_VER) - 1;
+
+        } else if (clcf->server_tokens == NGX_HTTP_SERVER_TOKENS_BUILD) {
+            n = sizeof(NGINX_VER_BUILD) - 1;
+
+        } else {
+            n = sizeof("nginx") - 1;
+        }
+
+        len += ngx_http_v3_encode_prefix_int(NULL, 92, 4)
+               + ngx_http_v3_encode_prefix_int(NULL, n, 7) + n;
+    }
+
+    if (r->headers_out.date == NULL) {
+        len += ngx_http_v3_encode_prefix_int(NULL, 6, 4)
+               + ngx_http_v3_encode_prefix_int(NULL, ngx_cached_http_time.len,
+                                               7)
+               + ngx_cached_http_time.len;
+    }
+
+    if (r->headers_out.content_type.len) {
+        n = r->headers_out.content_type.len;
+
+        if (r->headers_out.content_type_len == r->headers_out.content_type.len
+            && r->headers_out.charset.len)
+        {
+            n += sizeof("; charset=") - 1 + r->headers_out.charset.len;
+        }
+
+        len += ngx_http_v3_encode_prefix_int(NULL, 53, 4)
+               + ngx_http_v3_encode_prefix_int(NULL, n, 7) + n;
+    }
+
+    if (r->headers_out.content_length_n > 0) {
+        len += ngx_http_v3_encode_prefix_int(NULL, 4, 4) + 1 + NGX_OFF_T_LEN;
+
+    } else if (r->headers_out.content_length_n == 0) {
+        len += ngx_http_v3_encode_prefix_int(NULL, 4, 6);
+    }
+
+    if (r->headers_out.last_modified == NULL
+        && r->headers_out.last_modified_time != -1)
+    {
+        len += ngx_http_v3_encode_prefix_int(NULL, 10, 4) + 1
+               + sizeof("Last-Modified: Mon, 28 Sep 1970 06:00:00 GMT");
+    }
+
+    /* XXX location */
+
+#if (NGX_HTTP_GZIP)
+    if (r->gzip_vary) {
+        if (clcf->gzip_vary) {
+            /* Vary: Accept-Encoding */
+            len += ngx_http_v3_encode_prefix_int(NULL, 59, 6);
+
+        } else {
+            r->gzip_vary = 0;
+        }
+    }
+#endif
+
+    part = &r->headers_out.headers.part;
+    header = part->elts;
+
+    for (i = 0; /* void */; i++) {
+
+        if (i >= part->nelts) {
+            if (part->next == NULL) {
+                break;
+            }
+
+            part = part->next;
+            header = part->elts;
+            i = 0;
+        }
+
+        if (header[i].hash == 0) {
+            continue;
+        }
+
+        len += ngx_http_v3_encode_prefix_int(NULL, header[i].key.len, 3)
+               + header[i].key.len
+               + ngx_http_v3_encode_prefix_int(NULL, header[i].value.len, 7 )
+               + header[i].value.len;
+    }
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 header len:%uz", len);
+
+    b = ngx_create_temp_buf(r->pool, len);
+    if (b == NULL) {
+        return NULL;
+    }
+
+    *b->last++ = 0;
+    *b->last++ = 0;
+
+    if (r->headers_out.status == NGX_HTTP_OK) {
+        /* :status: 200 */
+        *b->last = 0xc0;
+        b->last = (u_char *) ngx_http_v3_encode_prefix_int(b->last, 25, 6);
+
+    } else {
+        /* :status: 200 */
+        *b->last = 0x70;
+        b->last = (u_char *) ngx_http_v3_encode_prefix_int(b->last, 25, 4);
+        *b->last = 0;
+        b->last = (u_char *) ngx_http_v3_encode_prefix_int(b->last, 3, 7);
+        b->last = ngx_sprintf(b->last, "%03ui", r->headers_out.status);
+    }
+
+    if (r->headers_out.server == NULL) {
+        if (clcf->server_tokens == NGX_HTTP_SERVER_TOKENS_ON) {
+            p = (u_char *) NGINX_VER;
+            n = sizeof(NGINX_VER) - 1;
+
+        } else if (clcf->server_tokens == NGX_HTTP_SERVER_TOKENS_BUILD) {
+            p = (u_char *) NGINX_VER_BUILD;
+            n = sizeof(NGINX_VER_BUILD) - 1;
+
+        } else {
+            p = (u_char *) "nginx";
+            n = sizeof("nginx") - 1;
+        }
+
+        /* server */
+        *b->last = 0x70;
+        b->last = (u_char *) ngx_http_v3_encode_prefix_int(b->last, 92, 4);
+        *b->last = 0;
+        b->last = (u_char *) ngx_http_v3_encode_prefix_int(b->last, n, 7);
+        b->last = ngx_cpymem(b->last, p, n);
+    }
+
+    if (r->headers_out.date == NULL) {
+        /* date */
+        *b->last = 0x70;
+        b->last = (u_char *) ngx_http_v3_encode_prefix_int(b->last, 6, 4);
+        *b->last = 0;
+        b->last = (u_char *) ngx_http_v3_encode_prefix_int(b->last,
+                                                 ngx_cached_http_time.len, 7);
+        b->last = ngx_cpymem(b->last, ngx_cached_http_time.data,
+                             ngx_cached_http_time.len);
+    }
+
+    if (r->headers_out.content_type.len) {
+        n = r->headers_out.content_type.len;
+
+        if (r->headers_out.content_type_len == r->headers_out.content_type.len
+            && r->headers_out.charset.len)
+        {
+            n += sizeof("; charset=") - 1 + r->headers_out.charset.len;
+        }
+
+        /* content-type: text/plain */
+        *b->last = 0x70;
+        b->last = (u_char *) ngx_http_v3_encode_prefix_int(b->last, 53, 4);
+        *b->last = 0;
+        b->last = (u_char *) ngx_http_v3_encode_prefix_int(b->last, n, 7);
+
+        p = b->last;
+        b->last = ngx_copy(b->last, r->headers_out.content_type.data,
+                           r->headers_out.content_type.len);
+
+        if (r->headers_out.content_type_len == r->headers_out.content_type.len
+            && r->headers_out.charset.len)
+        {
+            b->last = ngx_cpymem(b->last, "; charset=",
+                                 sizeof("; charset=") - 1);
+            b->last = ngx_copy(b->last, r->headers_out.charset.data,
+                               r->headers_out.charset.len);
+
+            /* update r->headers_out.content_type for possible logging */
+
+            r->headers_out.content_type.len = b->last - p;
+            r->headers_out.content_type.data = p;
+        }
+    }
+
+    if (r->headers_out.content_length_n > 0) {
+        /* content-length: 0 */
+        *b->last = 0x70;
+        b->last = (u_char *) ngx_http_v3_encode_prefix_int(b->last, 4, 4);
+        p = b->last++;
+        b->last = ngx_sprintf(b->last, "%O", r->headers_out.content_length_n);
+        *p = b->last - p - 1;
+
+    } else if (r->headers_out.content_length_n == 0) {
+        /* content-length: 0 */
+        *b->last = 0xc0;
+        b->last = (u_char *) ngx_http_v3_encode_prefix_int(b->last, 4, 6);
+    }
+
+    if (r->headers_out.last_modified == NULL
+        && r->headers_out.last_modified_time != -1)
+    {
+        /* last-modified */
+        *b->last = 0x70;
+        b->last = (u_char *) ngx_http_v3_encode_prefix_int(b->last, 10, 4);
+        p = b->last++;
+        b->last = ngx_http_time(b->last, r->headers_out.last_modified_time);
+        *p = b->last - p - 1;
+    }
+
+#if (NGX_HTTP_GZIP)
+    if (r->gzip_vary) {
+        /* vary: accept-encoding */
+        *b->last = 0xc0;
+        b->last = (u_char *) ngx_http_v3_encode_prefix_int(b->last, 59, 6);
+    }
+#endif
+
+    part = &r->headers_out.headers.part;
+    header = part->elts;
+
+    for (i = 0; /* void */; i++) {
+
+        if (i >= part->nelts) {
+            if (part->next == NULL) {
+                break;
+            }
+
+            part = part->next;
+            header = part->elts;
+            i = 0;
+        }
+
+        if (header[i].hash == 0) {
+            continue;
+        }
+
+        *b->last = 0x30;
+        b->last = (u_char *) ngx_http_v3_encode_prefix_int(b->last,
+                                                           header[i].key.len,
+                                                           3);
+        for (j = 0; j < header[i].key.len; j++) {
+            *b->last++ = ngx_tolower(header[i].key.data[j]);
+        }
+
+        *b->last = 0;
+        b->last = (u_char *) ngx_http_v3_encode_prefix_int(b->last,
+                                                           header[i].value.len,
+                                                           7);
+        b->last = ngx_copy(b->last, header[i].value.data, header[i].value.len);
+    }
+
+    if (r->header_only) {
+        b->last_buf = 1;
+    }
+
+    cl = ngx_alloc_chain_link(c->pool);
+    if (cl == NULL) {
+        return NULL;
+    }
+
+    cl->buf = b;
+    cl->next = NULL;
+
+    n = b->last - b->pos;
+
+    len = 1 + ngx_http_v3_encode_varlen_int(NULL, n);
+
+    b = ngx_create_temp_buf(c->pool, len);
+    if (b == NULL) {
+        return NULL;
+    }
+
+    *b->last++ = NGX_HTTP_V3_FRAME_HEADERS;
+    b->last = (u_char *) ngx_http_v3_encode_varlen_int(b->last, n);
+
+    hl = ngx_alloc_chain_link(c->pool);
+    if (hl == NULL) {
+        return NULL;
+    }
+
+    hl->buf = b;
+    hl->next = cl;
+
+    if (r->headers_out.content_length_n >= 0) {
+        len = 1 + ngx_http_v3_encode_varlen_int(NULL,
+                                              r->headers_out.content_length_n);
+
+        b = ngx_create_temp_buf(c->pool, len);
+        if (b == NULL) {
+            NULL;
+        }
+
+        *b->last++ = NGX_HTTP_V3_FRAME_DATA;
+        b->last = (u_char *) ngx_http_v3_encode_varlen_int(b->last,
+                                              r->headers_out.content_length_n);
+
+        bl = ngx_alloc_chain_link(c->pool);
+        if (bl == NULL) {
+            return NULL;
+        }
+
+        bl->buf = b;
+        bl->next = NULL;
+        cl->next = bl;
+    }
+
+    return hl;
+}
+
+
+ngx_chain_t *
+ngx_http_v3_create_trailers(ngx_http_request_t *r)
+{
+    ngx_buf_t    *b;
+    ngx_chain_t  *cl;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
+                   "http3 create trailers");
+
+    /* XXX */
+
+    b = ngx_calloc_buf(r->pool);
+    if (b == NULL) {
+        return NULL;
+    }
+
+    b->last_buf = 1;
+
+    cl = ngx_alloc_chain_link(r->pool);
+    if (cl == NULL) {
+        return NULL;
+    }
+
+    cl->buf = b;
+    cl->next = NULL;
+
+    return cl;
+}
new file mode 100644
--- /dev/null
+++ b/src/http/v3/ngx_http_v3_streams.c
@@ -0,0 +1,571 @@
+
+/*
+ * Copyright (C) Roman Arutyunyan
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_http.h>
+
+
+typedef ngx_int_t (*ngx_http_v3_handler_pt)(ngx_connection_t *c, void *data,
+    u_char ch);
+
+
+typedef struct {
+    ngx_http_v3_handler_pt          handler;
+    void                           *data;
+    ngx_int_t                       index;
+} ngx_http_v3_uni_stream_t;
+
+
+static void ngx_http_v3_close_uni_stream(ngx_connection_t *c);
+static void ngx_http_v3_read_uni_stream_type(ngx_event_t *rev);
+static void ngx_http_v3_uni_read_handler(ngx_event_t *rev);
+static void ngx_http_v3_dummy_write_handler(ngx_event_t *wev);
+static ngx_connection_t *ngx_http_v3_get_uni_stream(ngx_connection_t *c,
+    ngx_uint_t type);
+
+
+void
+ngx_http_v3_handle_client_uni_stream(ngx_connection_t *c)
+{
+    ngx_http_v3_uni_stream_t  *us;
+
+    c->log->connection = c->number;
+
+    ngx_http_v3_get_uni_stream(c, NGX_HTTP_V3_STREAM_CONTROL);
+    ngx_http_v3_get_uni_stream(c, NGX_HTTP_V3_STREAM_ENCODER);
+    ngx_http_v3_get_uni_stream(c, NGX_HTTP_V3_STREAM_DECODER);
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 new uni stream id:0x%uxL", c->qs->id);
+
+    us = ngx_pcalloc(c->pool, sizeof(ngx_http_v3_uni_stream_t));
+    if (us == NULL) {
+        ngx_http_v3_close_uni_stream(c);
+        return;
+    }
+
+    us->index = -1;
+
+    c->data = us;
+
+    c->read->handler = ngx_http_v3_read_uni_stream_type;
+    c->write->handler = ngx_http_v3_dummy_write_handler;
+
+    ngx_http_v3_read_uni_stream_type(c->read);
+}
+
+
+static void
+ngx_http_v3_close_uni_stream(ngx_connection_t *c)
+{
+    ngx_pool_t                *pool;
+    ngx_http_v3_connection_t  *h3c;
+    ngx_http_v3_uni_stream_t  *us;
+
+    us = c->data;
+    h3c = c->qs->parent->data;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 close stream");
+
+    if (us->index >= 0) {
+        h3c->known_streams[us->index] = NULL;
+    }
+
+    c->destroyed = 1;
+
+    pool = c->pool;
+
+    ngx_close_connection(c);
+
+    ngx_destroy_pool(pool);
+}
+
+
+static void
+ngx_http_v3_read_uni_stream_type(ngx_event_t *rev)
+{
+    u_char                     ch;
+    ssize_t                    n;
+    ngx_int_t                  index;
+    ngx_connection_t          *c;
+    ngx_http_v3_connection_t  *h3c;
+    ngx_http_v3_uni_stream_t  *us;
+
+    c = rev->data;
+    us = c->data;
+    h3c = c->qs->parent->data;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 read stream type");
+
+    while (rev->ready) {
+
+        n = c->recv(c, &ch, 1);
+
+        if (n == NGX_ERROR) {
+            goto failed;
+        }
+
+        if (n == NGX_AGAIN || n != 1) {
+            break;
+        }
+
+        switch (ch) {
+
+        case NGX_HTTP_V3_STREAM_ENCODER:
+
+            ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                           "http3 encoder stream");
+
+            index = NGX_HTTP_V3_STREAM_CLIENT_ENCODER;
+            us->handler = ngx_http_v3_parse_encoder;
+            n = sizeof(ngx_http_v3_parse_encoder_t);
+
+            break;
+
+        case NGX_HTTP_V3_STREAM_DECODER:
+
+            ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                           "http3 decoder stream");
+
+            index = NGX_HTTP_V3_STREAM_CLIENT_DECODER;
+            us->handler = ngx_http_v3_parse_decoder;
+            n = sizeof(ngx_http_v3_parse_decoder_t);
+
+            break;
+
+        case NGX_HTTP_V3_STREAM_CONTROL:
+
+            ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                           "http3 control stream");
+
+            index = NGX_HTTP_V3_STREAM_CLIENT_CONTROL;
+            us->handler = ngx_http_v3_parse_control;
+            n = sizeof(ngx_http_v3_parse_control_t);
+
+            break;
+
+        default:
+
+            ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                           "http3 stream 0x%02xi", (ngx_int_t) ch);
+            index = -1;
+            n = 0;
+        }
+
+        if (index >= 0) {
+            if (h3c->known_streams[index]) {
+                ngx_log_error(NGX_LOG_INFO, c->log, 0, "stream exists");
+                goto failed;
+            }
+
+            us->index = index;
+            h3c->known_streams[index] = c;
+        }
+
+        if (n) {
+            us->data = ngx_pcalloc(c->pool, n);
+            if (us->data == NULL) {
+                goto failed;
+            }
+        }
+
+        rev->handler = ngx_http_v3_uni_read_handler;
+        ngx_http_v3_uni_read_handler(rev);
+        return;
+    }
+
+    if (ngx_handle_read_event(rev, 0) != NGX_OK) {
+        goto failed;
+    }
+
+    return;
+
+failed:
+
+    ngx_http_v3_close_uni_stream(c);
+}
+
+
+static void
+ngx_http_v3_uni_read_handler(ngx_event_t *rev)
+{
+    u_char                     buf[128];
+    ssize_t                    n;
+    ngx_int_t                  rc, i;
+    ngx_connection_t          *c;
+    ngx_http_v3_uni_stream_t  *us;
+
+    c = rev->data;
+    us = c->data;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 read handler");
+
+    while (rev->ready) {
+
+        n = c->recv(c, buf, sizeof(buf));
+
+        if (n == NGX_ERROR || n == 0) {
+            goto failed;
+        }
+
+        if (n == NGX_AGAIN) {
+            break;
+        }
+
+        if (us->handler == NULL) {
+            continue;
+        }
+
+        for (i = 0; i < n; i++) {
+
+            rc = us->handler(c, us->data, buf[i]);
+
+            if (rc == NGX_ERROR) {
+                goto failed;
+            }
+
+            if (rc == NGX_DONE) {
+                goto done;
+            }
+
+            /* rc == NGX_AGAIN */
+        }
+    }
+
+    if (ngx_handle_read_event(rev, 0) != NGX_OK) {
+        goto failed;
+    }
+
+    return;
+
+done:
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 read done");
+
+failed:
+
+    ngx_http_v3_close_uni_stream(c);
+}
+
+
+static void
+ngx_http_v3_dummy_write_handler(ngx_event_t *wev)
+{
+    ngx_connection_t  *c;
+
+    c = wev->data;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 dummy write handler");
+
+    if (ngx_handle_write_event(wev, 0) != NGX_OK) {
+        ngx_http_v3_close_uni_stream(c);
+    }
+}
+
+
+/* XXX async & buffered stream writes */
+
+static ngx_connection_t *
+ngx_http_v3_get_uni_stream(ngx_connection_t *c, ngx_uint_t type)
+{
+    u_char                     buf[NGX_HTTP_V3_VARLEN_INT_LEN];
+    size_t                     n;
+    ngx_int_t                  index;
+    ngx_connection_t          *sc;
+    ngx_http_v3_connection_t  *h3c;
+    ngx_http_v3_uni_stream_t  *us;
+
+    switch (type) {
+    case NGX_HTTP_V3_STREAM_ENCODER:
+        index = NGX_HTTP_V3_STREAM_SERVER_ENCODER;
+        break;
+    case NGX_HTTP_V3_STREAM_DECODER:
+        index = NGX_HTTP_V3_STREAM_SERVER_DECODER;
+        break;
+    case NGX_HTTP_V3_STREAM_CONTROL:
+        index = NGX_HTTP_V3_STREAM_SERVER_CONTROL;
+        break;
+    default:
+        index = -1;
+    }
+
+    h3c = c->qs->parent->data;
+
+    if (index >= 0) {
+        if (h3c->known_streams[index]) {
+            return h3c->known_streams[index];
+        }
+    }
+
+    sc = ngx_quic_create_uni_stream(c);
+    if (sc == NULL) {
+        return NULL;
+    }
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 create uni stream, type:%ui", type);
+
+    us = ngx_pcalloc(sc->pool, sizeof(ngx_http_v3_uni_stream_t));
+    if (us == NULL) {
+        goto failed;
+    }
+
+    us->index = index;
+
+    sc->data = us;
+
+    sc->read->handler = ngx_http_v3_uni_read_handler;
+    sc->write->handler = ngx_http_v3_dummy_write_handler;
+
+    h3c->known_streams[index] = sc;
+
+    n = (u_char *) ngx_http_v3_encode_varlen_int(buf, type) - buf;
+
+    if (sc->send(sc, buf, n) != (ssize_t) n) {
+        goto failed;
+    }
+
+    return sc;
+
+failed:
+
+    ngx_http_v3_close_uni_stream(sc);
+
+    return NULL;
+}
+
+
+ngx_int_t
+ngx_http_v3_client_ref_insert(ngx_connection_t *c, ngx_uint_t dynamic,
+    ngx_uint_t index, ngx_str_t *value)
+{
+    u_char            *p, buf[NGX_HTTP_V3_PREFIX_INT_LEN * 2];
+    size_t             n;
+    ngx_connection_t  *ec;
+
+    ngx_log_debug3(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 client ref insert, %s[%ui] \"%V\"",
+                   dynamic ? "dynamic" : "static", index, value);
+
+    ec = ngx_http_v3_get_uni_stream(c, NGX_HTTP_V3_STREAM_ENCODER);
+    if (ec == NULL) {
+        return NGX_ERROR;
+    }
+
+    p = buf;
+
+    *p = (dynamic ? 0x80 : 0xc0);
+    p = (u_char *) ngx_http_v3_encode_prefix_int(p, index, 6);
+
+    /* XXX option for huffman? */
+    *p = 0;
+    p = (u_char *) ngx_http_v3_encode_prefix_int(p, value->len, 7);
+
+    n = p - buf;
+
+    if (ec->send(ec, buf, n) != (ssize_t) n) {
+        goto failed;
+    }
+
+    if (ec->send(ec, value->data, value->len) != (ssize_t) value->len) {
+        goto failed;
+    }
+
+    return NGX_OK;
+
+failed:
+
+    ngx_http_v3_close_uni_stream(ec);
+
+    return NGX_ERROR;
+}
+
+
+ngx_int_t
+ngx_http_v3_client_insert(ngx_connection_t *c, ngx_str_t *name,
+    ngx_str_t *value)
+{
+    u_char             buf[NGX_HTTP_V3_PREFIX_INT_LEN];
+    size_t             n;
+    ngx_connection_t  *ec;
+
+    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 client insert \"%V\":\"%V\"", name, value);
+
+    ec = ngx_http_v3_get_uni_stream(c, NGX_HTTP_V3_STREAM_ENCODER);
+    if (ec == NULL) {
+        return NGX_ERROR;
+    }
+
+    /* XXX option for huffman? */
+    buf[0] = 0x40;
+    n = (u_char *) ngx_http_v3_encode_prefix_int(buf, name->len, 5) - buf;
+
+    if (ec->send(ec, buf, n) != (ssize_t) n) {
+        goto failed;
+    }
+
+    if (ec->send(ec, name->data, name->len) != (ssize_t) name->len) {
+        goto failed;
+    }
+
+    /* XXX option for huffman? */
+    buf[0] = 0;
+    n = (u_char *) ngx_http_v3_encode_prefix_int(buf, value->len, 7) - buf;
+
+    if (ec->send(ec, buf, n) != (ssize_t) n) {
+        goto failed;
+    }
+
+    if (ec->send(ec, value->data, value->len) != (ssize_t) value->len) {
+        goto failed;
+    }
+
+    return NGX_OK;
+
+failed:
+
+    ngx_http_v3_close_uni_stream(ec);
+
+    return NGX_ERROR;
+}
+
+
+ngx_int_t
+ngx_http_v3_client_set_capacity(ngx_connection_t *c, ngx_uint_t capacity)
+{
+    u_char             buf[NGX_HTTP_V3_PREFIX_INT_LEN];
+    size_t             n;
+    ngx_connection_t  *ec;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 client set capacity %ui", capacity);
+
+    ec = ngx_http_v3_get_uni_stream(c, NGX_HTTP_V3_STREAM_ENCODER);
+    if (ec == NULL) {
+        return NGX_ERROR;
+    }
+
+    buf[0] = 0x20;
+    n = (u_char *) ngx_http_v3_encode_prefix_int(buf, capacity, 5) - buf;
+
+    if (ec->send(ec, buf, n) != (ssize_t) n) {
+        ngx_http_v3_close_uni_stream(ec);
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_http_v3_client_duplicate(ngx_connection_t *c, ngx_uint_t index)
+{
+    u_char             buf[NGX_HTTP_V3_PREFIX_INT_LEN];
+    size_t             n;
+    ngx_connection_t  *ec;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 client duplicate %ui", index);
+
+    ec = ngx_http_v3_get_uni_stream(c, NGX_HTTP_V3_STREAM_ENCODER);
+    if (ec == NULL) {
+        return NGX_ERROR;
+    }
+
+    buf[0] = 0;
+    n = (u_char *) ngx_http_v3_encode_prefix_int(buf, index, 5) - buf;
+
+    if (ec->send(ec, buf, n) != (ssize_t) n) {
+        ngx_http_v3_close_uni_stream(ec);
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_http_v3_client_ack_header(ngx_connection_t *c, ngx_uint_t stream_id)
+{
+    u_char             buf[NGX_HTTP_V3_PREFIX_INT_LEN];
+    size_t             n;
+    ngx_connection_t  *dc;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 client ack header %ui", stream_id);
+
+    dc = ngx_http_v3_get_uni_stream(c, NGX_HTTP_V3_STREAM_DECODER);
+    if (dc == NULL) {
+        return NGX_ERROR;
+    }
+
+    buf[0] = 0x80;
+    n = (u_char *) ngx_http_v3_encode_prefix_int(buf, stream_id, 7) - buf;
+
+    if (dc->send(dc, buf, n) != (ssize_t) n) {
+        ngx_http_v3_close_uni_stream(dc);
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_http_v3_client_cancel_stream(ngx_connection_t *c, ngx_uint_t stream_id)
+{
+    u_char             buf[NGX_HTTP_V3_PREFIX_INT_LEN];
+    size_t             n;
+    ngx_connection_t  *dc;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 client cancel stream %ui", stream_id);
+
+    dc = ngx_http_v3_get_uni_stream(c, NGX_HTTP_V3_STREAM_DECODER);
+    if (dc == NULL) {
+        return NGX_ERROR;
+    }
+
+    buf[0] = 0x40;
+    n = (u_char *) ngx_http_v3_encode_prefix_int(buf, stream_id, 6) - buf;
+
+    if (dc->send(dc, buf, n) != (ssize_t) n) {
+        ngx_http_v3_close_uni_stream(dc);
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_http_v3_client_inc_insert_count(ngx_connection_t *c, ngx_uint_t inc)
+{
+    u_char             buf[NGX_HTTP_V3_PREFIX_INT_LEN];
+    size_t             n;
+    ngx_connection_t  *dc;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 client increment insert count %ui", inc);
+
+    dc = ngx_http_v3_get_uni_stream(c, NGX_HTTP_V3_STREAM_DECODER);
+    if (dc == NULL) {
+        return NGX_ERROR;
+    }
+
+    buf[0] = 0;
+    n = (u_char *) ngx_http_v3_encode_prefix_int(buf, inc, 6) - buf;
+
+    if (dc->send(dc, buf, n) != (ssize_t) n) {
+        ngx_http_v3_close_uni_stream(dc);
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}
new file mode 100644
--- /dev/null
+++ b/src/http/v3/ngx_http_v3_tables.c
@@ -0,0 +1,416 @@
+
+/*
+ * Copyright (C) Roman Arutyunyan
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_http.h>
+
+
+static ngx_array_t *ngx_http_v3_get_dynamic_table(ngx_connection_t *c);
+static ngx_int_t ngx_http_v3_new_header(ngx_connection_t *c);
+
+
+static ngx_http_v3_header_t  ngx_http_v3_static_table[] = {
+
+    { ngx_string(":authority"),            ngx_string("") },
+    { ngx_string(":path"),                 ngx_string("/") },
+    { ngx_string("age"),                   ngx_string("0") },
+    { ngx_string("content-disposition"),   ngx_string("") },
+    { ngx_string("content-length"),        ngx_string("0") },
+    { ngx_string("cookie"),                ngx_string("") },
+    { ngx_string("date"),                  ngx_string("") },
+    { ngx_string("etag"),                  ngx_string("") },
+    { ngx_string("if-modified-since"),     ngx_string("") },
+    { ngx_string("if-none-match"),         ngx_string("") },
+    { ngx_string("last-modified"),         ngx_string("") },
+    { ngx_string("link"),                  ngx_string("") },
+    { ngx_string("location"),              ngx_string("") },
+    { ngx_string("referer"),               ngx_string("") },
+    { ngx_string("set-cookie"),            ngx_string("") },
+    { ngx_string(":method"),               ngx_string("CONNECT") },
+    { ngx_string(":method"),               ngx_string("DELETE") },
+    { ngx_string(":method"),               ngx_string("GET") },
+    { ngx_string(":method"),               ngx_string("HEAD") },
+    { ngx_string(":method"),               ngx_string("OPTIONS") },
+    { ngx_string(":method"),               ngx_string("POST") },
+    { ngx_string(":method"),               ngx_string("PUT") },
+    { ngx_string(":scheme"),               ngx_string("http") },
+    { ngx_string(":scheme"),               ngx_string("https") },
+    { ngx_string(":status"),               ngx_string("103") },
+    { ngx_string(":status"),               ngx_string("200") },
+    { ngx_string(":status"),               ngx_string("304") },
+    { ngx_string(":status"),               ngx_string("404") },
+    { ngx_string(":status"),               ngx_string("503") },
+    { ngx_string("accept"),                ngx_string("*/*") },
+    { ngx_string("accept"),
+          ngx_string("application/dns-message") },
+    { ngx_string("accept-encoding"),       ngx_string("gzip, deflate, br") },
+    { ngx_string("accept-ranges"),         ngx_string("bytes") },
+    { ngx_string("access-control-allow-headers"),
+                                           ngx_string("cache-control") },
+    { ngx_string("access-control-allow-headers"),
+                                           ngx_string("content-type") },
+    { ngx_string("access-control-allow-origin"),
+                                           ngx_string("*") },
+    { ngx_string("cache-control"),         ngx_string("max-age=0") },
+    { ngx_string("cache-control"),         ngx_string("max-age=2592000") },
+    { ngx_string("cache-control"),         ngx_string("max-age=604800") },
+    { ngx_string("cache-control"),         ngx_string("no-cache") },
+    { ngx_string("cache-control"),         ngx_string("no-store") },
+    { ngx_string("cache-control"),
+          ngx_string("public, max-age=31536000") },
+    { ngx_string("content-encoding"),      ngx_string("br") },
+    { ngx_string("content-encoding"),      ngx_string("gzip") },
+    { ngx_string("content-type"),
+          ngx_string("application/dns-message") },
+    { ngx_string("content-type"),
+          ngx_string("application/javascript") },
+    { ngx_string("content-type"),          ngx_string("application/json") },
+    { ngx_string("content-type"),
+          ngx_string("application/x-www-form-urlencoded") },
+    { ngx_string("content-type"),          ngx_string("image/gif") },
+    { ngx_string("content-type"),          ngx_string("image/jpeg") },
+    { ngx_string("content-type"),          ngx_string("image/png") },
+    { ngx_string("content-type"),          ngx_string("text/css") },
+    { ngx_string("content-type"),
+          ngx_string("text/html;charset=utf-8") },
+    { ngx_string("content-type"),          ngx_string("text/plain") },
+    { ngx_string("content-type"),
+          ngx_string("text/plain;charset=utf-8") },
+    { ngx_string("range"),                 ngx_string("bytes=0-") },
+    { ngx_string("strict-transport-security"),
+                                           ngx_string("max-age=31536000") },
+    { ngx_string("strict-transport-security"),
+          ngx_string("max-age=31536000;includesubdomains") },
+    { ngx_string("strict-transport-security"),
+          ngx_string("max-age=31536000;includesubdomains;preload") },
+    { ngx_string("vary"),                  ngx_string("accept-encoding") },
+    { ngx_string("vary"),                  ngx_string("origin") },
+    { ngx_string("x-content-type-options"),
+                                           ngx_string("nosniff") },
+    { ngx_string("x-xss-protection"),      ngx_string("1;mode=block") },
+    { ngx_string(":status"),               ngx_string("100") },
+    { ngx_string(":status"),               ngx_string("204") },
+    { ngx_string(":status"),               ngx_string("206") },
+    { ngx_string(":status"),               ngx_string("302") },
+    { ngx_string(":status"),               ngx_string("400") },
+    { ngx_string(":status"),               ngx_string("403") },
+    { ngx_string(":status"),               ngx_string("421") },
+    { ngx_string(":status"),               ngx_string("425") },
+    { ngx_string(":status"),               ngx_string("500") },
+    { ngx_string("accept-language"),       ngx_string("") },
+    { ngx_string("access-control-allow-credentials"),
+                                           ngx_string("FALSE") },
+    { ngx_string("access-control-allow-credentials"),
+                                           ngx_string("TRUE") },
+    { ngx_string("access-control-allow-headers"),
+                                           ngx_string("*") },
+    { ngx_string("access-control-allow-methods"),
+                                           ngx_string("get") },
+    { ngx_string("access-control-allow-methods"),
+                                           ngx_string("get, post, options") },
+    { ngx_string("access-control-allow-methods"),
+                                           ngx_string("options") },
+    { ngx_string("access-control-expose-headers"),
+                                           ngx_string("content-length") },
+    { ngx_string("access-control-request-headers"),
+                                           ngx_string("content-type") },
+    { ngx_string("access-control-request-method"),
+                                           ngx_string("get") },
+    { ngx_string("access-control-request-method"),
+                                           ngx_string("post") },
+    { ngx_string("alt-svc"),               ngx_string("clear") },
+    { ngx_string("authorization"),         ngx_string("") },
+    { ngx_string("content-security-policy"),
+          ngx_string("script-src 'none';object-src 'none';base-uri 'none'") },
+    { ngx_string("early-data"),            ngx_string("1") },
+    { ngx_string("expect-ct"),             ngx_string("") },
+    { ngx_string("forwarded"),             ngx_string("") },
+    { ngx_string("if-range"),              ngx_string("") },
+    { ngx_string("origin"),                ngx_string("") },
+    { ngx_string("purpose"),               ngx_string("prefetch") },
+    { ngx_string("server"),                ngx_string("") },
+    { ngx_string("timing-allow-origin"),   ngx_string("*") },
+    { ngx_string("upgrade-insecure-requests"),
+                                           ngx_string("1") },
+    { ngx_string("user-agent"),            ngx_string("") },
+    { ngx_string("x-forwarded-for"),       ngx_string("") },
+    { ngx_string("x-frame-options"),       ngx_string("deny") },
+    { ngx_string("x-frame-options"),       ngx_string("sameorigin") }
+};
+
+
+ngx_int_t
+ngx_http_v3_ref_insert(ngx_connection_t *c, ngx_uint_t dynamic,
+    ngx_uint_t index, ngx_str_t *value)
+{
+    ngx_array_t           *dt;
+    ngx_http_v3_header_t  *ref, *h;
+
+    ngx_log_debug3(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 ref insert %s[$ui] \"%V\"",
+                   dynamic ? "dynamic" : "static", index, value);
+
+    ref = ngx_http_v3_lookup_table(c, dynamic, index);
+    if (ref == NULL) {
+        return NGX_ERROR;
+    }
+
+    dt = ngx_http_v3_get_dynamic_table(c);
+    if (dt == NULL) {
+        return NGX_ERROR;
+    }
+
+    h = ngx_array_push(dt);
+    if (h == NULL) {
+        return NGX_ERROR;
+    }
+
+    h->name = ref->name;
+    h->value = *value;
+
+    if (ngx_http_v3_new_header(c) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_http_v3_insert(ngx_connection_t *c, ngx_str_t *name,
+    ngx_str_t *value)
+{
+    ngx_array_t           *dt;
+    ngx_http_v3_header_t  *h;
+
+    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 insert \"%V\":\"%V\"", name, value);
+
+    dt = ngx_http_v3_get_dynamic_table(c);
+    if (dt == NULL) {
+        return NGX_ERROR;
+    }
+
+    h = ngx_array_push(dt);
+    if (h == NULL) {
+        return NGX_ERROR;
+    }
+
+    h->name = *name;
+    h->value = *value;
+
+    if (ngx_http_v3_new_header(c) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_http_v3_set_capacity(ngx_connection_t *c, ngx_uint_t capacity)
+{
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 set capacity %ui", capacity);
+
+    /* XXX ignore capacity */
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_http_v3_duplicate(ngx_connection_t *c, ngx_uint_t index)
+{
+    ngx_array_t           *dt;
+    ngx_http_v3_header_t  *ref, *h;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 duplicate %ui", index);
+
+    ref = ngx_http_v3_lookup_table(c, 1, index);
+    if (ref == NULL) {
+        return NGX_ERROR;
+    }
+
+    dt = ngx_http_v3_get_dynamic_table(c);
+    if (dt == NULL) {
+        return NGX_ERROR;
+    }
+
+    h = ngx_array_push(dt);
+    if (h == NULL) {
+        return NGX_ERROR;
+    }
+
+    *h = *ref;
+
+    if (ngx_http_v3_new_header(c) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_http_v3_ack_header(ngx_connection_t *c, ngx_uint_t stream_id)
+{
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 ack header %ui", stream_id);
+
+    /* XXX */
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_http_v3_cancel_stream(ngx_connection_t *c, ngx_uint_t stream_id)
+{
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 cancel stream %ui", stream_id);
+
+    /* XXX */
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_http_v3_inc_insert_count(ngx_connection_t *c, ngx_uint_t inc)
+{
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 increment insert count %ui", inc);
+
+    /* XXX */
+
+    return NGX_OK;
+}
+
+
+static ngx_array_t *
+ngx_http_v3_get_dynamic_table(ngx_connection_t *c)
+{
+    ngx_connection_t          *pc;
+    ngx_http_v3_connection_t  *h3c;
+
+    pc = c->qs->parent;
+    h3c = pc->data;
+
+    if (h3c->dynamic) {
+        return h3c->dynamic;
+    }
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 create dynamic table");
+
+    h3c->dynamic = ngx_array_create(pc->pool, 1, sizeof(ngx_http_v3_header_t));
+
+    return h3c->dynamic;
+}
+
+
+ngx_http_v3_header_t *
+ngx_http_v3_lookup_table(ngx_connection_t *c, ngx_uint_t dynamic,
+    ngx_uint_t index)
+{
+    ngx_uint_t             nelts;
+    ngx_array_t           *dt;
+    ngx_http_v3_header_t  *table;
+
+    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 lookup %s[%ui]",
+                   dynamic ? "dynamic" : "static", index);
+
+    if (dynamic) {
+        dt = ngx_http_v3_get_dynamic_table(c);
+        if (dt == NULL) {
+            return NULL;
+        }
+
+        table = dt->elts;
+        nelts = dt->nelts;
+
+    } else {
+        table = ngx_http_v3_static_table;
+        nelts = sizeof(ngx_http_v3_static_table)
+                / sizeof(ngx_http_v3_static_table[0]);
+    }
+
+    if (index >= nelts) {
+        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 lookup out of bounds: %ui", nelts);
+        return NULL;
+    }
+
+    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 lookup \"%V\":\"%V\"",
+                   &table[index].name, &table[index].value);
+
+    return &table[index];
+}
+
+
+ngx_int_t
+ngx_http_v3_check_insert_count(ngx_connection_t *c, ngx_uint_t insert_count)
+{
+    size_t                     n;
+    ngx_http_v3_connection_t  *h3c;
+
+    h3c = c->qs->parent->data;
+    n = h3c->dynamic ? h3c->dynamic->nelts : 0;
+
+    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 check insert count %ui/%ui", insert_count, n);
+
+    if (n < insert_count) {
+        /* XXX how to get notified? */
+        /* XXX wake all streams on any arrival to the encoder stream? */
+        return NGX_AGAIN;
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_http_v3_new_header(ngx_connection_t *c)
+{
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 new dynamic header");
+
+    /* XXX report all waiting streams of a new header */
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_http_v3_set_param(ngx_connection_t *c, uint64_t id, uint64_t value)
+{
+    switch (id) {
+
+    case NGX_HTTP_V3_PARAM_MAX_TABLE_CAPACITY:
+        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 param QPACK_MAX_TABLE_CAPACITY:%uL", value);
+        break;
+
+    case NGX_HTTP_V3_PARAM_MAX_HEADER_LIST_SIZE:
+        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 param SETTINGS_MAX_HEADER_LIST_SIZE:%uL", value);
+        break;
+
+    case NGX_HTTP_V3_PARAM_BLOCKED_STREAMS:
+        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 param QPACK_BLOCKED_STREAMS:%uL", value);
+        break;
+
+    default:
+
+        ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 param #%uL:%uL", id, value);
+    }
+
+    return NGX_OK;
+}