changeset 8647:507da0d3b070 quic

Merged with the default branch.
author Sergey Kandaurov <pluknet@nginx.com>
date Tue, 24 Nov 2020 17:19:40 +0000
parents 4bf332873a83 (diff) 66a441bf669b (current diff)
children b80d9179ba2a
files auto/make auto/options src/event/ngx_event_openssl.c src/http/ngx_http_request.c src/http/ngx_http_request_body.c
diffstat 47 files changed, 17331 insertions(+), 110 deletions(-) [+]
line wrap: on
line diff
new file mode 100644
--- /dev/null
+++ b/README
@@ -0,0 +1,256 @@
+Experimental QUIC support for nginx
+-----------------------------------
+
+1. Introduction
+2. Installing
+3. Configuration
+4. Clients
+5. Troubleshooting
+6. Contributing
+7. Links
+
+1. Introduction
+
+    This is an experimental QUIC [1] / HTTP/3 [2] support for nginx.
+
+    The code is developed in a separate "quic" branch available
+    at https://hg.nginx.org/nginx-quic.  Currently it is based
+    on nginx mainline 1.19.x.  We merge new nginx releases into
+    this branch regularly.
+
+    The project code base is under the same BSD license as nginx.
+
+    The code is currently at a beta level of quality and should not
+    be used in production.
+
+    We are working on improving HTTP/3 support with the goal of
+    integrating it to the main NGINX codebase.  Expect frequent
+    updates of this code and don't rely on it for whatever purpose.
+
+    We'll be grateful for any feedback and code submissions however
+    we don't bear any responsibilities for any issues with this code.
+
+    You can always contact us via nginx-devel mailing list [3].
+
+    What works now:
+
+    Currently we support IETF-QUIC draft-27 through draft-32.
+    Earlier drafts are NOT supported as they have incompatible wire format.
+
+    You may look at src/event/ngx_event_quic.h for alternative values of the
+    NGX_QUIC_DRAFT_VERSION macro used to select IETF draft version number.
+
+    nginx should be able to respond to HTTP/3 requests over QUIC and
+    it should be possible to upload and download big files without errors.
+
+    + The handshake completes successfully
+    + One endpoint can update keys and its peer responds correctly
+    + 0-RTT data is being received and acted on
+    + Connection is established using TLS Resume Ticket
+    + A handshake that includes a Retry packet completes successfully
+    + Stream data is being exchanged and ACK'ed
+    + An H3 transaction succeeded
+    + One or both endpoints insert entries into dynamic table and
+      subsequently reference them from header blocks
+    + Version Negotiation packet is sent to client with unknown version
+    + Lost packets are detected and retransmitted properly
+
+     Not (yet) supported features:
+
+    - Explicit Congestion Notification (ECN) as specified in quic-recovery [5]
+    - A connection with the spin bit succeeds and the bit is spinning
+    - Structured Logging
+    - NAT Rebinding
+    - Address Mobility
+    - HTTP/3 trailers
+
+    Since the code is experimental and still under development,
+    a lot of things may not work as expected, for example:
+
+    - Flow control mechanism is basic and intended to avoid CPU hog and make
+      simple interactions possible
+
+    - Not all draft requirements are strictly followed; some of checks are
+      omitted for the sake of simplicity of initial implementation
+
+2. Installing
+
+    You will need a BoringSSL [4] library that provides QUIC support
+
+    $ hg clone -b quic https://hg.nginx.org/nginx-quic
+    $ cd nginx-quic
+    $ ./auto/configure --with-debug --with-http_v3_module       \
+                       --with-cc-opt="-I../boringssl/include"   \
+                       --with-ld-opt="-L../boringssl/build/ssl  \
+                                      -L../boringssl/build/crypto"
+    $ make
+
+    When configuring nginx, you can enable QUIC and HTTP/3 using the
+    following new configuration options:
+
+        --with-http_v3_module     - enable QUIC and HTTP/3
+        --with-http_quic_module   - enable QUIC for older HTTP versions
+        --with-stream_quic_module - enable QUIC in Stream
+
+3. Configuration
+
+    The HTTP "listen" directive got two new options: "http3" and "quic".
+    The "http3" option enables HTTP/3 over QUIC on the specified port.
+    The "quic" option enables QUIC for older HTTP versions on this port.
+
+    The Stream "listen" directive got a new option "quic" which enables
+    QUIC as client transport protocol instead of TCP or plain UDP.
+
+    Along with "http3" or "quic", you also have to specify "reuseport"
+    option [6] to make it work properly with multiple workers.
+
+    A number of directives were added that specify transport parameter values:
+
+        quic_max_idle_timeout
+        quic_max_ack_delay
+        quic_max_packet_size
+        quic_initial_max_data
+        quic_initial_max_stream_data_bidi_local
+        quic_initial_max_stream_data_bidi_remote
+        quic_initial_max_stream_data_uni
+        quic_initial_max_streams_bidi
+        quic_initial_max_streams_uni
+        quic_ack_delay_exponent
+        quic_active_migration
+        quic_active_connection_id_limit
+
+    To enable address validation:
+
+        quic_retry on;
+
+    To enable 0-RTT:
+
+        ssl_early_data on;
+
+    Make sure that TLS 1.3 is configured which is required for QUIC:
+
+        ssl_protocols TLSv1.3;
+
+    A number of directives were added that configure HTTP/3:
+
+        http3_max_field_size
+        http3_max_table_capacity
+        http3_max_blocked_streams
+        http3_max_concurrent_pushes
+        http3_push
+        http3_push_preload
+
+    Two additional variables are available: $quic and $http3.
+    The value of $quic is "quic" if QUIC connection is used,
+    and empty string otherwise. The value of $http3 is a string
+    "h3-xx" where "xx" is the supported draft number.
+
+Example configuration:
+
+    http {
+        log_format quic '$remote_addr - $remote_user [$time_local] '
+                        '"$request" $status $body_bytes_sent '
+                        '"$http_referer" "$http_user_agent" "$quic" "$http3"';
+
+        access_log logs/access.log quic;
+
+        server {
+            # for better compatibility it's recommended
+            # to use the same port for quic and https
+            listen 8443 http3 reuseport;
+            listen 8443 ssl;
+
+            ssl_certificate     certs/example.com.crt;
+            ssl_certificate_key certs/example.com.key;
+            ssl_protocols       TLSv1.3;
+
+            location / {
+                # required for browsers to direct them into quic port
+                add_header Alt-Svc '$http3=":8443"; ma=86400';
+            }
+        }
+    }
+
+4. Clients
+
+    * Browsers
+
+        Known to work: Firefox 75+ and Chrome 83+
+
+        Beware of strange issues: sometimes browser may decide to ignore QUIC
+        Cache clearing/restart might help.  Always check access.log and
+        error.log to make sure you are using HTTP/3 and not TCP https.
+
+        + to enable QUIC in Firefox, set the following in 'about:config':
+          network.http.http3.enabled = true
+
+        + to enable QUIC in Chrome, enable it on command line and force it
+          on your site:
+
+        $ ./chrome --enable-quic --quic-version=h3-29 \
+                       --origin-to-force-quic-on=example.com:8443
+
+    * Console clients
+
+        Known to work: ngtcp2, firefox's neqo and chromium's console clients:
+
+        $ examples/client 127.0.0.1 8443 https://example.com:8443/index.html
+
+        $ ./neqo-client https://127.0.0.1:8443/
+
+        $ chromium-build/out/my_build/quic_client http://example.com:8443 \
+                  --quic_version=h3-29 \
+                  --allow_unknown_root_cert \
+                  --disable_certificate_verification
+
+
+   If you've got it right, in the access log you should see something like:
+
+   127.0.0.1 - - [24/Apr/2020:11:27:29 +0300] "GET / HTTP/3" 200 805 "-"
+                                         "nghttp3/ngtcp2 client" "quic" "h3-29"
+
+
+5. Troubleshooting
+
+    Here are some tips that may help you to identify problems:
+
+    + Ensure you are building with proper SSL library that supports QUIC
+
+    + Ensure you are using the proper SSL library in runtime
+      (`nginx -V` will show you what you are using)
+
+    + Ensure your client is actually sending QUIC requests
+      (see "Clients" section about browsers and cache)
+
+      We recommend to start with simple console client like ngtcp2
+      to ensure you've got server configured properly before trying
+      with real browsers that may be very picky with certificates,
+      for example.
+
+    + Build nginx with debug support [7] and check your debug log.
+      It should contain all details about connection and why it
+      failed. All related messages contain "quic " prefix and can
+      be easily filtered out.
+
+    + If you want to investigate deeper, you may want to enable
+      additional debugging in src/event/ngx_event_quic.h:
+
+        #define NGX_QUIC_DEBUG_PACKETS
+        #define NGX_QUIC_DEBUG_FRAMES
+        #define NGX_QUIC_DEBUG_FRAMES_ALLOC
+        #define NGX_QUIC_DEBUG_CRYPTO
+
+6. Contributing
+
+    If you are willing to contribute, please refer to
+    http://nginx.org/en/docs/contributing_changes.html
+
+7. Links
+
+    [1] https://tools.ietf.org/html/draft-ietf-quic-transport
+    [2] https://tools.ietf.org/html/draft-ietf-quic-http
+    [3] https://mailman.nginx.org/mailman/listinfo/nginx-devel
+    [4] https://boringssl.googlesource.com/boringssl/
+    [5] https://tools.ietf.org/html/draft-ietf-quic-recovery
+    [6] https://nginx.org/en/docs/http/ngx_http_core_module.html#listen
+    [7] https://nginx.org/en/docs/debugging_log.html
--- a/auto/lib/openssl/conf
+++ b/auto/lib/openssl/conf
@@ -140,3 +140,43 @@ END
     fi
 
 fi
+
+
+if [ $USE_OPENSSL_QUIC = YES ]; then
+
+    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_LIBPTHREAD"
+    ngx_feature_test="SSL_CTX_set_quic_method(NULL, NULL)"
+    . auto/feature
+
+    if [ $ngx_found = no ]; then
+
+cat << END
+
+$0: error: certain modules require OpenSSL QUIC support.
+You can either do not enable the modules, or install the OpenSSL library
+into the system, or build the OpenSSL library statically from the source
+with nginx by using --with-openssl=<path> option.
+
+END
+        exit 1
+    fi
+
+    have=NGX_QUIC . auto/have
+fi
+
+
+if [ $USE_OPENSSL_QUIC = YES ]; then
+    ngx_feature="OpenSSL QUIC 0-RTT context"
+    ngx_feature_name="NGX_OPENSSL_QUIC_ZRTT_CTX"
+    ngx_feature_run=no
+    ngx_feature_incs="#include <openssl/ssl.h>"
+    ngx_feature_path=
+    ngx_feature_libs="-lssl -lcrypto $NGX_LIBDL $NGX_LIBPTHREAD"
+    ngx_feature_test="SSL_set_quic_early_data_context(NULL, NULL, 0)"
+    . auto/feature
+fi
--- 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,30 @@ 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
+        HTTP_QUIC=YES
+
+        # 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_encode.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
@@ -669,6 +693,21 @@ if [ $HTTP = YES ]; then
         . auto/module
     fi
 
+    if [ $HTTP_QUIC = YES ]; then
+        USE_OPENSSL_QUIC=YES
+        have=NGX_HTTP_QUIC . auto/have
+        HTTP_SSL=YES
+
+        ngx_module_name=ngx_http_quic_module
+        ngx_module_incs=
+        ngx_module_deps=src/http/modules/ngx_http_quic_module.h
+        ngx_module_srcs=src/http/modules/ngx_http_quic_module.c
+        ngx_module_libs=
+        ngx_module_link=$HTTP_QUIC
+
+        . auto/module
+    fi
+
     if [ $HTTP_SSL = YES ]; then
         USE_OPENSSL=YES
         have=NGX_HTTP_SSL . auto/have
@@ -1026,6 +1065,20 @@ if [ $STREAM != NO ]; then
 
     ngx_module_incs=
 
+    if [ $STREAM_QUIC = YES ]; then
+        USE_OPENSSL_QUIC=YES
+        have=NGX_STREAM_QUIC . auto/have
+        STREAM_SSL=YES
+
+        ngx_module_name=ngx_stream_quic_module
+        ngx_module_deps=src/stream/ngx_stream_quic_module.h
+        ngx_module_srcs=src/stream/ngx_stream_quic_module.c
+        ngx_module_libs=
+        ngx_module_link=$STREAM_QUIC
+
+        . auto/module
+    fi
+
     if [ $STREAM_SSL = YES ]; then
         USE_OPENSSL=YES
         have=NGX_STREAM_SSL . auto/have
@@ -1259,6 +1312,17 @@ if [ $USE_OPENSSL = YES ]; then
     ngx_module_link=YES
     ngx_module_order=
 
+    if [ $USE_OPENSSL_QUIC = YES ]; then
+        ngx_module_deps="$ngx_module_deps \
+                         src/event/ngx_event_quic.h \
+                         src/event/ngx_event_quic_transport.h \
+                         src/event/ngx_event_quic_protection.h"
+        ngx_module_srcs="$ngx_module_srcs \
+                         src/event/ngx_event_quic.c \
+                         src/event/ngx_event_quic_transport.c \
+                         src/event/ngx_event_quic_protection.c"
+    fi
+
     . auto/module
 fi
 
--- a/auto/options
+++ b/auto/options
@@ -58,7 +58,9 @@ HTTP_CACHE=YES
 HTTP_CHARSET=YES
 HTTP_GZIP=YES
 HTTP_SSL=NO
+HTTP_QUIC=NO
 HTTP_V2=NO
+HTTP_V3=NO
 HTTP_SSI=YES
 HTTP_REALIP=NO
 HTTP_XSLT=NO
@@ -116,6 +118,7 @@ MAIL_SMTP=YES
 
 STREAM=NO
 STREAM_SSL=NO
+STREAM_QUIC=NO
 STREAM_REALIP=NO
 STREAM_LIMIT_CONN=YES
 STREAM_ACCESS=YES
@@ -148,6 +151,7 @@ PCRE_CONF_OPT=
 PCRE_JIT=NO
 
 USE_OPENSSL=NO
+USE_OPENSSL_QUIC=NO
 OPENSSL=NONE
 
 USE_ZLIB=NO
@@ -226,7 +230,9 @@ do
         --http-scgi-temp-path=*)         NGX_HTTP_SCGI_TEMP_PATH="$value" ;;
 
         --with-http_ssl_module)          HTTP_SSL=YES               ;;
+        --with-http_quic_module)         HTTP_QUIC=YES              ;;
         --with-http_v2_module)           HTTP_V2=YES                ;;
+        --with-http_v3_module)           HTTP_V3=YES                ;;
         --with-http_realip_module)       HTTP_REALIP=YES            ;;
         --with-http_addition_module)     HTTP_ADDITION=YES          ;;
         --with-http_xslt_module)         HTTP_XSLT=YES              ;;
@@ -313,6 +319,7 @@ use the \"--with-mail_ssl_module\" optio
         --with-stream)                   STREAM=YES                 ;;
         --with-stream=dynamic)           STREAM=DYNAMIC             ;;
         --with-stream_ssl_module)        STREAM_SSL=YES             ;;
+        --with-stream_quic_module)       STREAM_QUIC=YES            ;;
         --with-stream_realip_module)     STREAM_REALIP=YES          ;;
         --with-stream_geoip_module)      STREAM_GEOIP=YES           ;;
         --with-stream_geoip_module=dynamic)
@@ -442,7 +449,9 @@ cat << END
   --with-file-aio                    enable file AIO support
 
   --with-http_ssl_module             enable ngx_http_ssl_module
+  --with-http_quic_module            enable ngx_http_quic_module
   --with-http_v2_module              enable ngx_http_v2_module
+  --with-http_v3_module              enable ngx_http_v3_module
   --with-http_realip_module          enable ngx_http_realip_module
   --with-http_addition_module        enable ngx_http_addition_module
   --with-http_xslt_module            enable ngx_http_xslt_module
@@ -531,6 +540,7 @@ cat << END
   --with-stream                      enable TCP/UDP proxy module
   --with-stream=dynamic              enable dynamic TCP/UDP proxy module
   --with-stream_ssl_module           enable ngx_stream_ssl_module
+  --with-stream_quic_module          enable ngx_stream_quic_module
   --with-stream_realip_module        enable ngx_stream_realip_module
   --with-stream_geoip_module         enable ngx_stream_geoip_module
   --with-stream_geoip_module=dynamic enable dynamic ngx_stream_geoip_module
--- a/src/core/ngx_connection.c
+++ b/src/core/ngx_connection.c
@@ -1034,6 +1034,12 @@ ngx_close_listening_sockets(ngx_cycle_t 
     ls = cycle->listening.elts;
     for (i = 0; i < cycle->listening.nelts; i++) {
 
+#if (NGX_QUIC)
+        if (ls[i].quic) {
+            continue;
+        }
+#endif
+
         c = ls[i].connection;
 
         if (c) {
@@ -1176,11 +1182,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);
     }
@@ -1189,7 +1190,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);
 
@@ -1221,6 +1222,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
@@ -75,6 +75,7 @@ struct ngx_listening_s {
     unsigned            reuseport:1;
     unsigned            add_reuseport:1;
     unsigned            keepalive:2;
+    unsigned            quic:1;
 
     unsigned            deferred_accept:1;
     unsigned            delete_deferred:1;
@@ -149,6 +150,10 @@ struct ngx_connection_s {
 
     ngx_proxy_protocol_t  *proxy_protocol;
 
+#if (NGX_QUIC || NGX_COMPAT)
+    ngx_quic_stream_t     *quic;
+#endif
+
 #if (NGX_SSL || NGX_COMPAT)
     ngx_ssl_connection_t  *ssl;
 #endif
--- a/src/core/ngx_core.h
+++ b/src/core/ngx_core.h
@@ -27,6 +27,7 @@ typedef struct ngx_connection_s      ngx
 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_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;
 
@@ -82,6 +83,9 @@ typedef void (*ngx_connection_handler_pt
 #include <ngx_resolver.h>
 #if (NGX_OPENSSL)
 #include <ngx_event_openssl.h>
+#if (NGX_OPENSSL_QUIC)
+#include <ngx_event_quic.h>
+#endif
 #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,26 @@ ngx_process_events_and_timers(ngx_cycle_
 ngx_int_t
 ngx_handle_read_event(ngx_event_t *rev, ngx_uint_t flags)
 {
+#if (NGX_QUIC)
+
+    ngx_connection_t  *c;
+
+    c = rev->data;
+
+    if (c->quic) {
+
+        if (!rev->active && !rev->ready) {
+            rev->active = 1;
+
+        } else if (rev->active && (rev->ready || (flags & NGX_CLOSE_EVENT))) {
+            rev->active = 0;
+        }
+
+        return NGX_OK;
+    }
+
+#endif
+
     if (ngx_event_flags & NGX_USE_CLEAR_EVENT) {
 
         /* kqueue, epoll */
@@ -338,14 +358,30 @@ 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 (NGX_QUIC)
+
+    if (c->quic) {
+
+        if (!wev->active && !wev->ready) {
+            wev->active = 1;
+
+        } else if (wev->active && wev->ready) {
+            wev->active = 0;
+        }
+
+        return NGX_OK;
+    }
+
+#endif
+
     if (ngx_event_flags & NGX_USE_CLEAR_EVENT) {
 
         /* kqueue, epoll */
@@ -916,6 +952,12 @@ ngx_send_lowat(ngx_connection_t *c, size
 {
     int  sndlowat;
 
+#if (NGX_QUIC)
+    if (c->quic) {
+        return NGX_OK;
+    }
+#endif
+
 #if (NGX_HAVE_LOWAT_EVENT)
 
     if (ngx_event_flags & NGX_USE_KQUEUE_EVENT) {
--- a/src/event/ngx_event.h
+++ b/src/event/ngx_event.h
@@ -167,6 +167,18 @@ struct ngx_event_aio_s {
 #endif
 
 
+#if !(NGX_WIN32)
+
+struct ngx_udp_connection_s {
+    ngx_rbtree_node_t          node;
+    ngx_connection_t          *connection;
+    ngx_str_t                  key;
+    ngx_buf_t                 *buffer;
+};
+
+#endif
+
+
 typedef struct {
     ngx_int_t  (*add)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);
     ngx_int_t  (*del)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);
@@ -501,6 +513,8 @@ void ngx_event_accept(ngx_event_t *ev);
 void ngx_event_recvmsg(ngx_event_t *ev);
 void ngx_udp_rbtree_insert_value(ngx_rbtree_node_t *temp,
     ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel);
+void ngx_insert_udp_connection(ngx_connection_t *c, ngx_udp_connection_t *udp,
+    ngx_str_t *key);
 #endif
 void ngx_delete_udp_connection(void *data);
 ngx_int_t ngx_trylock_accept_mutex(ngx_cycle_t *cycle);
--- a/src/event/ngx_event_openssl.c
+++ b/src/event/ngx_event_openssl.c
@@ -2869,6 +2869,13 @@ ngx_ssl_shutdown(ngx_connection_t *c)
     ngx_err_t   err;
     ngx_uint_t  tries;
 
+#if (NGX_QUIC)
+    if (c->quic) {
+        /* QUIC streams inherit SSL object */
+        return NGX_OK;
+    }
+#endif
+
     ngx_ssl_ocsp_cleanup(c);
 
     if (SSL_in_init(c->ssl->connection)) {
--- 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,6120 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_event.h>
+#include <ngx_event_quic_transport.h>
+#include <ngx_event_quic_protection.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_get_send_ctx(qc, level)                                      \
+    ((level) == ssl_encryption_initial) ? &((qc)->send_ctx[0])                \
+        : (((level) == ssl_encryption_handshake) ? &((qc)->send_ctx[1])       \
+                                                 : &((qc)->send_ctx[2]))
+
+#define NGX_QUIC_SEND_CTX_LAST  (NGX_QUIC_ENCRYPTION_LAST - 1)
+
+/*
+ * 7.4.  Cryptographic Message Buffering
+ *       Implementations MUST support buffering at least 4096 bytes of data
+ */
+#define NGX_QUIC_MAX_BUFFERED    65535
+
+#define NGX_QUIC_STREAM_GONE     (void *) -1
+
+#define NGX_QUIC_UNSET_PN        (uint64_t) -1
+
+/*
+ * Endpoints MUST discard packets that are too small to be valid QUIC
+ * packets.  With the set of AEAD functions defined in [QUIC-TLS],
+ * packets that are smaller than 21 bytes are never valid.
+ */
+#define NGX_QUIC_MIN_PKT_LEN     21
+
+#define NGX_QUIC_MIN_SR_PACKET   43 /* 5 random + 16 srt + 22 padding */
+#define NGX_QUIC_MAX_SR_PACKET   1200
+
+#define NGX_QUIC_MAX_ACK_GAP     2
+
+
+typedef struct {
+    ngx_rbtree_t                      tree;
+    ngx_rbtree_node_t                 sentinel;
+
+    uint64_t                          received;
+    uint64_t                          sent;
+    uint64_t                          recv_max_data;
+    uint64_t                          send_max_data;
+
+    uint64_t                          server_max_streams_uni;
+    uint64_t                          server_max_streams_bidi;
+    uint64_t                          server_streams_uni;
+    uint64_t                          server_streams_bidi;
+
+    uint64_t                          client_max_streams_uni;
+    uint64_t                          client_max_streams_bidi;
+    uint64_t                          client_streams_uni;
+    uint64_t                          client_streams_bidi;
+} ngx_quic_streams_t;
+
+
+typedef struct {
+    size_t                            in_flight;
+    size_t                            window;
+    size_t                            ssthresh;
+    ngx_msec_t                        recovery_start;
+} ngx_quic_congestion_t;
+
+
+/*
+ * 12.3.  Packet Numbers
+ *
+ *  Conceptually, a packet number space is the context in which a packet
+ *  can be processed and acknowledged.  Initial packets can only be sent
+ *  with Initial packet protection keys and acknowledged in packets which
+ *  are also Initial packets.
+*/
+typedef struct {
+    enum ssl_encryption_level_t       level;
+
+    uint64_t                          pnum;        /* to be sent */
+    uint64_t                          largest_ack; /* received from peer */
+    uint64_t                          largest_pn;  /* received from peer */
+
+    ngx_queue_t                       frames;
+    ngx_queue_t                       sent;
+
+    uint64_t                          pending_ack; /* non sent ack-eliciting */
+    uint64_t                          largest_range;
+    uint64_t                          first_range;
+    ngx_msec_t                        largest_received;
+    ngx_msec_t                        ack_delay_start;
+    ngx_uint_t                        nranges;
+    ngx_quic_ack_range_t              ranges[NGX_QUIC_MAX_RANGES];
+    ngx_uint_t                        send_ack;
+} ngx_quic_send_ctx_t;
+
+
+typedef struct {
+    ngx_udp_connection_t              udp;
+
+    uint32_t                          version;
+    ngx_str_t                         scid;  /* initial client ID */
+    ngx_str_t                         dcid;  /* server (our own) ID */
+    ngx_str_t                         odcid; /* original server ID */
+    ngx_str_t                         token;
+
+    struct sockaddr                  *sockaddr;
+    socklen_t                         socklen;
+
+    ngx_queue_t                       client_ids;
+    ngx_queue_t                       server_ids;
+    ngx_queue_t                       free_client_ids;
+    ngx_queue_t                       free_server_ids;
+    ngx_uint_t                        nclient_ids;
+    ngx_uint_t                        nserver_ids;
+    uint64_t                          max_retired_seqnum;
+    uint64_t                          client_seqnum;
+    uint64_t                          server_seqnum;
+
+    ngx_uint_t                        client_tp_done;
+    ngx_quic_tp_t                     tp;
+    ngx_quic_tp_t                     ctp;
+
+    ngx_quic_send_ctx_t               send_ctx[NGX_QUIC_SEND_CTX_LAST];
+
+    ngx_quic_frames_stream_t          crypto[NGX_QUIC_ENCRYPTION_LAST];
+
+    ngx_quic_keys_t                  *keys;
+
+    ngx_quic_conf_t                  *conf;
+
+    ngx_event_t                       push;
+    ngx_event_t                       pto;
+    ngx_event_t                       close;
+    ngx_queue_t                       free_frames;
+    ngx_msec_t                        last_cc;
+
+    ngx_msec_t                        latest_rtt;
+    ngx_msec_t                        avg_rtt;
+    ngx_msec_t                        min_rtt;
+    ngx_msec_t                        rttvar;
+
+    ngx_uint_t                        pto_count;
+
+#if (NGX_DEBUG)
+    ngx_uint_t                        nframes;
+#endif
+
+    ngx_quic_streams_t                streams;
+    ngx_quic_congestion_t             congestion;
+    size_t                            received;
+
+    ngx_uint_t                        error;
+    enum ssl_encryption_level_t       error_level;
+    ngx_uint_t                        error_ftype;
+    const char                       *error_reason;
+
+    unsigned                          error_app:1;
+    unsigned                          send_timer_set:1;
+    unsigned                          closing:1;
+    unsigned                          draining:1;
+    unsigned                          key_phase:1;
+    unsigned                          in_retry:1;
+    unsigned                          initialized:1;
+    unsigned                          validated:1;
+} ngx_quic_connection_t;
+
+
+typedef struct {
+    ngx_queue_t                       queue;
+    uint64_t                          seqnum;
+    size_t                            len;
+    u_char                            id[NGX_QUIC_CID_LEN_MAX];
+    u_char                            sr_token[NGX_QUIC_SR_TOKEN_LEN];
+} ngx_quic_client_id_t;
+
+
+typedef struct {
+    ngx_udp_connection_t              udp;
+    ngx_queue_t                       queue;
+    uint64_t                          seqnum;
+    size_t                            len;
+    u_char                            id[NGX_QUIC_CID_LEN_MAX];
+} ngx_quic_server_id_t;
+
+
+typedef ngx_int_t (*ngx_quic_frame_handler_pt)(ngx_connection_t *c,
+    ngx_quic_frame_t *frame, void *data);
+
+
+#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_quic_connection_t *ngx_quic_new_connection(ngx_connection_t *c,
+    ngx_quic_conf_t *conf, ngx_quic_header_t *pkt);
+static ngx_int_t ngx_quic_send_stateless_reset(ngx_connection_t *c,
+    ngx_quic_conf_t *conf, ngx_quic_header_t *pkt);
+static ngx_int_t ngx_quic_process_stateless_reset(ngx_connection_t *c,
+    ngx_quic_header_t *pkt);
+static ngx_int_t ngx_quic_negotiate_version(ngx_connection_t *c,
+    ngx_quic_header_t *inpkt);
+static ngx_int_t ngx_quic_create_server_id(ngx_connection_t *c, u_char *id);
+static ngx_int_t ngx_quic_send_retry(ngx_connection_t *c);
+static ngx_int_t ngx_quic_new_token(ngx_connection_t *c, ngx_str_t *token);
+static ngx_int_t ngx_quic_validate_token(ngx_connection_t *c,
+    ngx_quic_header_t *pkt);
+static ngx_int_t ngx_quic_init_connection(ngx_connection_t *c);
+static ngx_inline size_t ngx_quic_max_udp_payload(ngx_connection_t *c);
+static void ngx_quic_input_handler(ngx_event_t *rev);
+
+static void ngx_quic_close_connection(ngx_connection_t *c, ngx_int_t rc);
+static ngx_int_t ngx_quic_close_quic(ngx_connection_t *c, ngx_int_t rc);
+static void ngx_quic_close_timer_handler(ngx_event_t *ev);
+static ngx_int_t ngx_quic_close_streams(ngx_connection_t *c,
+    ngx_quic_connection_t *qc);
+
+static ngx_int_t ngx_quic_input(ngx_connection_t *c, ngx_buf_t *b,
+    ngx_quic_conf_t *conf);
+static ngx_int_t ngx_quic_process_packet(ngx_connection_t *c,
+    ngx_quic_conf_t *conf, ngx_quic_header_t *pkt);
+static ngx_int_t ngx_quic_init_secrets(ngx_connection_t *c);
+static void ngx_quic_discard_ctx(ngx_connection_t *c,
+    enum ssl_encryption_level_t level);
+static ngx_int_t ngx_quic_check_peer(ngx_quic_connection_t *qc,
+    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_ack_packet(ngx_connection_t *c,
+    ngx_quic_header_t *pkt);
+static ngx_int_t ngx_quic_send_ack_range(ngx_connection_t *c,
+    ngx_quic_send_ctx_t *ctx, uint64_t smallest, uint64_t largest);
+static void ngx_quic_drop_ack_ranges(ngx_connection_t *c,
+    ngx_quic_send_ctx_t *ctx, uint64_t pn);
+static ngx_int_t ngx_quic_send_ack(ngx_connection_t *c,
+    ngx_quic_send_ctx_t *ctx);
+static ngx_int_t ngx_quic_send_cc(ngx_connection_t *c);
+static ngx_int_t ngx_quic_send_new_token(ngx_connection_t *c);
+
+static ngx_int_t ngx_quic_handle_ack_frame(ngx_connection_t *c,
+    ngx_quic_header_t *pkt, ngx_quic_ack_frame_t *f);
+static ngx_int_t ngx_quic_handle_ack_frame_range(ngx_connection_t *c,
+    ngx_quic_send_ctx_t *ctx, uint64_t min, uint64_t max,
+    ngx_msec_t *send_time);
+static void ngx_quic_rtt_sample(ngx_connection_t *c, ngx_quic_ack_frame_t *ack,
+    enum ssl_encryption_level_t level, ngx_msec_t send_time);
+static ngx_inline ngx_msec_t ngx_quic_pto(ngx_connection_t *c,
+    ngx_quic_send_ctx_t *ctx);
+static void ngx_quic_handle_stream_ack(ngx_connection_t *c,
+    ngx_quic_frame_t *f);
+
+static ngx_int_t ngx_quic_handle_ordered_frame(ngx_connection_t *c,
+    ngx_quic_frames_stream_t *fs, ngx_quic_frame_t *frame,
+    ngx_quic_frame_handler_pt handler, void *data);
+static ngx_int_t ngx_quic_adjust_frame_offset(ngx_connection_t *c,
+    ngx_quic_frame_t *f, uint64_t offset_in);
+static ngx_int_t ngx_quic_buffer_frame(ngx_connection_t *c,
+    ngx_quic_frames_stream_t *stream, ngx_quic_frame_t *f);
+
+static ngx_int_t ngx_quic_handle_crypto_frame(ngx_connection_t *c,
+    ngx_quic_header_t *pkt, ngx_quic_frame_t *frame);
+static ngx_int_t ngx_quic_crypto_input(ngx_connection_t *c,
+    ngx_quic_frame_t *frame, void *data);
+static ngx_int_t ngx_quic_handle_stream_frame(ngx_connection_t *c,
+    ngx_quic_header_t *pkt, ngx_quic_frame_t *frame);
+static ngx_int_t ngx_quic_stream_input(ngx_connection_t *c,
+    ngx_quic_frame_t *frame, void *data);
+
+static ngx_int_t ngx_quic_handle_max_data_frame(ngx_connection_t *c,
+    ngx_quic_max_data_frame_t *f);
+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 ngx_int_t ngx_quic_handle_max_stream_data_frame(ngx_connection_t *c,
+    ngx_quic_header_t *pkt, ngx_quic_max_stream_data_frame_t *f);
+static ngx_int_t ngx_quic_handle_reset_stream_frame(ngx_connection_t *c,
+    ngx_quic_header_t *pkt, ngx_quic_reset_stream_frame_t *f);
+static ngx_int_t ngx_quic_handle_stop_sending_frame(ngx_connection_t *c,
+    ngx_quic_header_t *pkt, ngx_quic_stop_sending_frame_t *f);
+static ngx_int_t ngx_quic_handle_max_streams_frame(ngx_connection_t *c,
+    ngx_quic_header_t *pkt, ngx_quic_max_streams_frame_t *f);
+static ngx_int_t ngx_quic_handle_path_challenge_frame(ngx_connection_t *c,
+    ngx_quic_header_t *pkt, ngx_quic_path_challenge_frame_t *f);
+static ngx_int_t ngx_quic_handle_new_connection_id_frame(ngx_connection_t *c,
+    ngx_quic_header_t *pkt, ngx_quic_new_conn_id_frame_t *f);
+static ngx_int_t ngx_quic_retire_connection_id(ngx_connection_t *c,
+    enum ssl_encryption_level_t level, uint64_t seqnum);
+static ngx_int_t ngx_quic_handle_retire_connection_id_frame(ngx_connection_t *c,
+    ngx_quic_header_t *pkt, ngx_quic_retire_cid_frame_t *f);
+static ngx_int_t ngx_quic_issue_server_ids(ngx_connection_t *c);
+static void ngx_quic_clear_temp_server_ids(ngx_connection_t *c);
+static ngx_quic_server_id_t *ngx_quic_insert_server_id(ngx_connection_t *c,
+    ngx_str_t *id);
+static ngx_quic_client_id_t *ngx_quic_alloc_client_id(ngx_connection_t *c,
+    ngx_quic_connection_t *qc);
+static ngx_quic_server_id_t *ngx_quic_alloc_server_id(ngx_connection_t *c,
+    ngx_quic_connection_t *qc);
+
+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_frames(ngx_connection_t *c,
+    ngx_quic_send_ctx_t *ctx);
+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_quic_send_ctx_t *ctx, ngx_queue_t *frames);
+
+static void ngx_quic_set_packet_number(ngx_quic_header_t *pkt,
+    ngx_quic_send_ctx_t *ctx);
+static void ngx_quic_pto_handler(ngx_event_t *ev);
+static void ngx_quic_lost_handler(ngx_event_t *ev);
+static ngx_int_t ngx_quic_detect_lost(ngx_connection_t *c);
+static void ngx_quic_resend_frames(ngx_connection_t *c,
+    ngx_quic_send_ctx_t *ctx);
+static void ngx_quic_push_handler(ngx_event_t *ev);
+
+static void ngx_quic_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_client_stream(ngx_connection_t *c,
+    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 ngx_chain_t *ngx_quic_stream_send_chain(ngx_connection_t *c,
+    ngx_chain_t *in, off_t limit);
+static size_t ngx_quic_max_stream_frame(ngx_quic_connection_t *qc);
+static size_t ngx_quic_max_stream_flow(ngx_connection_t *c);
+static void ngx_quic_stream_cleanup_handler(void *data);
+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 void ngx_quic_congestion_ack(ngx_connection_t *c,
+    ngx_quic_frame_t *frame);
+static void ngx_quic_congestion_lost(ngx_connection_t *c,
+    ngx_quic_frame_t *frame);
+
+
+static 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 (NGX_DEBUG)
+
+static void
+ngx_quic_log_frame(ngx_log_t *log, ngx_quic_frame_t *f, ngx_uint_t tx)
+{
+    u_char      *p, *last, *pos, *end;
+    ssize_t      n;
+    uint64_t     gap, range, largest, smallest;
+    ngx_uint_t   i;
+    u_char       buf[NGX_MAX_ERROR_STR];
+
+    p = buf;
+    last = buf + sizeof(buf);
+
+    switch (f->type) {
+
+    case NGX_QUIC_FT_CRYPTO:
+        p = ngx_slprintf(p, last, "CRYPTO len:%uL off:%uL",
+                         f->u.crypto.length, f->u.crypto.offset);
+        break;
+
+    case NGX_QUIC_FT_PADDING:
+        p = ngx_slprintf(p, last, "PADDING");
+        break;
+
+    case NGX_QUIC_FT_ACK:
+    case NGX_QUIC_FT_ACK_ECN:
+
+        p = ngx_slprintf(p, last, "ACK n:%ui delay:%uL ",
+                         f->u.ack.range_count, f->u.ack.delay);
+
+        pos = f->u.ack.ranges_start;
+        end = f->u.ack.ranges_end;
+
+        largest = f->u.ack.largest;
+        smallest = f->u.ack.largest - f->u.ack.first_range;
+
+        if (largest == smallest) {
+            p = ngx_slprintf(p, last, "%uL", largest);
+
+        } else {
+            p = ngx_slprintf(p, last, "%uL-%uL", largest, smallest);
+        }
+
+        for (i = 0; i < f->u.ack.range_count; i++) {
+            n = ngx_quic_parse_ack_range(log, pos, end, &gap, &range);
+            if (n == NGX_ERROR) {
+                break;
+            }
+
+            pos += n;
+
+            largest = smallest - gap - 2;
+            smallest = largest - range;
+
+            if (largest == smallest) {
+                p = ngx_slprintf(p, last, " %uL", largest);
+
+            } else {
+                p = ngx_slprintf(p, last, " %uL-%uL", largest, smallest);
+            }
+        }
+
+        if (f->type == NGX_QUIC_FT_ACK_ECN) {
+            p = ngx_slprintf(p, last, " ECN counters ect0:%uL ect1:%uL ce:%uL",
+                             f->u.ack.ect0, f->u.ack.ect1, f->u.ack.ce);
+        }
+        break;
+
+    case NGX_QUIC_FT_PING:
+        p = ngx_slprintf(p, last, "PING");
+        break;
+
+    case NGX_QUIC_FT_NEW_CONNECTION_ID:
+        p = ngx_slprintf(p, last,
+                         "NEW_CONNECTION_ID seq:%uL retire:%uL len:%ud",
+                         f->u.ncid.seqnum, f->u.ncid.retire, f->u.ncid.len);
+        break;
+
+    case NGX_QUIC_FT_RETIRE_CONNECTION_ID:
+        p = ngx_slprintf(p, last, "RETIRE_CONNECTION_ID seqnum:%uL",
+                         f->u.retire_cid.sequence_number);
+        break;
+
+    case NGX_QUIC_FT_CONNECTION_CLOSE:
+    case NGX_QUIC_FT_CONNECTION_CLOSE_APP:
+        p = ngx_slprintf(p, last, "CONNECTION_CLOSE%s err:%ui",
+                         f->u.close.app ? "_APP" : "", f->u.close.error_code);
+
+        if (f->u.close.reason.len) {
+            p = ngx_slprintf(p, last, " %V", &f->u.close.reason);
+        }
+
+        if (f->type == NGX_QUIC_FT_CONNECTION_CLOSE) {
+            p = ngx_slprintf(p, last, " ft:%ui", f->u.close.frame_type);
+        }
+
+
+        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:
+
+        p = ngx_slprintf(p, last, "STREAM id:0x%xL", f->u.stream.stream_id);
+
+        if (f->u.stream.off) {
+            p = ngx_slprintf(p, last, " off:%uL", f->u.stream.offset);
+        }
+
+        if (f->u.stream.len) {
+            p = ngx_slprintf(p, last, " len:%uL", f->u.stream.length);
+        }
+
+        if (f->u.stream.fin) {
+            p = ngx_slprintf(p, last, " fin:1");
+        }
+
+        break;
+
+    case NGX_QUIC_FT_MAX_DATA:
+        p = ngx_slprintf(p, last, "MAX_DATA max_data:%uL on recv",
+                         f->u.max_data.max_data);
+        break;
+
+    case NGX_QUIC_FT_RESET_STREAM:
+       p = ngx_slprintf(p, last, "RESET_STREAM"
+                        " id:0x%xL error_code:0x%xL final_size:0x%xL",
+                        f->u.reset_stream.id, f->u.reset_stream.error_code,
+                        f->u.reset_stream.final_size);
+        break;
+
+    case NGX_QUIC_FT_STOP_SENDING:
+        p = ngx_slprintf(p, last, "STOP_SENDING id:0x%xL err:0x%xL",
+                         f->u.stop_sending.id, f->u.stop_sending.error_code);
+        break;
+
+    case NGX_QUIC_FT_STREAMS_BLOCKED:
+    case NGX_QUIC_FT_STREAMS_BLOCKED2:
+        p = ngx_slprintf(p, last, "STREAMS_BLOCKED limit:%uL bidi:%ui",
+                         f->u.streams_blocked.limit, f->u.streams_blocked.bidi);
+        break;
+
+    case NGX_QUIC_FT_MAX_STREAMS:
+    case NGX_QUIC_FT_MAX_STREAMS2:
+        p = ngx_slprintf(p, last, "MAX_STREAMS limit:%uL bidi:%ui",
+                         f->u.max_streams.limit, f->u.max_streams.bidi);
+        break;
+
+    case NGX_QUIC_FT_MAX_STREAM_DATA:
+        p = ngx_slprintf(p, last, "MAX_STREAM_DATA id:0x%xL limit:%uL",
+                         f->u.max_stream_data.id, f->u.max_stream_data.limit);
+        break;
+
+
+    case NGX_QUIC_FT_DATA_BLOCKED:
+        p = ngx_slprintf(p, last, "DATA_BLOCKED limit:%uL",
+                         f->u.data_blocked.limit);
+        break;
+
+    case NGX_QUIC_FT_STREAM_DATA_BLOCKED:
+        p = ngx_slprintf(p, last, "STREAM_DATA_BLOCKED id:0x%xL limit:%uL",
+                         f->u.stream_data_blocked.id,
+                         f->u.stream_data_blocked.limit);
+        break;
+
+    case NGX_QUIC_FT_PATH_CHALLENGE:
+        p = ngx_slprintf(p, last, "PATH_CHALLENGE data:0x%xL",
+                         *(uint64_t *) &f->u.path_challenge.data);
+        break;
+
+    case NGX_QUIC_FT_PATH_RESPONSE:
+        p = ngx_slprintf(p, last, "PATH_RESPONSE data:0x%xL",
+                         f->u.path_response);
+        break;
+
+    case NGX_QUIC_FT_NEW_TOKEN:
+        p = ngx_slprintf(p, last, "NEW_TOKEN");
+        break;
+
+    case NGX_QUIC_FT_HANDSHAKE_DONE:
+        p = ngx_slprintf(p, last, "HANDSHAKE DONE");
+        break;
+
+    default:
+        p = ngx_slprintf(p, last, "unknown type 0x%xi", f->type);
+        break;
+    }
+
+    ngx_log_debug4(NGX_LOG_DEBUG_EVENT, log, 0, "quic frame %s %s %*s",
+                   tx ? "tx" : "rx", ngx_quic_level_name(f->level),
+                   p - buf, buf);
+}
+
+
+static void
+ngx_quic_connstate_dbg(ngx_connection_t *c)
+{
+    u_char                 *p, *last;
+    ngx_quic_connection_t  *qc;
+    u_char                  buf[NGX_MAX_ERROR_STR];
+
+    p = buf;
+    last = p + sizeof(buf);
+
+    qc = ngx_quic_get_connection(c);
+
+    p = ngx_slprintf(p, last, "state:");
+
+    if (qc) {
+
+        if (qc->error) {
+            p = ngx_slprintf(p, last, "%s", qc->error_app ? " app" : "");
+            p = ngx_slprintf(p, last, " error:%ui", qc->error);
+
+            if (qc->error_reason) {
+                p = ngx_slprintf(p, last, " \"%s\"", qc->error_reason);
+            }
+        }
+
+        p = ngx_slprintf(p, last, "%s", qc->closing ? " closing" : "");
+        p = ngx_slprintf(p, last, "%s", qc->draining ? " draining" : "");
+        p = ngx_slprintf(p, last, "%s", qc->key_phase ? " kp" : "");
+        p = ngx_slprintf(p, last, "%s", qc->in_retry ? " retry" : "");
+        p = ngx_slprintf(p, last, "%s", qc->validated? " valid" : "");
+
+    } else {
+        p = ngx_slprintf(p, last, " early");
+    }
+
+    if (c->read->timer_set) {
+        p = ngx_slprintf(p, last,
+                         qc && qc->send_timer_set ? " send:%M" : " read:%M",
+                         c->read->timer.key - ngx_current_msec);
+    }
+
+    if (qc) {
+
+        if (qc->push.timer_set) {
+            p = ngx_slprintf(p, last, " push:%M",
+                             qc->push.timer.key - ngx_current_msec);
+        }
+
+        if (qc->pto.timer_set) {
+            p = ngx_slprintf(p, last, " pto:%M",
+                             qc->pto.timer.key - ngx_current_msec);
+        }
+
+        if (qc->close.timer_set) {
+            p = ngx_slprintf(p, last, " close:%M",
+                             qc->close.timer.key - ngx_current_msec);
+        }
+    }
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic %*s", p - buf, buf);
+}
+
+#else
+
+#define ngx_quic_log_frame(log, f, tx)
+#define ngx_quic_connstate_dbg(c)
+
+#endif
+
+
+#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_connection_t  *qc;
+
+    c = ngx_ssl_get_connection((ngx_ssl_conn_t *) ssl_conn);
+    qc = ngx_quic_get_connection(c);
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic ngx_quic_set_read_secret() level:%d", level);
+#ifdef NGX_QUIC_DEBUG_CRYPTO
+    ngx_quic_hexdump(c->log, "quic read secret", rsecret, secret_len);
+#endif
+
+    return ngx_quic_keys_set_encryption_secret(c->pool, 0, qc->keys, level,
+                                               cipher, rsecret, 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 *wsecret, size_t secret_len)
+{
+    ngx_connection_t       *c;
+    ngx_quic_connection_t  *qc;
+
+    c = ngx_ssl_get_connection((ngx_ssl_conn_t *) ssl_conn);
+    qc = ngx_quic_get_connection(c);
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic ngx_quic_set_write_secret() level:%d", level);
+#ifdef NGX_QUIC_DEBUG_CRYPTO
+    ngx_quic_hexdump(c->log, "quic write secret", wsecret, secret_len);
+#endif
+
+    return ngx_quic_keys_set_encryption_secret(c->pool, 1, qc->keys, level,
+                                               cipher, wsecret, 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 *rsecret,
+    const uint8_t *wsecret, size_t secret_len)
+{
+    ngx_connection_t       *c;
+    const SSL_CIPHER       *cipher;
+    ngx_quic_connection_t  *qc;
+
+    c = ngx_ssl_get_connection((ngx_ssl_conn_t *) ssl_conn);
+    qc = ngx_quic_get_connection(c);
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic ngx_quic_set_encryption_secrets() level:%d", level);
+#ifdef NGX_QUIC_DEBUG_CRYPTO
+    ngx_quic_hexdump(c->log, "quic read secret", rsecret, secret_len);
+#endif
+
+    cipher = SSL_get_current_cipher(ssl_conn);
+
+    if (ngx_quic_keys_set_encryption_secret(c->pool, 0, qc->keys, level,
+                                            cipher, rsecret, secret_len)
+        != 1)
+    {
+        return 0;
+    }
+
+    if (level == ssl_encryption_early_data) {
+        return 1;
+    }
+
+#ifdef NGX_QUIC_DEBUG_CRYPTO
+    ngx_quic_hexdump(c->log, "quic write secret", wsecret, secret_len);
+#endif
+
+    return ngx_quic_keys_set_encryption_secret(c->pool, 1, qc->keys, level,
+                                               cipher, wsecret, 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)
+{
+    u_char                    *p, *end;
+    size_t                     client_params_len, fsize, limit;
+    const uint8_t             *client_params;
+    ngx_quic_frame_t          *frame;
+    ngx_connection_t          *c;
+    ngx_quic_connection_t     *qc;
+    ngx_quic_frames_stream_t  *fs;
+
+    c = ngx_ssl_get_connection((ngx_ssl_conn_t *) ssl_conn);
+    qc = ngx_quic_get_connection(c);
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic ngx_quic_add_handshake_data");
+
+    if (!qc->client_tp_done) {
+        /*
+         * things to do once during handshake: check ALPN and transport
+         * parameters; we want to break handshake if something is wrong
+         * here;
+         */
+
+#if defined(TLSEXT_TYPE_application_layer_protocol_negotiation)
+        if (qc->conf->require_alpn) {
+            unsigned int          len;
+            const unsigned char  *data;
+
+            SSL_get0_alpn_selected(ssl_conn, &data, &len);
+
+            if (len == 0) {
+                qc->error = 0x100 + SSL_AD_NO_APPLICATION_PROTOCOL;
+                qc->error_reason = "unsupported protocol in ALPN extension";
+
+                ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                              "quic unsupported protocol in ALPN extension");
+                return 0;
+            }
+        }
+#endif
+
+        SSL_get_peer_quic_transport_params(ssl_conn, &client_params,
+                                           &client_params_len);
+
+        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic SSL_get_peer_quic_transport_params():"
+                       " params_len:%ui", client_params_len);
+
+        if (client_params_len == 0) {
+            /* quic-tls 8.2 */
+            qc->error = NGX_QUIC_ERR_CRYPTO(SSL_AD_MISSING_EXTENSION);
+            qc->error_reason = "missing transport parameters";
+
+            ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                          "missing transport parameters");
+            return 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)
+        {
+            qc->error = NGX_QUIC_ERR_TRANSPORT_PARAMETER_ERROR;
+            qc->error_reason = "failed to process transport parameters";
+
+            return 0;
+        }
+
+        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;
+        }
+
+        if (qc->ctp.max_udp_payload_size < NGX_QUIC_MIN_INITIAL_SIZE
+            || qc->ctp.max_udp_payload_size > NGX_QUIC_MAX_UDP_PAYLOAD_SIZE)
+        {
+            qc->error = NGX_QUIC_ERR_TRANSPORT_PARAMETER_ERROR;
+            qc->error_reason = "invalid maximum packet size";
+
+            ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                          "quic maximum packet size is invalid");
+            return 0;
+        }
+
+        if (qc->ctp.max_udp_payload_size > ngx_quic_max_udp_payload(c)) {
+            qc->ctp.max_udp_payload_size = ngx_quic_max_udp_payload(c);
+            ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                          "quic client maximum packet size truncated");
+        }
+
+#if (NGX_QUIC_DRAFT_VERSION >= 28)
+        if (qc->scid.len != qc->ctp.initial_scid.len
+            || ngx_memcmp(qc->scid.data, qc->ctp.initial_scid.data,
+                          qc->scid.len) != 0)
+        {
+            ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                          "quic client initial_source_connection_id "
+                          "mismatch");
+            return 0;
+        }
+#endif
+
+        qc->streams.server_max_streams_bidi = qc->ctp.initial_max_streams_bidi;
+        qc->streams.server_max_streams_uni = qc->ctp.initial_max_streams_uni;
+
+        qc->client_tp_done = 1;
+    }
+
+    /*
+     * we need to fit at least 1 frame into a packet, thus account head/tail;
+     * 17 = 1 + 8x2 is max header for CRYPTO frame, with 1 byte for frame type
+     */
+    limit = qc->ctp.max_udp_payload_size - NGX_QUIC_MAX_LONG_HEADER - 17
+            - EVP_GCM_TLS_TAG_LEN;
+
+    fs = &qc->crypto[level];
+
+    p = (u_char *) data;
+    end = (u_char *) data + len;
+
+    while (p < end) {
+
+        fsize = ngx_min(limit, (size_t) (end - p));
+
+        frame = ngx_quic_alloc_frame(c, fsize);
+        if (frame == NULL) {
+            return 0;
+        }
+
+        ngx_memcpy(frame->data, p, fsize);
+
+        frame->level = level;
+        frame->type = NGX_QUIC_FT_CRYPTO;
+        frame->u.crypto.offset = fs->sent;
+        frame->u.crypto.length = fsize;
+        frame->u.crypto.data = frame->data;
+
+        fs->sent += fsize;
+        p += fsize;
+
+        ngx_quic_queue_frame(qc, frame);
+    }
+
+    return 1;
+}
+
+
+static int
+ngx_quic_flush_flight(ngx_ssl_conn_t *ssl_conn)
+{
+#if (NGX_DEBUG)
+    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,
+                   "quic ngx_quic_flush_flight()");
+#endif
+    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_connection_t  *qc;
+
+    c = ngx_ssl_get_connection((ngx_ssl_conn_t *) ssl_conn);
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic ngx_quic_send_alert() lvl:%d  alert:%d",
+                   (int) level, (int) alert);
+
+    qc = ngx_quic_get_connection(c);
+    if (qc == NULL) {
+        return 1;
+    }
+
+    qc->error_level = level;
+    qc->error = NGX_QUIC_ERR_CRYPTO(alert);
+    qc->error_reason = "TLS alert";
+    qc->error_app = 0;
+    qc->error_ftype = 0;
+
+    if (ngx_quic_send_cc(c) != NGX_OK) {
+        return 0;
+    }
+
+    return 1;
+}
+
+
+void
+ngx_quic_run(ngx_connection_t *c, ngx_quic_conf_t *conf)
+{
+    ngx_int_t               rc;
+    ngx_quic_connection_t  *qc;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, "quic run");
+
+    rc = ngx_quic_input(c, c->buffer, conf);
+    if (rc != NGX_OK) {
+        ngx_quic_close_connection(c, rc == NGX_DECLINED ? NGX_DONE : NGX_ERROR);
+        return;
+    }
+
+    qc = ngx_quic_get_connection(c);
+
+    ngx_add_timer(c->read, qc->in_retry ? NGX_QUIC_RETRY_TIMEOUT
+                                        : qc->tp.max_idle_timeout);
+
+    c->read->handler = ngx_quic_input_handler;
+
+    ngx_quic_connstate_dbg(c);
+    return;
+}
+
+
+static ngx_quic_connection_t *
+ngx_quic_new_connection(ngx_connection_t *c, ngx_quic_conf_t *conf,
+    ngx_quic_header_t *pkt)
+{
+    ngx_uint_t              i;
+    ngx_quic_tp_t          *ctp;
+    ngx_quic_client_id_t   *cid;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_pcalloc(c->pool, sizeof(ngx_quic_connection_t));
+    if (qc == NULL) {
+        return NULL;
+    }
+
+    qc->keys = ngx_quic_keys_new(c->pool);
+    if (qc->keys == NULL) {
+        return NULL;
+    }
+
+    qc->version = pkt->version;
+
+    ngx_rbtree_init(&qc->streams.tree, &qc->streams.sentinel,
+                    ngx_quic_rbtree_insert_stream);
+
+    for (i = 0; i < NGX_QUIC_SEND_CTX_LAST; i++) {
+        ngx_queue_init(&qc->send_ctx[i].frames);
+        ngx_queue_init(&qc->send_ctx[i].sent);
+        qc->send_ctx[i].largest_pn = NGX_QUIC_UNSET_PN;
+        qc->send_ctx[i].largest_ack = NGX_QUIC_UNSET_PN;
+        qc->send_ctx[i].largest_range = NGX_QUIC_UNSET_PN;
+        qc->send_ctx[i].pending_ack = NGX_QUIC_UNSET_PN;
+    }
+
+    qc->send_ctx[0].level = ssl_encryption_initial;
+    qc->send_ctx[1].level = ssl_encryption_handshake;
+    qc->send_ctx[2].level = ssl_encryption_application;
+
+    for (i = 0; i < NGX_QUIC_ENCRYPTION_LAST; i++) {
+        ngx_queue_init(&qc->crypto[i].frames);
+    }
+
+    ngx_queue_init(&qc->free_frames);
+    ngx_queue_init(&qc->client_ids);
+    ngx_queue_init(&qc->server_ids);
+    ngx_queue_init(&qc->free_client_ids);
+    ngx_queue_init(&qc->free_server_ids);
+
+    qc->avg_rtt = NGX_QUIC_INITIAL_RTT;
+    qc->rttvar = NGX_QUIC_INITIAL_RTT / 2;
+    qc->min_rtt = NGX_TIMER_INFINITE;
+
+    /*
+     * qc->latest_rtt = 0
+     * qc->nclient_ids = 0
+     * qc->nserver_ids = 0
+     * qc->max_retired_seqnum = 0
+     */
+
+    qc->received = pkt->raw->last - pkt->raw->start;
+
+    qc->pto.log = c->log;
+    qc->pto.data = c;
+    qc->pto.handler = ngx_quic_pto_handler;
+    qc->pto.cancelable = 1;
+
+    qc->push.log = c->log;
+    qc->push.data = c;
+    qc->push.handler = ngx_quic_push_handler;
+    qc->push.cancelable = 1;
+
+    qc->conf = conf;
+    qc->tp = conf->tp;
+
+    if (qc->tp.disable_active_migration) {
+        qc->sockaddr = ngx_palloc(c->pool, c->socklen);
+        if (qc->sockaddr == NULL) {
+            return NULL;
+        }
+
+        ngx_memcpy(qc->sockaddr, c->sockaddr, c->socklen);
+        qc->socklen = c->socklen;
+    }
+
+    ctp = &qc->ctp;
+    ctp->max_udp_payload_size = ngx_quic_max_udp_payload(c);
+    ctp->ack_delay_exponent = NGX_QUIC_DEFAULT_ACK_DELAY_EXPONENT;
+    ctp->max_ack_delay = NGX_QUIC_DEFAULT_MAX_ACK_DELAY;
+
+    qc->streams.recv_max_data = qc->tp.initial_max_data;
+
+    qc->streams.client_max_streams_uni = qc->tp.initial_max_streams_uni;
+    qc->streams.client_max_streams_bidi = qc->tp.initial_max_streams_bidi;
+
+    qc->congestion.window = ngx_min(10 * qc->tp.max_udp_payload_size,
+                                    ngx_max(2 * qc->tp.max_udp_payload_size,
+                                            14720));
+    qc->congestion.ssthresh = (size_t) -1;
+    qc->congestion.recovery_start = ngx_current_msec;
+
+    qc->odcid.len = pkt->dcid.len;
+    qc->odcid.data = ngx_pstrdup(c->pool, &pkt->dcid);
+    if (qc->odcid.data == NULL) {
+        return NULL;
+    }
+
+    qc->dcid.len = NGX_QUIC_SERVER_CID_LEN;
+    qc->dcid.data = ngx_pnalloc(c->pool, qc->dcid.len);
+    if (qc->dcid.data == NULL) {
+        return NULL;
+    }
+
+    if (ngx_quic_create_server_id(c, qc->dcid.data) != NGX_OK) {
+        return NULL;
+    }
+
+#if (NGX_QUIC_DRAFT_VERSION >= 28)
+    qc->tp.original_dcid = qc->odcid;
+#endif
+    qc->tp.initial_scid = qc->dcid;
+
+    qc->scid.len = pkt->scid.len;
+    qc->scid.data = ngx_pnalloc(c->pool, qc->scid.len);
+    if (qc->scid.data == NULL) {
+        return NULL;
+    }
+    ngx_memcpy(qc->scid.data, pkt->scid.data, qc->scid.len);
+
+    cid = ngx_quic_alloc_client_id(c, qc);
+    if (cid == NULL) {
+        return NULL;
+    }
+
+    cid->seqnum = 0;
+    cid->len = pkt->scid.len;
+    ngx_memcpy(cid->id, pkt->scid.data, pkt->scid.len);
+
+    ngx_queue_insert_tail(&qc->client_ids, &cid->queue);
+    qc->nclient_ids++;
+    qc->client_seqnum = 0;
+
+    qc->server_seqnum = NGX_QUIC_UNSET_PN;
+
+    return qc;
+}
+
+
+static ngx_int_t
+ngx_quic_send_stateless_reset(ngx_connection_t *c, ngx_quic_conf_t *conf,
+    ngx_quic_header_t *pkt)
+{
+    u_char    *token;
+    size_t     len, max;
+    uint16_t   rndbytes;
+    u_char     buf[NGX_QUIC_MAX_SR_PACKET];
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic handle stateless reset output");
+
+    if (pkt->len <= NGX_QUIC_MIN_PKT_LEN) {
+        return NGX_DECLINED;
+    }
+
+    if (pkt->len <= NGX_QUIC_MIN_SR_PACKET) {
+        len = pkt->len - 1;
+
+    } else {
+        max = ngx_min(NGX_QUIC_MAX_SR_PACKET, pkt->len * 3);
+
+        if (RAND_bytes((u_char *) &rndbytes, sizeof(rndbytes)) != 1) {
+            return NGX_ERROR;
+        }
+
+        len = (rndbytes % (max - NGX_QUIC_MIN_SR_PACKET + 1))
+              + NGX_QUIC_MIN_SR_PACKET;
+    }
+
+    if (RAND_bytes(buf, len - NGX_QUIC_SR_TOKEN_LEN) != 1) {
+        return NGX_ERROR;
+    }
+
+    buf[0] &= ~NGX_QUIC_PKT_LONG;
+    buf[0] |= NGX_QUIC_PKT_FIXED_BIT;
+
+    token = &buf[len - NGX_QUIC_SR_TOKEN_LEN];
+
+    if (ngx_quic_new_sr_token(c, &pkt->dcid, &conf->sr_token_key, token)
+        != NGX_OK)
+    {
+        return NGX_ERROR;
+    }
+
+    (void) c->send(c, buf, len);
+
+    return NGX_DECLINED;
+}
+
+
+static ngx_int_t
+ngx_quic_process_stateless_reset(ngx_connection_t *c, ngx_quic_header_t *pkt)
+{
+    u_char                 *tail, ch;
+    ngx_uint_t              i;
+    ngx_queue_t            *q;
+    ngx_quic_client_id_t   *cid;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    /* A stateless reset uses an entire UDP datagram */
+    if (pkt->raw->start != pkt->data) {
+        return NGX_DECLINED;
+    }
+
+    tail = pkt->raw->last - NGX_QUIC_SR_TOKEN_LEN;
+
+    for (q = ngx_queue_head(&qc->client_ids);
+         q != ngx_queue_sentinel(&qc->client_ids);
+         q = ngx_queue_next(q))
+    {
+        cid = ngx_queue_data(q, ngx_quic_client_id_t, queue);
+
+        if (cid->seqnum == 0) {
+            /* no stateless reset token in initial connection id */
+            continue;
+        }
+
+        /* constant time comparison */
+
+        for (ch = 0, i = 0; i < NGX_QUIC_SR_TOKEN_LEN; i++) {
+            ch |= tail[i] ^ cid->sr_token[i];
+        }
+
+        if (ch == 0) {
+            return NGX_OK;
+        }
+    }
+
+    return NGX_DECLINED;
+}
+
+
+static ngx_int_t
+ngx_quic_negotiate_version(ngx_connection_t *c, ngx_quic_header_t *inpkt)
+{
+    size_t             len;
+    ngx_quic_header_t  pkt;
+    static u_char      buf[NGX_QUIC_MAX_UDP_PAYLOAD_SIZE];
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "sending version negotiation packet");
+
+    pkt.log = c->log;
+    pkt.flags = NGX_QUIC_PKT_LONG | NGX_QUIC_PKT_FIXED_BIT;
+    pkt.dcid = inpkt->scid;
+    pkt.scid = inpkt->dcid;
+
+    len = ngx_quic_create_version_negotiation(&pkt, buf);
+
+#ifdef NGX_QUIC_DEBUG_PACKETS
+    ngx_quic_hexdump(c->log, "quic vnego packet to send", buf, len);
+#endif
+
+    (void) c->send(c, buf, len);
+
+    return NGX_ERROR;
+}
+
+
+static ngx_int_t
+ngx_quic_create_server_id(ngx_connection_t *c, u_char *id)
+{
+    if (RAND_bytes(id, NGX_QUIC_SERVER_CID_LEN) != 1) {
+        return NGX_ERROR;
+    }
+
+    ngx_quic_hexdump(c->log, "quic create server id",
+                     id, NGX_QUIC_SERVER_CID_LEN);
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_send_retry(ngx_connection_t *c)
+{
+    ssize_t                 len;
+    ngx_str_t               res, token;
+    ngx_quic_header_t       pkt;
+    ngx_quic_connection_t  *qc;
+    u_char                  buf[NGX_QUIC_RETRY_BUFFER_SIZE];
+
+    qc = ngx_quic_get_connection(c);
+
+    if (ngx_quic_new_token(c, &token) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    ngx_memzero(&pkt, sizeof(ngx_quic_header_t));
+    pkt.flags = NGX_QUIC_PKT_FIXED_BIT | NGX_QUIC_PKT_LONG | NGX_QUIC_PKT_RETRY;
+    pkt.version = qc->version;
+    pkt.log = c->log;
+    pkt.odcid = qc->odcid;
+    pkt.dcid = qc->scid;
+    pkt.scid = qc->dcid;
+    pkt.token = token;
+
+    res.data = buf;
+
+    if (ngx_quic_encrypt(&pkt, &res) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+#ifdef NGX_QUIC_DEBUG_PACKETS
+    ngx_quic_hexdump(c->log, "quic packet to send", res.data, res.len);
+#endif
+
+    len = c->send(c, res.data, res.len);
+    if (len == NGX_ERROR || (size_t) len != res.len) {
+        return NGX_ERROR;
+    }
+
+    qc->token = token;
+#if (NGX_QUIC_DRAFT_VERSION < 28)
+    qc->tp.original_dcid = qc->odcid;
+#endif
+    qc->tp.retry_scid = qc->dcid;
+    qc->in_retry = 1;
+
+    if (ngx_quic_insert_server_id(c, &qc->dcid) == NULL) {
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_new_token(ngx_connection_t *c, ngx_str_t *token)
+{
+    int                     len, iv_len;
+    u_char                 *data, *p, *key, *iv;
+    ngx_msec_t              now;
+    EVP_CIPHER_CTX         *ctx;
+    const EVP_CIPHER       *cipher;
+    struct sockaddr_in     *sin;
+#if (NGX_HAVE_INET6)
+    struct sockaddr_in6    *sin6;
+#endif
+    ngx_quic_connection_t  *qc;
+    u_char                  in[NGX_QUIC_MAX_TOKEN_SIZE];
+
+    switch (c->sockaddr->sa_family) {
+
+#if (NGX_HAVE_INET6)
+    case AF_INET6:
+        sin6 = (struct sockaddr_in6 *) c->sockaddr;
+
+        len = sizeof(struct in6_addr);
+        data = sin6->sin6_addr.s6_addr;
+
+        break;
+#endif
+
+#if (NGX_HAVE_UNIX_DOMAIN)
+    case AF_UNIX:
+
+        len = ngx_min(c->addr_text.len, NGX_QUIC_MAX_TOKEN_SIZE - sizeof(now));
+        data = c->addr_text.data;
+
+        break;
+#endif
+
+    default: /* AF_INET */
+        sin = (struct sockaddr_in *) c->sockaddr;
+
+        len = sizeof(in_addr_t);
+        data = (u_char *) &sin->sin_addr;
+
+        break;
+    }
+
+    p = ngx_cpymem(in, data, len);
+
+    now = ngx_current_msec;
+    len += sizeof(now);
+    ngx_memcpy(p, &now, sizeof(now));
+
+    cipher = EVP_aes_256_cbc();
+    iv_len = EVP_CIPHER_iv_length(cipher);
+
+    token->len = iv_len + len + EVP_CIPHER_block_size(cipher);
+    token->data = ngx_pnalloc(c->pool, token->len);
+    if (token->data == NULL) {
+        return NGX_ERROR;
+    }
+
+    ctx = EVP_CIPHER_CTX_new();
+    if (ctx == NULL) {
+        return NGX_ERROR;
+    }
+
+    qc = ngx_quic_get_connection(c);
+    key = qc->conf->token_key;
+    iv = token->data;
+
+    if (RAND_bytes(iv, iv_len) <= 0
+        || !EVP_EncryptInit_ex(ctx, cipher, NULL, key, iv))
+    {
+        EVP_CIPHER_CTX_free(ctx);
+        return NGX_ERROR;
+    }
+
+    token->len = iv_len;
+
+    if (EVP_EncryptUpdate(ctx, token->data + token->len, &len, in, len) != 1) {
+        EVP_CIPHER_CTX_free(ctx);
+        return NGX_ERROR;
+    }
+
+    token->len += len;
+
+    if (EVP_EncryptFinal_ex(ctx, token->data + token->len, &len) <= 0) {
+        EVP_CIPHER_CTX_free(ctx);
+        return NGX_ERROR;
+    }
+
+    token->len += len;
+
+    EVP_CIPHER_CTX_free(ctx);
+
+#ifdef NGX_QUIC_DEBUG_PACKETS
+    ngx_quic_hexdump(c->log, "quic new token", token->data, token->len);
+#endif
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_validate_token(ngx_connection_t *c, ngx_quic_header_t *pkt)
+{
+    int                     len, tlen, iv_len;
+    u_char                 *key, *iv, *p, *data;
+    ngx_msec_t              msec;
+    EVP_CIPHER_CTX         *ctx;
+    const EVP_CIPHER       *cipher;
+    struct sockaddr_in     *sin;
+#if (NGX_HAVE_INET6)
+    struct sockaddr_in6    *sin6;
+#endif
+    ngx_quic_connection_t  *qc;
+    u_char                  tdec[NGX_QUIC_MAX_TOKEN_SIZE];
+
+    qc = ngx_quic_get_connection(c);
+
+    /* Retry token */
+
+    if (qc->token.len) {
+        if (pkt->token.len != qc->token.len) {
+            goto bad_token;
+        }
+
+        if (ngx_memcmp(pkt->token.data, qc->token.data, pkt->token.len) != 0) {
+            goto bad_token;
+        }
+
+        return NGX_OK;
+    }
+
+    /* NEW_TOKEN in a previous connection */
+
+    cipher = EVP_aes_256_cbc();
+    key = qc->conf->token_key;
+    iv = pkt->token.data;
+    iv_len = EVP_CIPHER_iv_length(cipher);
+
+    /* sanity checks */
+
+    if (pkt->token.len < (size_t) iv_len + EVP_CIPHER_block_size(cipher)) {
+        goto bad_token;
+    }
+
+    if (pkt->token.len > (size_t) iv_len + NGX_QUIC_MAX_TOKEN_SIZE) {
+        goto bad_token;
+    }
+
+    ctx = EVP_CIPHER_CTX_new();
+    if (ctx == NULL) {
+        return NGX_ERROR;
+    }
+
+    if (!EVP_DecryptInit_ex(ctx, cipher, NULL, key, iv)) {
+        EVP_CIPHER_CTX_free(ctx);
+        return NGX_ERROR;
+    }
+
+    p = pkt->token.data + iv_len;
+    len = pkt->token.len - iv_len;
+
+    if (EVP_DecryptUpdate(ctx, tdec, &len, p, len) != 1) {
+        EVP_CIPHER_CTX_free(ctx);
+        goto bad_token;
+    }
+
+    if (EVP_DecryptFinal_ex(ctx, tdec + len, &tlen) <= 0) {
+        EVP_CIPHER_CTX_free(ctx);
+        goto bad_token;
+    }
+
+    EVP_CIPHER_CTX_free(ctx);
+
+    switch (c->sockaddr->sa_family) {
+
+#if (NGX_HAVE_INET6)
+    case AF_INET6:
+        sin6 = (struct sockaddr_in6 *) c->sockaddr;
+
+        len = sizeof(struct in6_addr);
+        data = sin6->sin6_addr.s6_addr;
+
+        break;
+#endif
+
+#if (NGX_HAVE_UNIX_DOMAIN)
+    case AF_UNIX:
+
+        len = ngx_min(c->addr_text.len, NGX_QUIC_MAX_TOKEN_SIZE - sizeof(msec));
+        data = c->addr_text.data;
+
+        break;
+#endif
+
+    default: /* AF_INET */
+        sin = (struct sockaddr_in *) c->sockaddr;
+
+        len = sizeof(in_addr_t);
+        data = (u_char *) &sin->sin_addr;
+
+        break;
+    }
+
+    if (ngx_memcmp(tdec, data, len) != 0) {
+        goto bad_token;
+    }
+
+    ngx_memcpy(&msec, tdec + len, sizeof(msec));
+
+    if (ngx_current_msec - msec > NGX_QUIC_RETRY_LIFETIME) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0, "quic expired token");
+        return NGX_DECLINED;
+    }
+
+    return NGX_OK;
+
+bad_token:
+
+    ngx_log_error(NGX_LOG_INFO, c->log, 0, "quic invalid token");
+
+    qc->error = NGX_QUIC_ERR_INVALID_TOKEN;
+    qc->error_reason = "invalid_token";
+
+    return NGX_DECLINED;
+}
+
+
+static ngx_int_t
+ngx_quic_init_connection(ngx_connection_t *c)
+{
+    u_char                 *p;
+    size_t                  clen;
+    ssize_t                 len;
+    ngx_ssl_conn_t         *ssl_conn;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    if (ngx_ssl_create_connection(qc->conf->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,
+                      "quic SSL_set_quic_method() failed");
+        return NGX_ERROR;
+    }
+
+#ifdef SSL_READ_EARLY_DATA_SUCCESS
+    if (SSL_CTX_get_max_early_data(qc->conf->ssl->ctx)) {
+        SSL_set_quic_early_data_enabled(ssl_conn, 1);
+    }
+#endif
+
+    if (ngx_quic_new_sr_token(c, &qc->dcid, &qc->conf->sr_token_key,
+                              qc->tp.sr_token)
+        != NGX_OK)
+    {
+        return NGX_ERROR;
+    }
+
+    ngx_quic_hexdump(c->log, "quic stateless reset token",
+                     qc->tp.sr_token, (size_t) NGX_QUIC_SR_TOKEN_LEN);
+
+    len = ngx_quic_create_transport_params(NULL, NULL, &qc->tp, &clen);
+    /* 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, NULL);
+    if (len < 0) {
+        return NGX_ERROR;
+    }
+
+#ifdef NGX_QUIC_DEBUG_PACKETS
+    ngx_quic_hexdump(c->log, "quic transport parameters", p, len);
+#endif
+
+    if (SSL_set_quic_transport_params(ssl_conn, p, len) == 0) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "quic SSL_set_quic_transport_params() failed");
+        return NGX_ERROR;
+    }
+
+#if NGX_OPENSSL_QUIC_ZRTT_CTX
+    if (SSL_set_quic_early_data_context(ssl_conn, p, clen) == 0) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "quic SSL_set_quic_early_data_context() failed");
+        return NGX_ERROR;
+    }
+#endif
+
+    return NGX_OK;
+}
+
+
+static ngx_inline size_t
+ngx_quic_max_udp_payload(ngx_connection_t *c)
+{
+    /* TODO: path MTU discovery */
+
+#if (NGX_HAVE_INET6)
+    if (c->sockaddr->sa_family == AF_INET6) {
+        return NGX_QUIC_MAX_UDP_PAYLOAD_OUT6;
+    }
+#endif
+
+    return NGX_QUIC_MAX_UDP_PAYLOAD_OUT;
+}
+
+
+static void
+ngx_quic_input_handler(ngx_event_t *rev)
+{
+    ssize_t                 n;
+    ngx_int_t               rc;
+    ngx_buf_t               b;
+    ngx_connection_t       *c;
+    ngx_quic_connection_t  *qc;
+    static u_char           buf[NGX_QUIC_MAX_UDP_PAYLOAD_SIZE];
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, rev->log, 0, "quic input handler");
+
+    ngx_memzero(&b, sizeof(ngx_buf_t));
+    b.start = buf;
+    b.end = buf + sizeof(buf);
+    b.pos = b.last = b.start;
+    b.memory = 1;
+
+    c = rev->data;
+    qc = ngx_quic_get_connection(c);
+
+    c->log->action = "handling quic input";
+
+    if (rev->timedout) {
+        ngx_log_error(NGX_LOG_INFO, c->log, NGX_ETIMEDOUT,
+                      "quic client timed out");
+        ngx_quic_close_connection(c, NGX_DONE);
+        return;
+    }
+
+    if (c->close) {
+        qc->error_reason = "graceful shutdown";
+        ngx_quic_close_connection(c, NGX_OK);
+        return;
+    }
+
+    n = c->recv(c, b.start, b.end - b.start);
+
+    if (n == NGX_AGAIN) {
+        if (qc->closing) {
+            ngx_quic_close_connection(c, NGX_OK);
+        }
+        return;
+    }
+
+    if (n == NGX_ERROR) {
+        c->read->eof = 1;
+        ngx_quic_close_connection(c, NGX_ERROR);
+        return;
+    }
+
+    if (qc->tp.disable_active_migration) {
+        if (c->socklen != qc->socklen
+            || ngx_memcmp(c->sockaddr, qc->sockaddr, c->socklen) != 0)
+        {
+            ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                           "quic dropping packet from new address");
+            return;
+        }
+    }
+
+    b.last += n;
+    qc->received += n;
+
+    rc = ngx_quic_input(c, &b, NULL);
+
+    if (rc == NGX_ERROR) {
+        ngx_quic_close_connection(c, NGX_ERROR);
+        return;
+    }
+
+    if (rc == NGX_DECLINED) {
+        return;
+    }
+
+    /* rc == NGX_OK */
+
+    qc->send_timer_set = 0;
+    ngx_add_timer(rev, qc->tp.max_idle_timeout);
+
+    ngx_quic_connstate_dbg(c);
+}
+
+
+static void
+ngx_quic_close_connection(ngx_connection_t *c, ngx_int_t rc)
+{
+    ngx_pool_t             *pool;
+    ngx_quic_connection_t  *qc;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic ngx_quic_close_connection rc:%i", rc);
+
+    qc = ngx_quic_get_connection(c);
+
+    if (qc == NULL) {
+        ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                      "quic close connection early error");
+
+    } else if (ngx_quic_close_quic(c, rc) == NGX_AGAIN) {
+        return;
+    }
+
+    if (c->ssl) {
+        (void) ngx_ssl_shutdown(c);
+    }
+
+    if (c->read->timer_set) {
+        ngx_del_timer(c->read);
+    }
+
+#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_close_quic(ngx_connection_t *c, ngx_int_t rc)
+{
+    ngx_uint_t              i;
+    ngx_queue_t            *q;
+    ngx_quic_send_ctx_t    *ctx;
+    ngx_quic_server_id_t   *sid;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    if (!qc->closing) {
+
+        /* drop packets from retransmit queues, no ack is expected */
+        for (i = 0; i < NGX_QUIC_SEND_CTX_LAST; i++) {
+            ctx = ngx_quic_get_send_ctx(qc, i);
+            ngx_quic_free_frames(c, &ctx->sent);
+        }
+
+        if (rc == NGX_DONE) {
+
+            /*
+             *  10.2.  Idle Timeout
+             *
+             *  If the idle timeout is enabled by either peer, a connection is
+             *  silently closed and its state is discarded when it remains idle
+             */
+
+            ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                           "quic closing %s connection",
+                           qc->draining ? "drained" : "idle");
+
+        } else {
+
+            /*
+             * 10.3.  Immediate Close
+             *
+             *  An endpoint sends a CONNECTION_CLOSE frame (Section 19.19)
+             *  to terminate the connection immediately.
+             */
+
+            qc->error_level = c->ssl ? SSL_quic_read_level(c->ssl->connection)
+                                     : ssl_encryption_initial;
+
+            if (rc == NGX_OK) {
+                 ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                                "quic immediate close drain:%d",
+                                qc->draining);
+
+                qc->close.log = c->log;
+                qc->close.data = c;
+                qc->close.handler = ngx_quic_close_timer_handler;
+                qc->close.cancelable = 1;
+
+                ctx = ngx_quic_get_send_ctx(qc, qc->error_level);
+
+                ngx_add_timer(&qc->close, 3 * ngx_quic_pto(c, ctx));
+
+                qc->error = NGX_QUIC_ERR_NO_ERROR;
+
+            } else {
+                if (qc->error == 0 && !qc->error_app) {
+                    qc->error = NGX_QUIC_ERR_INTERNAL_ERROR;
+                }
+
+                ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                               "quic immediate close due to %s error: %ui %s",
+                               qc->error_app ? "app " : "", qc->error,
+                               qc->error_reason ? qc->error_reason : "");
+            }
+
+            (void) ngx_quic_send_cc(c);
+
+            if (qc->error_level == ssl_encryption_handshake) {
+                /* for clients that might not have handshake keys */
+                qc->error_level = ssl_encryption_initial;
+                (void) ngx_quic_send_cc(c);
+            }
+        }
+
+        qc->closing = 1;
+    }
+
+    if (rc == NGX_ERROR && qc->close.timer_set) {
+        /* do not wait for timer in case of fatal error */
+        ngx_del_timer(&qc->close);
+    }
+
+    if (ngx_quic_close_streams(c, qc) == NGX_AGAIN) {
+        return NGX_AGAIN;
+    }
+
+    if (qc->push.timer_set) {
+        ngx_del_timer(&qc->push);
+    }
+
+    if (qc->pto.timer_set) {
+        ngx_del_timer(&qc->pto);
+    }
+
+    if (qc->push.posted) {
+        ngx_delete_posted_event(&qc->push);
+    }
+
+    for (i = 0; i < NGX_QUIC_ENCRYPTION_LAST; i++) {
+        ngx_quic_free_frames(c, &qc->crypto[i].frames);
+    }
+
+    for (i = 0; i < NGX_QUIC_SEND_CTX_LAST; i++) {
+        ngx_quic_free_frames(c, &qc->send_ctx[i].frames);
+        ngx_quic_free_frames(c, &qc->send_ctx[i].sent);
+    }
+
+    while (!ngx_queue_empty(&qc->server_ids)) {
+        q = ngx_queue_head(&qc->server_ids);
+        sid = ngx_queue_data(q, ngx_quic_server_id_t, queue);
+
+        ngx_queue_remove(q);
+        ngx_rbtree_delete(&c->listening->rbtree, &sid->udp.node);
+        qc->nserver_ids--;
+    }
+
+    if (qc->close.timer_set) {
+        return NGX_AGAIN;
+    }
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic part of connection is terminated");
+
+    /* may be tested from SSL callback during SSL shutdown */
+    c->udp = NULL;
+
+    return NGX_OK;
+}
+
+
+void
+ngx_quic_finalize_connection(ngx_connection_t *c, ngx_uint_t err,
+    const char *reason)
+{
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+    qc->error = err;
+    qc->error_reason = reason;
+    qc->error_app = 1;
+    qc->error_ftype = 0;
+
+    ngx_quic_close_connection(c, NGX_ERROR);
+}
+
+
+static void
+ngx_quic_close_timer_handler(ngx_event_t *ev)
+{
+    ngx_connection_t  *c;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, ev->log, 0, "quic close timer");
+
+    c = ev->data;
+    ngx_quic_close_connection(c, NGX_DONE);
+}
+
+
+static ngx_int_t
+ngx_quic_close_streams(ngx_connection_t *c, ngx_quic_connection_t *qc)
+{
+    ngx_event_t        *rev, *wev;
+    ngx_rbtree_t       *tree;
+    ngx_rbtree_node_t  *node;
+    ngx_quic_stream_t  *qs;
+
+#if (NGX_DEBUG)
+    ngx_uint_t          ns;
+#endif
+
+    tree = &qc->streams.tree;
+
+    if (tree->root == tree->sentinel) {
+        return NGX_OK;
+    }
+
+#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->error = 1;
+        rev->ready = 1;
+
+        wev = qs->c->write;
+        wev->error = 1;
+        wev->ready = 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 NGX_AGAIN;
+}
+
+
+static ngx_int_t
+ngx_quic_input(ngx_connection_t *c, ngx_buf_t *b, ngx_quic_conf_t *conf)
+{
+    u_char                 *p;
+    ngx_int_t               rc;
+    ngx_uint_t              good;
+    ngx_quic_header_t       pkt;
+    ngx_quic_connection_t  *qc;
+
+    good = 0;
+
+    p = b->pos;
+
+    while (p < b->last) {
+
+        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];
+        pkt.raw->pos++;
+
+        qc = ngx_quic_get_connection(c);
+        if (qc) {
+            qc->error = 0;
+            qc->error_reason = 0;
+        }
+
+        rc = ngx_quic_process_packet(c, conf, &pkt);
+
+#if (NGX_DEBUG)
+        if (pkt.parsed) {
+            ngx_log_debug5(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                           "quic packet %s done decr:%d pn:%L perr:%ui rc:%i",
+                           ngx_quic_level_name(pkt.level), pkt.decrypted,
+                           pkt.pn, pkt.error, rc);
+        } else {
+            ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                           "quic packet done parse failed rc:%i", rc);
+        }
+#endif
+
+        if (rc == NGX_ERROR) {
+            return NGX_ERROR;
+        }
+
+        if (rc == NGX_OK) {
+            good = 1;
+        }
+
+        /* NGX_OK || NGX_DECLINED */
+
+        /*
+         * we get NGX_DECLINED when there are no keys [yet] available
+         * to decrypt packet.
+         * Instead of queueing it, we ignore it and rely on the sender's
+         * retransmission:
+         *
+         * 12.2.  Coalescing Packets:
+         *
+         * For example, if decryption fails (because the keys are
+         * not available or any other reason), the receiver MAY either
+         * discard or buffer the packet for later processing and MUST
+         * attempt to process the remaining packets.
+         *
+         * We also skip packets that don't match connection state
+         * or cannot be parsed properly.
+         */
+
+        /* b->pos is at header end, adjust by actual packet length */
+        b->pos = pkt.data + pkt.len;
+
+        /* firefox workaround: skip zero padding at the end of quic packet */
+        while (b->pos < b->last && *(b->pos) == 0) {
+            b->pos++;
+        }
+
+        p = b->pos;
+    }
+
+    return good ? NGX_OK : NGX_DECLINED;
+}
+
+
+static ngx_int_t
+ngx_quic_process_packet(ngx_connection_t *c, ngx_quic_conf_t *conf,
+    ngx_quic_header_t *pkt)
+{
+    ngx_int_t               rc;
+    ngx_quic_send_ctx_t    *ctx;
+    ngx_quic_connection_t  *qc;
+
+    static u_char           buf[NGX_QUIC_MAX_UDP_PAYLOAD_SIZE];
+
+    c->log->action = "parsing quic packet";
+
+    rc = ngx_quic_parse_packet(pkt);
+
+    if (rc == NGX_DECLINED || rc == NGX_ERROR) {
+        return rc;
+    }
+
+    pkt->parsed = 1;
+
+    c->log->action = "processing quic packet";
+
+    qc = ngx_quic_get_connection(c);
+
+#if (NGX_DEBUG)
+    ngx_quic_hexdump(c->log, "quic packet rx dcid",
+                     pkt->dcid.data, pkt->dcid.len);
+
+    if (pkt->level != ssl_encryption_application) {
+        ngx_quic_hexdump(c->log, "quic packet rx scid", pkt->scid.data,
+                         pkt->scid.len);
+    }
+
+    if (pkt->level == ssl_encryption_initial) {
+        ngx_quic_hexdump(c->log, "quic token", pkt->token.data, pkt->token.len);
+    }
+#endif
+
+    if (qc) {
+
+        if (rc == NGX_ABORT) {
+            ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                          "quic unsupported version: 0x%xD", pkt->version);
+            return NGX_DECLINED;
+        }
+
+        if (pkt->level != ssl_encryption_application) {
+            if (pkt->version != qc->version) {
+                ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                              "quic version mismatch: 0x%xD", pkt->version);
+                return NGX_DECLINED;
+            }
+        }
+
+        if (ngx_quic_check_peer(qc, pkt) != NGX_OK) {
+
+            if (pkt->level == ssl_encryption_application) {
+                if (ngx_quic_process_stateless_reset(c, pkt) == NGX_OK) {
+                    ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                                  "quic stateless reset packet detected");
+
+                    qc->draining = 1;
+                    ngx_quic_close_connection(c, NGX_OK);
+
+                    return NGX_OK;
+                }
+
+                return ngx_quic_send_stateless_reset(c, qc->conf, pkt);
+            }
+
+            return NGX_DECLINED;
+        }
+
+        if (qc->in_retry) {
+
+            c->log->action = "retrying quic connection";
+
+            if (pkt->level != ssl_encryption_initial) {
+                ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                              "quic discard late retry packet");
+                return NGX_DECLINED;
+            }
+
+            if (!pkt->token.len) {
+                ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                              "quic discard retry packet without token");
+                return NGX_DECLINED;
+            }
+
+            qc->odcid.len = pkt->dcid.len;
+            qc->odcid.data = ngx_pstrdup(c->pool, &pkt->dcid);
+            if (qc->odcid.data == NULL) {
+                return NGX_ERROR;
+            }
+
+            ngx_quic_clear_temp_server_ids(c);
+
+            qc->dcid.len = NGX_QUIC_SERVER_CID_LEN;
+            qc->dcid.data = ngx_pnalloc(c->pool, qc->dcid.len);
+            if (qc->dcid.data == NULL) {
+                return NGX_ERROR;
+            }
+
+            if (ngx_quic_create_server_id(c, qc->dcid.data) != NGX_OK) {
+                return NGX_ERROR;
+            }
+
+            qc->server_seqnum = 0;
+
+            if (ngx_quic_insert_server_id(c, &qc->dcid) == NULL) {
+                return NGX_ERROR;
+            }
+
+            qc->tp.initial_scid = qc->dcid;
+            qc->in_retry = 0;
+
+            if (ngx_quic_init_secrets(c) != NGX_OK) {
+                return NGX_ERROR;
+            }
+
+            if (ngx_quic_validate_token(c, pkt) != NGX_OK) {
+                return NGX_ERROR;
+            }
+
+            qc->validated = 1;
+        }
+
+    } else {
+
+        if (rc == NGX_ABORT) {
+            return ngx_quic_negotiate_version(c, pkt);
+        }
+
+        if (pkt->level == ssl_encryption_initial) {
+
+            c->log->action = "creating quic connection";
+
+            if (pkt->dcid.len < NGX_QUIC_CID_LEN_MIN) {
+                /* 7.2.  Negotiating Connection IDs */
+                ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                              "quic too short dcid in initial"
+                              " packet: len:%i", pkt->dcid.len);
+                return NGX_ERROR;
+            }
+
+            qc = ngx_quic_new_connection(c, conf, pkt);
+            if (qc == NULL) {
+                return NGX_ERROR;
+            }
+
+            c->udp = &qc->udp;
+
+            if (ngx_terminate || ngx_exiting) {
+                qc->error = NGX_QUIC_ERR_CONNECTION_REFUSED;
+                return NGX_ERROR;
+            }
+
+            if (pkt->token.len) {
+                rc = ngx_quic_validate_token(c, pkt);
+
+                if (rc == NGX_OK) {
+                    qc->validated = 1;
+
+                } else if (rc == NGX_ERROR) {
+                    return NGX_ERROR;
+
+                } else {
+                    /* NGX_DECLINED */
+                    if (conf->retry) {
+                        return ngx_quic_send_retry(c);
+                    }
+                }
+
+            } else if (conf->retry) {
+                return ngx_quic_send_retry(c);
+            }
+
+            if (ngx_quic_init_secrets(c) != NGX_OK) {
+                return NGX_ERROR;
+            }
+
+            if (ngx_quic_insert_server_id(c, &qc->odcid) == NULL) {
+                return NGX_ERROR;
+            }
+
+            qc->server_seqnum = 0;
+
+            if (ngx_quic_insert_server_id(c, &qc->dcid) == NULL) {
+                return NGX_ERROR;
+            }
+
+        } else if (pkt->level == ssl_encryption_application) {
+            return ngx_quic_send_stateless_reset(c, conf, pkt);
+
+        } else {
+            return NGX_ERROR;
+        }
+    }
+
+    c->log->action = "decrypting packet";
+
+    if (!ngx_quic_keys_available(qc->keys, pkt->level)) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "quic no level %d keys yet, ignoring packet", pkt->level);
+        return NGX_DECLINED;
+    }
+
+    pkt->keys = qc->keys;
+    pkt->key_phase = qc->key_phase;
+    pkt->plaintext = buf;
+
+    ctx = ngx_quic_get_send_ctx(qc, pkt->level);
+
+    rc = ngx_quic_decrypt(pkt, &ctx->largest_pn);
+    if (rc != NGX_OK) {
+        qc->error = pkt->error;
+        qc->error_reason = "failed to decrypt packet";
+        return rc;
+    }
+
+    pkt->decrypted = 1;
+
+    if (c->ssl == NULL) {
+        if (ngx_quic_init_connection(c) != NGX_OK) {
+            return NGX_ERROR;
+        }
+    }
+
+    if (pkt->level == ssl_encryption_handshake) {
+        /*
+         * 4.10.1. The successful use of Handshake packets indicates
+         * that no more Initial packets need to be exchanged
+         */
+        ngx_quic_discard_ctx(c, ssl_encryption_initial);
+
+        if (qc->validated == 0) {
+            qc->validated = 1;
+            ngx_post_event(&qc->push, &ngx_posted_events);
+        }
+    }
+
+    pkt->received = ngx_current_msec;
+
+    c->log->action = "handling payload";
+
+    if (pkt->level != ssl_encryption_application) {
+        return ngx_quic_payload_handler(c, pkt);
+    }
+
+    if (!pkt->key_update) {
+        return ngx_quic_payload_handler(c, pkt);
+    }
+
+    /* switch keys and generate next on Key Phase change */
+
+    qc->key_phase ^= 1;
+    ngx_quic_keys_switch(c, qc->keys);
+
+    rc = ngx_quic_payload_handler(c, pkt);
+    if (rc != NGX_OK) {
+        return rc;
+    }
+
+    return ngx_quic_keys_update(c, qc->keys);
+}
+
+
+static ngx_int_t
+ngx_quic_init_secrets(ngx_connection_t *c)
+{
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    if (ngx_quic_keys_set_initial_secret(c->pool, qc->keys, &qc->odcid)
+        != NGX_OK)
+    {
+        return NGX_ERROR;
+    }
+
+    qc->initialized = 1;
+
+    return NGX_OK;
+}
+
+
+static void
+ngx_quic_discard_ctx(ngx_connection_t *c, enum ssl_encryption_level_t level)
+{
+    ngx_queue_t            *q;
+    ngx_quic_frame_t       *f;
+    ngx_quic_send_ctx_t    *ctx;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    if (!ngx_quic_keys_available(qc->keys, level)) {
+        return;
+    }
+
+    ngx_quic_keys_discard(qc->keys, level);
+
+    qc->pto_count = 0;
+
+    ctx = ngx_quic_get_send_ctx(qc, level);
+
+    while (!ngx_queue_empty(&ctx->sent)) {
+        q = ngx_queue_head(&ctx->sent);
+        ngx_queue_remove(q);
+
+        f = ngx_queue_data(q, ngx_quic_frame_t, queue);
+        ngx_quic_congestion_ack(c, f);
+        ngx_quic_free_frame(c, f);
+    }
+
+    while (!ngx_queue_empty(&ctx->frames)) {
+        q = ngx_queue_head(&ctx->frames);
+        ngx_queue_remove(q);
+
+        f = ngx_queue_data(q, ngx_quic_frame_t, queue);
+        ngx_quic_congestion_ack(c, f);
+        ngx_quic_free_frame(c, f);
+    }
+
+    if (level == ssl_encryption_initial) {
+        ngx_quic_clear_temp_server_ids(c);
+    }
+
+    ctx->send_ack = 0;
+}
+
+
+static ngx_int_t
+ngx_quic_check_peer(ngx_quic_connection_t *qc, ngx_quic_header_t *pkt)
+{
+    ngx_queue_t           *q;
+    ngx_quic_client_id_t  *cid;
+
+    if (pkt->level == ssl_encryption_application) {
+        return NGX_OK;
+    }
+
+    for (q = ngx_queue_head(&qc->client_ids);
+         q != ngx_queue_sentinel(&qc->client_ids);
+         q = ngx_queue_next(q))
+    {
+        cid = ngx_queue_data(q, ngx_quic_client_id_t, queue);
+
+        if (pkt->scid.len == cid->len
+            && ngx_memcmp(pkt->scid.data, cid->id, cid->len) == 0)
+        {
+            return NGX_OK;
+        }
+    }
+
+    ngx_log_error(NGX_LOG_INFO, pkt->log, 0, "quic unexpected quic scid");
+    return NGX_ERROR;
+}
+
+
+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              do_close;
+    ngx_quic_frame_t        frame;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    if (qc->closing) {
+        /*
+         * 10.1  Closing and Draining Connection States
+         * ... delayed or reordered packets are properly discarded.
+         *
+         *  An endpoint retains only enough information to generate
+         *  a packet containing a CONNECTION_CLOSE frame and to identify
+         *  packets as belonging to the connection.
+         */
+
+        qc->error_level = pkt->level;
+        qc->error = NGX_QUIC_ERR_NO_ERROR;
+        qc->error_reason = "connection is closing, packet discarded";
+        qc->error_ftype = 0;
+        qc->error_app = 0;
+
+        return ngx_quic_send_cc(c);
+    }
+
+    p = pkt->payload.data;
+    end = p + pkt->payload.len;
+
+    do_close = 0;
+
+    while (p < end) {
+
+        c->log->action = "parsing frames";
+
+        len = ngx_quic_parse_frame(pkt, p, end, &frame);
+
+        if (len < 0) {
+            qc->error = pkt->error;
+            return NGX_ERROR;
+        }
+
+        ngx_quic_log_frame(c->log, &frame, 0);
+
+        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;
+            }
+
+            continue;
+
+        case NGX_QUIC_FT_PADDING:
+            /* no action required */
+            continue;
+
+        case NGX_QUIC_FT_CONNECTION_CLOSE:
+        case NGX_QUIC_FT_CONNECTION_CLOSE_APP:
+            do_close = 1;
+            continue;
+        }
+
+        /* got there with ack-eliciting packet */
+        pkt->need_ack = 1;
+
+        switch (frame.type) {
+
+        case NGX_QUIC_FT_CRYPTO:
+
+            if (ngx_quic_handle_crypto_frame(c, pkt, &frame) != NGX_OK) {
+                return NGX_ERROR;
+            }
+
+            break;
+
+        case NGX_QUIC_FT_PING:
+            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) != NGX_OK) {
+                return NGX_ERROR;
+            }
+
+            break;
+
+        case NGX_QUIC_FT_MAX_DATA:
+
+            if (ngx_quic_handle_max_data_frame(c, &frame.u.max_data) != NGX_OK)
+            {
+                return NGX_ERROR;
+            }
+
+            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;
+            }
+
+            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;
+            }
+
+            break;
+
+        case NGX_QUIC_FT_MAX_STREAM_DATA:
+
+            if (ngx_quic_handle_max_stream_data_frame(c, pkt,
+                                                      &frame.u.max_stream_data)
+                != NGX_OK)
+            {
+                return NGX_ERROR;
+            }
+
+            break;
+
+        case NGX_QUIC_FT_RESET_STREAM:
+
+            if (ngx_quic_handle_reset_stream_frame(c, pkt,
+                                                   &frame.u.reset_stream)
+                != NGX_OK)
+            {
+                return NGX_ERROR;
+            }
+
+            break;
+
+        case NGX_QUIC_FT_STOP_SENDING:
+
+            if (ngx_quic_handle_stop_sending_frame(c, pkt,
+                                                   &frame.u.stop_sending)
+                != NGX_OK)
+            {
+                return NGX_ERROR;
+            }
+
+            break;
+
+        case NGX_QUIC_FT_MAX_STREAMS:
+        case NGX_QUIC_FT_MAX_STREAMS2:
+
+            if (ngx_quic_handle_max_streams_frame(c, pkt, &frame.u.max_streams)
+                != NGX_OK)
+            {
+                return NGX_ERROR;
+            }
+
+            break;
+
+        case NGX_QUIC_FT_PATH_CHALLENGE:
+
+            if (ngx_quic_handle_path_challenge_frame(c, pkt,
+                                                     &frame.u.path_challenge)
+                != NGX_OK)
+            {
+                return NGX_ERROR;
+            }
+
+            break;
+
+        case NGX_QUIC_FT_NEW_CONNECTION_ID:
+
+            if (ngx_quic_handle_new_connection_id_frame(c, pkt, &frame.u.ncid)
+                != NGX_OK)
+            {
+                return NGX_ERROR;
+            }
+
+            break;
+
+        case NGX_QUIC_FT_RETIRE_CONNECTION_ID:
+
+            if (ngx_quic_handle_retire_connection_id_frame(c, pkt,
+                                                           &frame.u.retire_cid)
+                != NGX_OK)
+            {
+                return NGX_ERROR;
+            }
+
+            break;
+
+        case NGX_QUIC_FT_PATH_RESPONSE:
+
+            /* TODO: handle */
+            ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                           "quic frame handler not implemented");
+            break;
+
+        default:
+            ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                           "quic missing frame handler");
+            return NGX_ERROR;
+        }
+    }
+
+    if (p != end) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "quic trailing garbage in payload:%ui bytes", end - p);
+
+        qc->error = NGX_QUIC_ERR_FRAME_ENCODING_ERROR;
+        return NGX_ERROR;
+    }
+
+    if (do_close) {
+        qc->draining = 1;
+        ngx_quic_close_connection(c, NGX_OK);
+    }
+
+    if (ngx_quic_ack_packet(c, pkt) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_ack_packet(ngx_connection_t *c, ngx_quic_header_t *pkt)
+{
+    uint64_t                base, largest, smallest, gs, ge, gap, range, pn;
+    uint64_t                prev_pending;
+    ngx_uint_t              i, nr;
+    ngx_quic_send_ctx_t    *ctx;
+    ngx_quic_ack_range_t   *r;
+    ngx_quic_connection_t  *qc;
+
+    c->log->action = "preparing ack";
+
+    qc = ngx_quic_get_connection(c);
+
+    ctx = ngx_quic_get_send_ctx(qc, pkt->level);
+
+    ngx_log_debug4(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic ngx_quic_ack_packet pn:%uL largest %L fr:%uL"
+                   " nranges:%ui", pkt->pn, (int64_t) ctx->largest_range,
+                   ctx->first_range, ctx->nranges);
+
+    prev_pending = ctx->pending_ack;
+
+    if (pkt->need_ack) {
+
+        ngx_post_event(&qc->push, &ngx_posted_events);
+
+        if (ctx->send_ack == 0) {
+            ctx->ack_delay_start = ngx_current_msec;
+        }
+
+        ctx->send_ack++;
+
+        if (ctx->pending_ack == NGX_QUIC_UNSET_PN
+            || ctx->pending_ack < pkt->pn)
+        {
+            ctx->pending_ack = pkt->pn;
+        }
+    }
+
+    base = ctx->largest_range;
+    pn = pkt->pn;
+
+    if (base == NGX_QUIC_UNSET_PN) {
+        ctx->largest_range = pn;
+        ctx->largest_received = pkt->received;
+        return NGX_OK;
+    }
+
+    if (base == pn) {
+        return NGX_OK;
+    }
+
+    largest = base;
+    smallest = largest - ctx->first_range;
+
+    if (pn > base) {
+
+        if (pn - base == 1) {
+            ctx->first_range++;
+            ctx->largest_range = pn;
+            ctx->largest_received = pkt->received;
+
+            return NGX_OK;
+
+        } else {
+            /* new gap in front of current largest */
+
+            /* no place for new range, send current range as is */
+            if (ctx->nranges == NGX_QUIC_MAX_RANGES) {
+
+                if (prev_pending != NGX_QUIC_UNSET_PN) {
+                    if (ngx_quic_send_ack(c, ctx) != NGX_OK) {
+                        return NGX_ERROR;
+                    }
+                }
+
+                if (prev_pending == ctx->pending_ack || !pkt->need_ack) {
+                    ctx->pending_ack = NGX_QUIC_UNSET_PN;
+                }
+            }
+
+            gap = pn - base - 2;
+            range = ctx->first_range;
+
+            ctx->first_range = 0;
+            ctx->largest_range = pn;
+            ctx->largest_received = pkt->received;
+
+            /* packet is out of order, force send */
+            if (pkt->need_ack) {
+                ctx->send_ack = NGX_QUIC_MAX_ACK_GAP;
+            }
+
+            i = 0;
+
+            goto insert;
+        }
+    }
+
+    /*  pn < base, perform lookup in existing ranges */
+
+    /* packet is out of order */
+    if (pkt->need_ack) {
+        ctx->send_ack = NGX_QUIC_MAX_ACK_GAP;
+    }
+
+    if (pn >= smallest && pn <= largest) {
+        return NGX_OK;
+    }
+
+#if (NGX_SUPPRESS_WARN)
+    r = NULL;
+#endif
+
+    for (i = 0; i < ctx->nranges; i++) {
+        r = &ctx->ranges[i];
+
+        ge = smallest - 1;
+        gs = ge - r->gap;
+
+        if (pn >= gs && pn <= ge) {
+
+            if (gs == ge) {
+                /* gap size is exactly one packet, now filled */
+
+                /* data moves to previous range, current is removed */
+
+                if (i == 0) {
+                    ctx->first_range += r->range + 2;
+
+                } else {
+                    ctx->ranges[i - 1].range += r->range + 2;
+                }
+
+                nr = ctx->nranges - i - 1;
+                if (nr) {
+                    ngx_memmove(&ctx->ranges[i], &ctx->ranges[i + 1],
+                                sizeof(ngx_quic_ack_range_t) * nr);
+                }
+
+                ctx->nranges--;
+
+            } else if (pn == gs) {
+                /* current gap shrinks from tail (current range grows) */
+                r->gap--;
+                r->range++;
+
+            } else if (pn == ge) {
+                /* current gap shrinks from head (previous range grows) */
+                r->gap--;
+
+                if (i == 0) {
+                    ctx->first_range++;
+
+                } else {
+                    ctx->ranges[i - 1].range++;
+                }
+
+            } else {
+                /* current gap is split into two parts */
+
+                gap = ge - pn - 1;
+                range = 0;
+
+                if (ctx->nranges == NGX_QUIC_MAX_RANGES) {
+                    if (prev_pending != NGX_QUIC_UNSET_PN) {
+                        if (ngx_quic_send_ack(c, ctx) != NGX_OK) {
+                            return NGX_ERROR;
+                        }
+                    }
+
+                    if (prev_pending == ctx->pending_ack || !pkt->need_ack) {
+                        ctx->pending_ack = NGX_QUIC_UNSET_PN;
+                    }
+                }
+
+                r->gap = pn - gs - 1;
+                goto insert;
+            }
+
+            return NGX_OK;
+        }
+
+        largest = smallest - r->gap - 2;
+        smallest = largest - r->range;
+
+        if (pn >= smallest && pn <= largest) {
+            /* this packet number is already known */
+            return NGX_OK;
+        }
+
+    }
+
+    if (pn == smallest - 1) {
+        /* extend first or last range */
+
+        if (i == 0) {
+            ctx->first_range++;
+
+        } else {
+            r->range++;
+        }
+
+        return NGX_OK;
+    }
+
+    /* nothing found, add new range at the tail  */
+
+    if (ctx->nranges == NGX_QUIC_MAX_RANGES) {
+        /* packet is too old to keep it */
+
+        if (pkt->need_ack) {
+            return ngx_quic_send_ack_range(c, ctx, pn, pn);
+        }
+
+        return NGX_OK;
+    }
+
+    gap = smallest - 2 - pn;
+    range = 0;
+
+insert:
+
+    if (ctx->nranges < NGX_QUIC_MAX_RANGES) {
+        ctx->nranges++;
+    }
+
+    ngx_memmove(&ctx->ranges[i + 1], &ctx->ranges[i],
+                sizeof(ngx_quic_ack_range_t) * (ctx->nranges - i - 1));
+
+    ctx->ranges[i].gap = gap;
+    ctx->ranges[i].range = range;
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_send_ack_range(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx,
+    uint64_t smallest, uint64_t largest)
+{
+    ngx_quic_frame_t  *frame;
+
+    frame = ngx_quic_alloc_frame(c, 0);
+    if (frame == NULL) {
+        return NGX_ERROR;
+    }
+
+    frame->level = ctx->level;
+    frame->type = NGX_QUIC_FT_ACK;
+    frame->u.ack.largest = largest;
+    frame->u.ack.delay = 0;
+    frame->u.ack.range_count = 0;
+    frame->u.ack.first_range = largest - smallest;
+
+    return NGX_OK;
+}
+
+
+static void
+ngx_quic_drop_ack_ranges(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx,
+    uint64_t pn)
+{
+    uint64_t               base;
+    ngx_uint_t             i, smallest, largest;
+    ngx_quic_ack_range_t  *r;
+
+    ngx_log_debug4(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic ngx_quic_drop_ack_ranges pn:%uL largest:%uL"
+                   " fr:%uL nranges:%ui", pn, ctx->largest_range,
+                   ctx->first_range, ctx->nranges);
+
+    base = ctx->largest_range;
+
+    if (base == NGX_QUIC_UNSET_PN) {
+        return;
+    }
+
+    if (ctx->pending_ack != NGX_QUIC_UNSET_PN && pn >= ctx->pending_ack) {
+        ctx->pending_ack = NGX_QUIC_UNSET_PN;
+    }
+
+    largest = base;
+    smallest = largest - ctx->first_range;
+
+    if (pn >= largest) {
+        ctx->largest_range = NGX_QUIC_UNSET_PN;
+        ctx->first_range = 0;
+        ctx->nranges = 0;
+        return;
+    }
+
+    if (pn >= smallest) {
+        ctx->first_range = largest - pn - 1;
+        ctx->nranges = 0;
+        return;
+    }
+
+    for (i = 0; i < ctx->nranges; i++) {
+        r = &ctx->ranges[i];
+
+        largest = smallest - r->gap - 2;
+        smallest = largest - r->range;
+
+        if (pn >= largest) {
+            ctx->nranges = i;
+            return;
+        }
+        if (pn >= smallest) {
+            r->range = largest - pn - 1;
+            ctx->nranges = i + 1;
+            return;
+        }
+    }
+}
+
+
+static ngx_int_t
+ngx_quic_send_ack(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx)
+{
+    u_char                 *p;
+    size_t                  ranges_len;
+    uint64_t                ack_delay;
+    ngx_uint_t              i;
+    ngx_quic_frame_t       *frame;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    if (ctx->level == ssl_encryption_application) {
+        ack_delay = ngx_current_msec - ctx->largest_received;
+        ack_delay *= 1000;
+        ack_delay >>= qc->ctp.ack_delay_exponent;
+
+    } else {
+        ack_delay = 0;
+    }
+
+    ranges_len = 0;
+
+    for (i = 0; i < ctx->nranges; i++) {
+        ranges_len += ngx_quic_create_ack_range(NULL, ctx->ranges[i].gap,
+                                                ctx->ranges[i].range);
+    }
+
+    frame = ngx_quic_alloc_frame(c, ranges_len);
+    if (frame == NULL) {
+        return NGX_ERROR;
+    }
+
+    p = frame->data;
+
+    for (i = 0; i < ctx->nranges; i++) {
+        p += ngx_quic_create_ack_range(p, ctx->ranges[i].gap,
+                                       ctx->ranges[i].range);
+    }
+
+    frame->level = ctx->level;
+    frame->type = NGX_QUIC_FT_ACK;
+    frame->u.ack.largest = ctx->largest_range;
+    frame->u.ack.delay = ack_delay;
+    frame->u.ack.range_count = ctx->nranges;
+    frame->u.ack.first_range = ctx->first_range;
+    frame->u.ack.ranges_start = frame->data;
+    frame->u.ack.ranges_end = frame->data + ranges_len;
+
+    ngx_quic_queue_frame(qc, frame);
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_send_cc(ngx_connection_t *c)
+{
+    ngx_quic_frame_t       *frame;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    if (qc->draining) {
+        return NGX_OK;
+    }
+
+    if (!qc->initialized) {
+        /* try to initialize secrets to send an early error */
+        if (ngx_quic_init_secrets(c) != NGX_OK) {
+            return NGX_OK;
+        }
+    }
+
+    if (qc->closing
+        && ngx_current_msec - qc->last_cc < NGX_QUIC_CC_MIN_INTERVAL)
+    {
+        /* dot not send CC too often */
+        return NGX_OK;
+    }
+
+    frame = ngx_quic_alloc_frame(c, 0);
+    if (frame == NULL) {
+        return NGX_ERROR;
+    }
+
+    frame->level = qc->error_level;
+    frame->type = NGX_QUIC_FT_CONNECTION_CLOSE;
+    frame->u.close.error_code = qc->error;
+    frame->u.close.frame_type = qc->error_ftype;
+    frame->u.close.app = qc->error_app;
+
+    if (qc->error_reason) {
+        frame->u.close.reason.len = ngx_strlen(qc->error_reason);
+        frame->u.close.reason.data = (u_char *) qc->error_reason;
+    }
+
+    ngx_quic_queue_frame(qc, frame);
+
+    qc->last_cc = ngx_current_msec;
+
+    return ngx_quic_output(c);
+}
+
+
+static ngx_int_t
+ngx_quic_send_new_token(ngx_connection_t *c)
+{
+    ngx_str_t               token;
+    ngx_quic_frame_t       *frame;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    if (!qc->conf->retry) {
+        return NGX_OK;
+    }
+
+    if (ngx_quic_new_token(c, &token) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    frame = ngx_quic_alloc_frame(c, 0);
+    if (frame == NULL) {
+        return NGX_ERROR;
+    }
+
+    frame->level = ssl_encryption_application;
+    frame->type = NGX_QUIC_FT_NEW_TOKEN;
+    frame->u.token.length = token.len;
+    frame->u.token.data = token.data;
+
+    ngx_quic_queue_frame(qc, frame);
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_handle_ack_frame(ngx_connection_t *c, ngx_quic_header_t *pkt,
+    ngx_quic_ack_frame_t *ack)
+{
+    ssize_t                 n;
+    u_char                 *pos, *end;
+    uint64_t                min, max, gap, range;
+    ngx_msec_t              send_time;
+    ngx_uint_t              i;
+    ngx_quic_send_ctx_t    *ctx;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    ctx = ngx_quic_get_send_ctx(qc, pkt->level);
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic ngx_quic_handle_ack_frame level:%d", pkt->level);
+
+    /*
+     *  If any computed packet number is negative, an endpoint MUST
+     *  generate a connection error of type FRAME_ENCODING_ERROR.
+     *  (19.3.1)
+     */
+
+    if (ack->first_range > ack->largest) {
+        qc->error = NGX_QUIC_ERR_FRAME_ENCODING_ERROR;
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "quic invalid first range in ack frame");
+        return NGX_ERROR;
+    }
+
+    min = ack->largest - ack->first_range;
+    max = ack->largest;
+
+    if (ngx_quic_handle_ack_frame_range(c, ctx, min, max, &send_time)
+        != NGX_OK)
+    {
+        return NGX_ERROR;
+    }
+
+    /* 13.2.3.  Receiver Tracking of ACK Frames */
+    if (ctx->largest_ack < max || ctx->largest_ack == NGX_QUIC_UNSET_PN) {
+        ctx->largest_ack = max;
+        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic updated largest received ack:%uL", max);
+
+        /*
+         *  An endpoint generates an RTT sample on receiving an
+         *  ACK frame that meets the following two conditions:
+         *
+         *  - the largest acknowledged packet number is newly acknowledged
+         *  - at least one of the newly acknowledged packets was ack-eliciting.
+         */
+
+        if (send_time != NGX_TIMER_INFINITE) {
+            ngx_quic_rtt_sample(c, ack, pkt->level, send_time);
+        }
+    }
+
+    pos = ack->ranges_start;
+    end = ack->ranges_end;
+
+    for (i = 0; i < ack->range_count; i++) {
+
+        n = ngx_quic_parse_ack_range(pkt->log, pos, end, &gap, &range);
+        if (n == NGX_ERROR) {
+            return NGX_ERROR;
+        }
+        pos += n;
+
+        if (gap + 2 > min) {
+            qc->error = NGX_QUIC_ERR_FRAME_ENCODING_ERROR;
+            ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                         "quic invalid range:%ui in ack frame", i);
+            return NGX_ERROR;
+        }
+
+        max = min - gap - 2;
+
+        if (range > max) {
+            qc->error = NGX_QUIC_ERR_FRAME_ENCODING_ERROR;
+            ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                         "quic invalid range:%ui in ack frame", i);
+            return NGX_ERROR;
+        }
+
+        min = max - range;
+
+        if (ngx_quic_handle_ack_frame_range(c, ctx, min, max, &send_time)
+            != NGX_OK)
+        {
+            return NGX_ERROR;
+        }
+    }
+
+    return ngx_quic_detect_lost(c);
+}
+
+
+static ngx_int_t
+ngx_quic_handle_ack_frame_range(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx,
+    uint64_t min, uint64_t max, ngx_msec_t *send_time)
+{
+    uint64_t                found_num;
+    ngx_uint_t              found;
+    ngx_queue_t            *q;
+    ngx_quic_frame_t       *f;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    *send_time = NGX_TIMER_INFINITE;
+    found = 0;
+    found_num = 0;
+
+    q = ngx_queue_last(&ctx->sent);
+
+    while (q != ngx_queue_sentinel(&ctx->sent)) {
+
+        f = ngx_queue_data(q, ngx_quic_frame_t, queue);
+        q = ngx_queue_prev(q);
+
+        if (f->pnum >= min && f->pnum <= max) {
+            ngx_quic_congestion_ack(c, f);
+
+            switch (f->type) {
+            case NGX_QUIC_FT_ACK:
+            case NGX_QUIC_FT_ACK_ECN:
+                ngx_quic_drop_ack_ranges(c, ctx, f->u.ack.largest);
+                break;
+
+            case NGX_QUIC_FT_STREAM0:
+            case NGX_QUIC_FT_STREAM1:
+            case NGX_QUIC_FT_STREAM2:
+            case NGX_QUIC_FT_STREAM3:
+            case NGX_QUIC_FT_STREAM4:
+            case NGX_QUIC_FT_STREAM5:
+            case NGX_QUIC_FT_STREAM6:
+            case NGX_QUIC_FT_STREAM7:
+                ngx_quic_handle_stream_ack(c, f);
+                break;
+            }
+
+            if (f->pnum > found_num || !found) {
+                *send_time = f->last;
+                found_num = f->pnum;
+            }
+
+            ngx_queue_remove(&f->queue);
+            ngx_quic_free_frame(c, f);
+            found = 1;
+        }
+    }
+
+    if (!found) {
+
+        if (max < ctx->pnum) {
+            /* duplicate ACK or ACK for non-ack-eliciting frame */
+            return NGX_OK;
+        }
+
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "quic ACK for the packet not sent");
+
+        qc->error = NGX_QUIC_ERR_PROTOCOL_VIOLATION;
+        qc->error_ftype = NGX_QUIC_FT_ACK;
+        qc->error_reason = "unknown packet number";
+
+        return NGX_ERROR;
+    }
+
+    if (!qc->push.timer_set) {
+        ngx_post_event(&qc->push, &ngx_posted_events);
+    }
+
+    qc->pto_count = 0;
+
+    return NGX_OK;
+}
+
+
+static void
+ngx_quic_rtt_sample(ngx_connection_t *c, ngx_quic_ack_frame_t *ack,
+    enum ssl_encryption_level_t level, ngx_msec_t send_time)
+{
+    ngx_msec_t              latest_rtt, ack_delay, adjusted_rtt, rttvar_sample;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    latest_rtt = ngx_current_msec - send_time;
+    qc->latest_rtt = latest_rtt;
+
+    if (qc->min_rtt == NGX_TIMER_INFINITE) {
+        qc->min_rtt = latest_rtt;
+        qc->avg_rtt = latest_rtt;
+        qc->rttvar = latest_rtt / 2;
+
+    } else {
+        qc->min_rtt = ngx_min(qc->min_rtt, latest_rtt);
+
+
+        if (level == ssl_encryption_application) {
+            ack_delay = ack->delay * (1 << qc->ctp.ack_delay_exponent) / 1000;
+            ack_delay = ngx_min(ack_delay, qc->ctp.max_ack_delay);
+
+        } else {
+            ack_delay = 0;
+        }
+
+        adjusted_rtt = latest_rtt;
+
+        if (qc->min_rtt + ack_delay < latest_rtt) {
+            adjusted_rtt -= ack_delay;
+        }
+
+        qc->avg_rtt = 0.875 * qc->avg_rtt + 0.125 * adjusted_rtt;
+        rttvar_sample = ngx_abs((ngx_msec_int_t) (qc->avg_rtt - adjusted_rtt));
+        qc->rttvar = 0.75 * qc->rttvar + 0.25 * rttvar_sample;
+    }
+
+    ngx_log_debug4(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic rtt sample latest:%M min:%M avg:%M var:%M",
+                   latest_rtt, qc->min_rtt, qc->avg_rtt, qc->rttvar);
+}
+
+
+static ngx_inline ngx_msec_t
+ngx_quic_pto(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx)
+{
+    ngx_msec_t              duration;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    /* PTO calculation: quic-recovery, Appendix 8 */
+    duration = qc->avg_rtt;
+
+    duration += ngx_max(4 * qc->rttvar, NGX_QUIC_TIME_GRANULARITY);
+    duration <<= qc->pto_count;
+
+    if (qc->congestion.in_flight == 0) { /* no in-flight packets */
+        return duration;
+    }
+
+    if (ctx == &qc->send_ctx[2] && c->ssl->handshaked) {
+        /* application send space */
+
+        duration += qc->tp.max_ack_delay << qc->pto_count;
+    }
+
+    return duration;
+}
+
+
+static void
+ngx_quic_handle_stream_ack(ngx_connection_t *c, ngx_quic_frame_t *f)
+{
+    uint64_t                sent, unacked;
+    ngx_event_t            *wev;
+    ngx_quic_stream_t      *sn;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    sn = ngx_quic_find_stream(&qc->streams.tree, f->u.stream.stream_id);
+    if (sn == NULL) {
+        return;
+    }
+
+    wev = sn->c->write;
+    sent = sn->c->sent;
+    unacked = sent - sn->acked;
+
+    if (unacked >= NGX_QUIC_STREAM_BUFSIZE && wev->active) {
+        wev->ready = 1;
+        ngx_post_event(wev, &ngx_posted_events);
+    }
+
+    sn->acked += f->u.stream.length;
+
+    ngx_log_debug3(NGX_LOG_DEBUG_EVENT, sn->c->log, 0,
+                   "quic stream ack len:%uL acked:%uL unacked:%uL",
+                   f->u.stream.length, sn->acked, sent - sn->acked);
+}
+
+
+static ngx_int_t
+ngx_quic_handle_ordered_frame(ngx_connection_t *c, ngx_quic_frames_stream_t *fs,
+    ngx_quic_frame_t *frame, ngx_quic_frame_handler_pt handler, void *data)
+{
+    size_t                     full_len;
+    ngx_int_t                  rc;
+    ngx_queue_t               *q;
+    ngx_quic_ordered_frame_t  *f;
+
+    f = &frame->u.ord;
+
+    if (f->offset > fs->received) {
+        ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic out-of-order frame: expecting:%uL got:%uL",
+                       fs->received, f->offset);
+
+        return ngx_quic_buffer_frame(c, fs, frame);
+    }
+
+    if (f->offset < fs->received) {
+
+        if (ngx_quic_adjust_frame_offset(c, frame, fs->received)
+            == NGX_DONE)
+        {
+            /* old/duplicate data range */
+            return handler == ngx_quic_crypto_input ? NGX_DECLINED : NGX_OK;
+        }
+
+        /* intersecting data range, frame modified */
+    }
+
+    /* f->offset == fs->received */
+
+    rc = handler(c, frame, data);
+    if (rc == NGX_ERROR) {
+        return NGX_ERROR;
+
+    } else if (rc == NGX_DONE) {
+        /* handler destroyed stream, queue no longer exists */
+        return NGX_OK;
+    }
+
+    /* rc == NGX_OK */
+
+    fs->received += f->length;
+
+    /* now check the queue if we can continue with buffered frames */
+
+    do {
+        q = ngx_queue_head(&fs->frames);
+        if (q == ngx_queue_sentinel(&fs->frames)) {
+            break;
+        }
+
+        frame = ngx_queue_data(q, ngx_quic_frame_t, queue);
+        f = &frame->u.ord;
+
+        if (f->offset > fs->received) {
+            /* gap found, nothing more to do */
+            break;
+        }
+
+        full_len = f->length;
+
+        if (f->offset < fs->received) {
+
+            if (ngx_quic_adjust_frame_offset(c, frame, fs->received)
+                == NGX_DONE)
+            {
+                /* old/duplicate data range */
+                ngx_queue_remove(q);
+                fs->total -= f->length;
+
+                ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                              "quic skipped buffered frame, total:%ui",
+                              fs->total);
+                ngx_quic_free_frame(c, frame);
+                continue;
+            }
+
+            /* frame was adjusted, proceed to input */
+        }
+
+        /* f->offset == fs->received */
+
+        rc = handler(c, frame, data);
+
+        if (rc == NGX_ERROR) {
+            return NGX_ERROR;
+
+        } else if (rc == NGX_DONE) {
+            /* handler destroyed stream, queue no longer exists */
+            return NGX_OK;
+        }
+
+        fs->received += f->length;
+        fs->total -= full_len;
+
+        ngx_queue_remove(q);
+
+        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                      "quic consumed buffered frame, total:%ui", fs->total);
+
+        ngx_quic_free_frame(c, frame);
+
+    } while (1);
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_adjust_frame_offset(ngx_connection_t *c, ngx_quic_frame_t *frame,
+    uint64_t offset_in)
+{
+    size_t                     tail;
+    ngx_quic_ordered_frame_t  *f;
+
+    f = &frame->u.ord;
+
+    tail = offset_in - f->offset;
+
+    if (tail >= f->length) {
+        /* range preceeding already received data or duplicate, ignore */
+
+        ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic old or duplicate data in ordered frame, ignored");
+        return NGX_DONE;
+    }
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic adjusted ordered frame data start to expected offset");
+
+    /* intersecting range: adjust data size */
+
+    f->offset += tail;
+    f->data += tail;
+    f->length -= tail;
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_buffer_frame(ngx_connection_t *c, ngx_quic_frames_stream_t *fs,
+    ngx_quic_frame_t *frame)
+{
+    u_char                    *data;
+    ngx_queue_t               *q;
+    ngx_quic_frame_t          *dst, *item;
+    ngx_quic_ordered_frame_t  *f, *df;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic ngx_quic_buffer_frame");
+
+    f = &frame->u.ord;
+
+    /* frame start offset is in the future, buffer it */
+
+    dst = ngx_quic_alloc_frame(c, f->length);
+    if (dst == NULL) {
+        return NGX_ERROR;
+    }
+
+    data = dst->data;
+    ngx_memcpy(dst, frame, sizeof(ngx_quic_frame_t));
+    dst->data = data;
+
+    ngx_memcpy(dst->data, f->data, f->length);
+
+    df = &dst->u.ord;
+    df->data = dst->data;
+
+    fs->total += f->length;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                  "quic ordered frame with unexpected offset:"
+                  " buffered total:%ui", fs->total);
+
+    if (ngx_queue_empty(&fs->frames)) {
+        ngx_queue_insert_after(&fs->frames, &dst->queue);
+        return NGX_OK;
+    }
+
+    for (q = ngx_queue_last(&fs->frames);
+         q != ngx_queue_sentinel(&fs->frames);
+         q = ngx_queue_prev(q))
+    {
+        item = ngx_queue_data(q, ngx_quic_frame_t, queue);
+        f = &item->u.ord;
+
+        if (f->offset < df->offset) {
+            ngx_queue_insert_after(q, &dst->queue);
+            return NGX_OK;
+        }
+    }
+
+    ngx_queue_insert_after(&fs->frames, &dst->queue);
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_handle_crypto_frame(ngx_connection_t *c, ngx_quic_header_t *pkt,
+    ngx_quic_frame_t *frame)
+{
+    uint64_t                   last;
+    ngx_int_t                  rc;
+    ngx_quic_send_ctx_t       *ctx;
+    ngx_quic_connection_t     *qc;
+    ngx_quic_crypto_frame_t   *f;
+    ngx_quic_frames_stream_t  *fs;
+
+    qc = ngx_quic_get_connection(c);
+    fs = &qc->crypto[pkt->level];
+    f = &frame->u.crypto;
+
+    /* no overflow since both values are 62-bit */
+    last = f->offset + f->length;
+
+    if (last > fs->received && last - fs->received > NGX_QUIC_MAX_BUFFERED) {
+        qc->error = NGX_QUIC_ERR_CRYPTO_BUFFER_EXCEEDED;
+        return NGX_ERROR;
+    }
+
+    rc = ngx_quic_handle_ordered_frame(c, fs, frame, ngx_quic_crypto_input,
+                                       NULL);
+    if (rc != NGX_DECLINED) {
+        return rc;
+    }
+
+    /* speeding up handshake completion */
+
+    if (pkt->level == ssl_encryption_initial) {
+        ctx = ngx_quic_get_send_ctx(qc, pkt->level);
+
+        if (!ngx_queue_empty(&ctx->sent)) {
+            ngx_quic_resend_frames(c, ctx);
+        }
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_crypto_input(ngx_connection_t *c, ngx_quic_frame_t *frame, void *data)
+{
+    int                       n, sslerr;
+    ngx_ssl_conn_t           *ssl_conn;
+    ngx_quic_connection_t    *qc;
+    ngx_quic_crypto_frame_t  *f;
+
+    qc = ngx_quic_get_connection(c);
+
+    f = &frame->u.crypto;
+
+    ssl_conn = c->ssl->connection;
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic 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->length))
+    {
+        ngx_ssl_error(NGX_LOG_INFO, c->log, 0,
+                      "SSL_provide_quic_data() failed");
+        return NGX_ERROR;
+    }
+
+    n = SSL_do_handshake(ssl_conn);
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic SSL_quic_read_level:%d SSL_quic_write_level:%d",
+                   (int) SSL_quic_read_level(ssl_conn),
+                   (int) SSL_quic_write_level(ssl_conn));
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, "SSL_do_handshake: %d", n);
+
+    if (n <= 0) {
+        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;
+        }
+
+        return NGX_OK;
+    }
+
+    if (SSL_in_init(ssl_conn)) {
+        return NGX_OK;
+    }
+
+    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,
+                   "quic handshake completed successfully");
+
+    c->ssl->handshaked = 1;
+    c->ssl->no_wait_shutdown = 1;
+
+    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_quic_queue_frame(qc, frame);
+
+    if (ngx_quic_send_new_token(c) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    /*
+     * Generating next keys before a key update is received.
+     * See quic-tls 9.4 Header Protection Timing Side-Channels.
+     */
+
+    if (ngx_quic_keys_update(c, qc->keys) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    /*
+     * 4.10.2 An endpoint MUST discard its handshake keys
+     * when the TLS handshake is confirmed
+     */
+    ngx_quic_discard_ctx(c, ssl_encryption_handshake);
+
+    if (ngx_quic_issue_server_ids(c) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_handle_stream_frame(ngx_connection_t *c, ngx_quic_header_t *pkt,
+    ngx_quic_frame_t *frame)
+{
+    size_t                     window;
+    uint64_t                   last;
+    ngx_buf_t                 *b;
+    ngx_pool_t                *pool;
+    ngx_connection_t          *sc;
+    ngx_quic_stream_t         *sn;
+    ngx_quic_connection_t     *qc;
+    ngx_quic_stream_frame_t   *f;
+    ngx_quic_frames_stream_t  *fs;
+
+    qc = ngx_quic_get_connection(c);
+    f = &frame->u.stream;
+
+    if ((f->stream_id & NGX_QUIC_STREAM_UNIDIRECTIONAL)
+        && (f->stream_id & NGX_QUIC_STREAM_SERVER_INITIATED))
+    {
+        qc->error = NGX_QUIC_ERR_STREAM_STATE_ERROR;
+        return NGX_ERROR;
+    }
+
+    /* no overflow since both values are 62-bit */
+    last = f->offset + f->length;
+
+    sn = ngx_quic_find_stream(&qc->streams.tree, f->stream_id);
+
+    if (sn == NULL) {
+        sn = ngx_quic_create_client_stream(c, f->stream_id);
+
+        if (sn == NULL) {
+            return NGX_ERROR;
+        }
+
+        if (sn == NGX_QUIC_STREAM_GONE) {
+            return NGX_OK;
+        }
+
+        sc = sn->c;
+        fs = &sn->fs;
+        b = sn->b;
+        window = b->end - b->last;
+
+        if (last > window) {
+            qc->error = NGX_QUIC_ERR_FLOW_CONTROL_ERROR;
+            goto cleanup;
+        }
+
+        if (ngx_quic_handle_ordered_frame(c, fs, frame, ngx_quic_stream_input,
+                                          sn)
+            != NGX_OK)
+        {
+            goto cleanup;
+        }
+
+        sc->listening->handler(sc);
+
+        return NGX_OK;
+    }
+
+    fs = &sn->fs;
+    b = sn->b;
+    window = (b->pos - b->start) + (b->end - b->last);
+
+    if (last > fs->received && last - fs->received > window) {
+        qc->error = NGX_QUIC_ERR_FLOW_CONTROL_ERROR;
+        return NGX_ERROR;
+    }
+
+    return ngx_quic_handle_ordered_frame(c, fs, frame, ngx_quic_stream_input,
+                                         sn);
+
+cleanup:
+
+    pool = sc->pool;
+
+    ngx_close_connection(sc);
+    ngx_destroy_pool(pool);
+
+    return NGX_ERROR;
+}
+
+
+static ngx_int_t
+ngx_quic_stream_input(ngx_connection_t *c, ngx_quic_frame_t *frame, void *data)
+{
+    uint64_t                  id;
+    ngx_buf_t                *b;
+    ngx_event_t              *rev;
+    ngx_quic_stream_t        *sn;
+    ngx_quic_connection_t    *qc;
+    ngx_quic_stream_frame_t  *f;
+
+    qc = ngx_quic_get_connection(c);
+    sn = data;
+
+    f = &frame->u.stream;
+    id = f->stream_id;
+
+    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,
+                      "quic 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);
+    }
+
+    /* check if stream was destroyed by handler */
+    if (ngx_quic_find_stream(&qc->streams.tree, id) == NULL) {
+        return NGX_DONE;
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_handle_max_data_frame(ngx_connection_t *c,
+    ngx_quic_max_data_frame_t *f)
+{
+    ngx_event_t            *wev;
+    ngx_rbtree_t           *tree;
+    ngx_rbtree_node_t      *node;
+    ngx_quic_stream_t      *qs;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+    tree = &qc->streams.tree;
+
+    if (f->max_data <= qc->streams.send_max_data) {
+        return NGX_OK;
+    }
+
+    if (qc->streams.sent >= qc->streams.send_max_data) {
+
+        for (node = ngx_rbtree_min(tree->root, tree->sentinel);
+             node;
+             node = ngx_rbtree_next(tree, node))
+        {
+            qs = (ngx_quic_stream_t *) node;
+            wev = qs->c->write;
+
+            if (wev->active) {
+                wev->ready = 1;
+                ngx_post_event(wev, &ngx_posted_events);
+            }
+        }
+    }
+
+    qc->streams.send_max_data = f->max_data;
+
+    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)
+{
+    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 = ngx_quic_get_connection(c);
+
+    if ((f->id & NGX_QUIC_STREAM_UNIDIRECTIONAL)
+        && (f->id & NGX_QUIC_STREAM_SERVER_INITIATED))
+    {
+        qc->error = NGX_QUIC_ERR_STREAM_STATE_ERROR;
+        return NGX_ERROR;
+    }
+
+    sn = ngx_quic_find_stream(&qc->streams.tree, f->id);
+
+    if (sn == NULL) {
+        sn = ngx_quic_create_client_stream(c, f->id);
+
+        if (sn == NULL) {
+            return NGX_ERROR;
+        }
+
+        if (sn == NGX_QUIC_STREAM_GONE) {
+            return NGX_OK;
+        }
+
+        b = sn->b;
+        n = b->end - b->last;
+
+        sn->c->listening->handler(sn->c);
+
+    } else {
+        b = sn->b;
+        n = sn->fs.received + (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_quic_queue_frame(qc, frame);
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_handle_max_stream_data_frame(ngx_connection_t *c,
+    ngx_quic_header_t *pkt, ngx_quic_max_stream_data_frame_t *f)
+{
+    uint64_t                sent;
+    ngx_event_t            *wev;
+    ngx_quic_stream_t      *sn;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    if ((f->id & NGX_QUIC_STREAM_UNIDIRECTIONAL)
+        && (f->id & NGX_QUIC_STREAM_SERVER_INITIATED) == 0)
+    {
+        qc->error = NGX_QUIC_ERR_STREAM_STATE_ERROR;
+        return NGX_ERROR;
+    }
+
+    sn = ngx_quic_find_stream(&qc->streams.tree, f->id);
+
+    if (sn == NULL) {
+        sn = ngx_quic_create_client_stream(c, f->id);
+
+        if (sn == NULL) {
+            return NGX_ERROR;
+        }
+
+        if (sn == NGX_QUIC_STREAM_GONE) {
+            return NGX_OK;
+        }
+
+        if (f->limit > sn->send_max_data) {
+            sn->send_max_data = f->limit;
+        }
+
+        sn->c->listening->handler(sn->c);
+
+        return NGX_OK;
+    }
+
+    if (f->limit <= sn->send_max_data) {
+        return NGX_OK;
+    }
+
+    sent = sn->c->sent;
+
+    if (sent >= sn->send_max_data) {
+        wev = sn->c->write;
+
+        if (wev->active) {
+            wev->ready = 1;
+            ngx_post_event(wev, &ngx_posted_events);
+        }
+    }
+
+    sn->send_max_data = f->limit;
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_handle_reset_stream_frame(ngx_connection_t *c,
+    ngx_quic_header_t *pkt, ngx_quic_reset_stream_frame_t *f)
+{
+    ngx_event_t            *rev;
+    ngx_connection_t       *sc;
+    ngx_quic_stream_t      *sn;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    if ((f->id & NGX_QUIC_STREAM_UNIDIRECTIONAL)
+        && (f->id & NGX_QUIC_STREAM_SERVER_INITIATED))
+    {
+        qc->error = NGX_QUIC_ERR_STREAM_STATE_ERROR;
+        return NGX_ERROR;
+    }
+
+    sn = ngx_quic_find_stream(&qc->streams.tree, f->id);
+
+    if (sn == NULL) {
+        sn = ngx_quic_create_client_stream(c, f->id);
+
+        if (sn == NULL) {
+            return NGX_ERROR;
+        }
+
+        if (sn == NGX_QUIC_STREAM_GONE) {
+            return NGX_OK;
+        }
+
+        sc = sn->c;
+
+        rev = sc->read;
+        rev->error = 1;
+        rev->ready = 1;
+
+        sc->listening->handler(sc);
+
+        return NGX_OK;
+    }
+
+    rev = sn->c->read;
+    rev->error = 1;
+    rev->ready = 1;
+
+    if (rev->active) {
+        rev->handler(rev);
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_handle_stop_sending_frame(ngx_connection_t *c,
+    ngx_quic_header_t *pkt, ngx_quic_stop_sending_frame_t *f)
+{
+    ngx_event_t            *wev;
+    ngx_connection_t       *sc;
+    ngx_quic_stream_t      *sn;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    if ((f->id & NGX_QUIC_STREAM_UNIDIRECTIONAL)
+        && (f->id & NGX_QUIC_STREAM_SERVER_INITIATED) == 0)
+    {
+        qc->error = NGX_QUIC_ERR_STREAM_STATE_ERROR;
+        return NGX_ERROR;
+    }
+
+    sn = ngx_quic_find_stream(&qc->streams.tree, f->id);
+
+    if (sn == NULL) {
+        sn = ngx_quic_create_client_stream(c, f->id);
+
+        if (sn == NULL) {
+            return NGX_ERROR;
+        }
+
+        if (sn == NGX_QUIC_STREAM_GONE) {
+            return NGX_OK;
+        }
+
+        sc = sn->c;
+
+        wev = sc->write;
+        wev->error = 1;
+        wev->ready = 1;
+
+        sc->listening->handler(sc);
+
+        return NGX_OK;
+    }
+
+    wev = sn->c->write;
+    wev->error = 1;
+    wev->ready = 1;
+
+    if (wev->active) {
+        wev->handler(wev);
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_handle_max_streams_frame(ngx_connection_t *c,
+    ngx_quic_header_t *pkt, ngx_quic_max_streams_frame_t *f)
+{
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    if (f->bidi) {
+        if (qc->streams.server_max_streams_bidi < f->limit) {
+            qc->streams.server_max_streams_bidi = f->limit;
+
+            ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                           "quic max_streams_bidi:%uL", f->limit);
+        }
+
+    } else {
+        if (qc->streams.server_max_streams_uni < f->limit) {
+            qc->streams.server_max_streams_uni = f->limit;
+
+            ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                           "quic max_streams_uni:%uL", f->limit);
+        }
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_handle_path_challenge_frame(ngx_connection_t *c,
+    ngx_quic_header_t *pkt, ngx_quic_path_challenge_frame_t *f)
+{
+    ngx_quic_frame_t       *frame;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    frame = ngx_quic_alloc_frame(c, 0);
+    if (frame == NULL) {
+        return NGX_ERROR;
+    }
+
+    frame->level = pkt->level;
+    frame->type = NGX_QUIC_FT_PATH_RESPONSE;
+    frame->u.path_response = *f;
+
+    ngx_quic_queue_frame(qc, frame);
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_handle_new_connection_id_frame(ngx_connection_t *c,
+    ngx_quic_header_t *pkt, ngx_quic_new_conn_id_frame_t *f)
+{
+    ngx_queue_t            *q;
+    ngx_quic_client_id_t   *cid, *item;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    if (f->seqnum < qc->max_retired_seqnum) {
+        /*
+         *  An endpoint that receives a NEW_CONNECTION_ID frame with
+         *  a sequence number smaller than the Retire Prior To field
+         *  of a previously received NEW_CONNECTION_ID frame MUST send
+         *  a corresponding RETIRE_CONNECTION_ID frame that retires
+         *  the newly received connection  ID, unless it has already
+         *  done so for that sequence number.
+         */
+
+        if (ngx_quic_retire_connection_id(c, pkt->level, f->seqnum) != NGX_OK) {
+            return NGX_ERROR;
+        }
+
+        goto retire;
+    }
+
+    cid = NULL;
+
+    for (q = ngx_queue_head(&qc->client_ids);
+         q != ngx_queue_sentinel(&qc->client_ids);
+         q = ngx_queue_next(q))
+    {
+        item = ngx_queue_data(q, ngx_quic_client_id_t, queue);
+
+        if (item->seqnum == f->seqnum) {
+            cid = item;
+            break;
+        }
+    }
+
+    if (cid) {
+        /*
+         * Transmission errors, timeouts and retransmissions might cause the
+         * same NEW_CONNECTION_ID frame to be received multiple times
+         */
+
+        if (cid->len != f->len
+            || ngx_strncmp(cid->id, f->cid, f->len) != 0
+            || ngx_strncmp(cid->sr_token, f->srt, NGX_QUIC_SR_TOKEN_LEN) != 0)
+        {
+            /*
+             * ..a sequence number is used for different connection IDs,
+             * the endpoint MAY treat that receipt as a connection error
+             * of type PROTOCOL_VIOLATION.
+             */
+            qc->error = NGX_QUIC_ERR_PROTOCOL_VIOLATION;
+            qc->error_reason = "seqnum refers to different connection id/token";
+            return NGX_ERROR;
+        }
+
+    } else {
+
+        cid = ngx_quic_alloc_client_id(c, qc);
+        if (cid == NULL) {
+            return NGX_ERROR;
+        }
+
+        cid->seqnum = f->seqnum;
+        cid->len = f->len;
+        ngx_memcpy(cid->id, f->cid, f->len);
+
+        ngx_memcpy(cid->sr_token, f->srt, NGX_QUIC_SR_TOKEN_LEN);
+
+        ngx_queue_insert_tail(&qc->client_ids, &cid->queue);
+        qc->nclient_ids++;
+
+        /* always use latest available connection id */
+        if (f->seqnum > qc->client_seqnum) {
+            qc->scid.len = cid->len;
+            qc->scid.data = cid->id;
+            qc->client_seqnum = f->seqnum;
+        }
+    }
+
+retire:
+
+    if (qc->max_retired_seqnum && f->retire <= qc->max_retired_seqnum) {
+        /*
+         * Once a sender indicates a Retire Prior To value, smaller values sent
+         * in subsequent NEW_CONNECTION_ID frames have no effect.  A receiver
+         * MUST ignore any Retire Prior To fields that do not increase the
+         * largest received Retire Prior To value.
+         */
+        goto done;
+    }
+
+    qc->max_retired_seqnum = f->retire;
+
+    q = ngx_queue_head(&qc->client_ids);
+
+    while (q != ngx_queue_sentinel(&qc->client_ids)) {
+
+        cid = ngx_queue_data(q, ngx_quic_client_id_t, queue);
+        q = ngx_queue_next(q);
+
+        if (cid->seqnum >= f->retire) {
+            continue;
+        }
+
+        /* this connection id must be retired */
+
+        if (ngx_quic_retire_connection_id(c, pkt->level, cid->seqnum)
+            != NGX_OK)
+        {
+            return NGX_ERROR;
+        }
+
+        ngx_queue_remove(&cid->queue);
+        ngx_queue_insert_head(&qc->free_client_ids, &cid->queue);
+        qc->nclient_ids--;
+    }
+
+done:
+
+    if (qc->nclient_ids > qc->tp.active_connection_id_limit) {
+        /*
+         * After processing a NEW_CONNECTION_ID frame and
+         * adding and retiring active connection IDs, if the number of active
+         * connection IDs exceeds the value advertised in its
+         * active_connection_id_limit transport parameter, an endpoint MUST
+         * close the connection with an error of type CONNECTION_ID_LIMIT_ERROR.
+         */
+        qc->error = NGX_QUIC_ERR_CONNECTION_ID_LIMIT_ERROR;
+        qc->error_reason = "too many connection ids received";
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_retire_connection_id(ngx_connection_t *c,
+    enum ssl_encryption_level_t level, uint64_t seqnum)
+{
+    ngx_quic_frame_t       *frame;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    frame = ngx_quic_alloc_frame(c, 0);
+    if (frame == NULL) {
+        return NGX_ERROR;
+    }
+
+    frame->level = level;
+    frame->type = NGX_QUIC_FT_RETIRE_CONNECTION_ID;
+    frame->u.retire_cid.sequence_number = seqnum;
+
+    ngx_quic_queue_frame(qc, frame);
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_handle_retire_connection_id_frame(ngx_connection_t *c,
+    ngx_quic_header_t *pkt, ngx_quic_retire_cid_frame_t *f)
+{
+    ngx_queue_t            *q;
+    ngx_quic_server_id_t   *sid;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    for (q = ngx_queue_head(&qc->server_ids);
+         q != ngx_queue_sentinel(&qc->server_ids);
+         q = ngx_queue_next(q))
+    {
+        sid = ngx_queue_data(q, ngx_quic_server_id_t, queue);
+
+        if (sid->seqnum == f->sequence_number) {
+            ngx_queue_remove(q);
+            ngx_queue_insert_tail(&qc->free_server_ids, &sid->queue);
+            ngx_rbtree_delete(&c->listening->rbtree, &sid->udp.node);
+            qc->nserver_ids--;
+            break;
+        }
+    }
+
+    return ngx_quic_issue_server_ids(c);
+}
+
+
+static ngx_int_t
+ngx_quic_issue_server_ids(ngx_connection_t *c)
+{
+    ngx_str_t               dcid;
+    ngx_uint_t              n;
+    ngx_quic_frame_t       *frame;
+    ngx_quic_server_id_t   *sid;
+    ngx_quic_connection_t  *qc;
+    u_char                  id[NGX_QUIC_SERVER_CID_LEN];
+
+    qc = ngx_quic_get_connection(c);
+
+    n = ngx_min(NGX_QUIC_MAX_SERVER_IDS, qc->ctp.active_connection_id_limit);
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic issue server ids has:%ui max:%ui", qc->nserver_ids, n);
+
+    while (qc->nserver_ids < n) {
+        if (ngx_quic_create_server_id(c, id) != NGX_OK) {
+            return NGX_ERROR;
+        }
+
+        dcid.len = NGX_QUIC_SERVER_CID_LEN;
+        dcid.data = id;
+
+        sid = ngx_quic_insert_server_id(c, &dcid);
+        if (sid == NULL) {
+            return NGX_ERROR;
+        }
+
+        frame = ngx_quic_alloc_frame(c, 0);
+        if (frame == NULL) {
+            return NGX_ERROR;
+        }
+
+        frame->level = ssl_encryption_application;
+        frame->type = NGX_QUIC_FT_NEW_CONNECTION_ID;
+        frame->u.ncid.seqnum = sid->seqnum;
+        frame->u.ncid.retire = 0;
+        frame->u.ncid.len = NGX_QUIC_SERVER_CID_LEN;
+        ngx_memcpy(frame->u.ncid.cid, id, NGX_QUIC_SERVER_CID_LEN);
+
+        if (ngx_quic_new_sr_token(c, &dcid, &qc->conf->sr_token_key,
+                                  frame->u.ncid.srt)
+            != NGX_OK)
+        {
+            return NGX_ERROR;
+        }
+
+        ngx_quic_queue_frame(qc, frame);
+    }
+
+    return NGX_OK;
+}
+
+
+static void
+ngx_quic_clear_temp_server_ids(ngx_connection_t *c)
+{
+    ngx_queue_t            *q, *next;
+    ngx_quic_server_id_t   *sid;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic clear temp server ids");
+
+    for (q = ngx_queue_head(&qc->server_ids);
+         q != ngx_queue_sentinel(&qc->server_ids);
+         q = next)
+    {
+        next = ngx_queue_next(q);
+        sid = ngx_queue_data(q, ngx_quic_server_id_t, queue);
+
+        if (sid->seqnum != NGX_QUIC_UNSET_PN) {
+            continue;
+        }
+
+        ngx_queue_remove(q);
+        ngx_queue_insert_tail(&qc->free_server_ids, &sid->queue);
+        ngx_rbtree_delete(&c->listening->rbtree, &sid->udp.node);
+        qc->nserver_ids--;
+    }
+}
+
+
+static ngx_quic_server_id_t *
+ngx_quic_insert_server_id(ngx_connection_t *c, ngx_str_t *id)
+{
+    ngx_str_t               dcid;
+    ngx_quic_server_id_t   *sid;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    sid = ngx_quic_alloc_server_id(c, qc);
+    if (sid == NULL) {
+        return NULL;
+    }
+
+    sid->seqnum = qc->server_seqnum;
+
+    if (qc->server_seqnum != NGX_QUIC_UNSET_PN) {
+        qc->server_seqnum++;
+    }
+
+    sid->len = id->len;
+    ngx_memcpy(sid->id, id->data, id->len);
+
+    ngx_queue_insert_tail(&qc->server_ids, &sid->queue);
+    qc->nserver_ids++;
+
+    dcid.data = sid->id;
+    dcid.len = sid->len;
+
+    ngx_insert_udp_connection(c, &sid->udp, &dcid);
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic insert server id seqnum:%uL", sid->seqnum);
+
+    ngx_quic_hexdump(c->log, "quic server id", id->data, id->len);
+
+    return sid;
+}
+
+
+static ngx_quic_client_id_t *
+ngx_quic_alloc_client_id(ngx_connection_t *c, ngx_quic_connection_t *qc)
+{
+    ngx_queue_t           *q;
+    ngx_quic_client_id_t  *cid;
+
+    if (!ngx_queue_empty(&qc->free_client_ids)) {
+
+        q = ngx_queue_head(&qc->free_client_ids);
+        cid = ngx_queue_data(q, ngx_quic_client_id_t, queue);
+
+        ngx_queue_remove(&cid->queue);
+
+        ngx_memzero(cid, sizeof(ngx_quic_client_id_t));
+
+    } else {
+
+        cid = ngx_pcalloc(c->pool, sizeof(ngx_quic_client_id_t));
+        if (cid == NULL) {
+            return NULL;
+        }
+    }
+
+    return cid;
+}
+
+
+static ngx_quic_server_id_t *
+ngx_quic_alloc_server_id(ngx_connection_t *c, ngx_quic_connection_t *qc)
+{
+    ngx_queue_t           *q;
+    ngx_quic_server_id_t  *sid;
+
+    if (!ngx_queue_empty(&qc->free_server_ids)) {
+
+        q = ngx_queue_head(&qc->free_server_ids);
+        sid = ngx_queue_data(q, ngx_quic_server_id_t, queue);
+
+        ngx_queue_remove(&sid->queue);
+
+        ngx_memzero(sid, sizeof(ngx_quic_server_id_t));
+
+    } else {
+
+        sid = ngx_pcalloc(c->pool, sizeof(ngx_quic_server_id_t));
+        if (sid == NULL) {
+            return NULL;
+        }
+    }
+
+    return sid;
+}
+
+
+static void
+ngx_quic_queue_frame(ngx_quic_connection_t *qc, ngx_quic_frame_t *frame)
+{
+    ngx_quic_send_ctx_t  *ctx;
+
+    ctx = ngx_quic_get_send_ctx(qc, frame->level);
+
+    ngx_queue_insert_tail(&ctx->frames, &frame->queue);
+
+    frame->len = ngx_quic_create_frame(NULL, frame);
+    /* always succeeds */
+
+    if (qc->closing) {
+        return;
+    }
+
+    ngx_post_event(&qc->push, &ngx_posted_events);
+}
+
+
+static ngx_int_t
+ngx_quic_output(ngx_connection_t *c)
+{
+    ngx_uint_t              i;
+    ngx_msec_t              delay;
+    ngx_quic_send_ctx_t    *ctx;
+    ngx_quic_connection_t  *qc;
+
+    c->log->action = "sending frames";
+
+    qc = ngx_quic_get_connection(c);
+
+    for (i = 0; i < NGX_QUIC_SEND_CTX_LAST; i++) {
+
+        ctx = &qc->send_ctx[i];
+
+        if (ctx->send_ack) {
+
+            if (ctx->level == ssl_encryption_application)  {
+
+                delay = ngx_current_msec - ctx->ack_delay_start;
+
+                if (ctx->send_ack < NGX_QUIC_MAX_ACK_GAP
+                    && delay < qc->tp.max_ack_delay)
+                {
+                    if (!qc->push.timer_set && !qc->closing) {
+                        ngx_add_timer(&qc->push, qc->tp.max_ack_delay - delay);
+                    }
+
+                    goto output;
+                }
+            }
+
+            if (ngx_quic_send_ack(c, ctx) != NGX_OK) {
+                return NGX_ERROR;
+            }
+            ctx->send_ack = 0;
+        }
+
+    output:
+
+        if (ngx_quic_output_frames(c, ctx) != 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);
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_output_frames(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx)
+{
+    size_t                  len, hlen, cutoff;
+    ngx_uint_t              need_ack;
+    ngx_queue_t            *q, range;
+    ngx_quic_frame_t       *f;
+    ngx_quic_congestion_t  *cg;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+    cg = &qc->congestion;
+
+    if (ngx_queue_empty(&ctx->frames)) {
+        return NGX_OK;
+    }
+
+    q = ngx_queue_head(&ctx->frames);
+    f = ngx_queue_data(q, ngx_quic_frame_t, queue);
+
+    /* all frames in same send_ctx share same level */
+    hlen = (f->level == ssl_encryption_application) ? NGX_QUIC_MAX_SHORT_HEADER
+                                                    : NGX_QUIC_MAX_LONG_HEADER;
+    hlen += EVP_GCM_TLS_TAG_LEN;
+    hlen -= NGX_QUIC_MAX_CID_LEN - qc->scid.len;
+
+    do {
+        len = 0;
+        need_ack = 0;
+        ngx_queue_init(&range);
+
+        do {
+            /* process group of frames that fits into packet */
+            f = ngx_queue_data(q, ngx_quic_frame_t, queue);
+
+            if (len && hlen + len + f->len > qc->ctp.max_udp_payload_size) {
+                break;
+            }
+
+            if (f->need_ack) {
+                need_ack = 1;
+            }
+
+            if (need_ack && cg->in_flight + len + f->len > cg->window) {
+                break;
+            }
+
+            if (!qc->validated) {
+                /*
+                 * Prior to validation, endpoints are limited in what they
+                 * are able to send.  During the handshake, a server cannot
+                 * send more than three times the data it receives;
+                 */
+
+                if (f->level == ssl_encryption_initial) {
+                    cutoff = (c->sent + NGX_QUIC_MIN_INITIAL_SIZE) / 3;
+
+                } else {
+                    cutoff = (c->sent + hlen + len + f->len) / 3;
+                }
+
+                if (cutoff > qc->received) {
+                    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                                   "quic hit amplification limit"
+                                   " received:%uz sent:%O",
+                                   qc->received, c->sent);
+                    break;
+                }
+            }
+
+            q = ngx_queue_next(q);
+
+            f->first = ngx_current_msec;
+
+            ngx_queue_remove(&f->queue);
+            ngx_queue_insert_tail(&range, &f->queue);
+
+            len += f->len;
+
+        } while (q != ngx_queue_sentinel(&ctx->frames));
+
+        if (ngx_queue_empty(&range)) {
+            break;
+        }
+
+        if (ngx_quic_send_frames(c, ctx, &range) != NGX_OK) {
+            return NGX_ERROR;
+        }
+
+    } while (q != ngx_queue_sentinel(&ctx->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;
+
+    do {
+        q = ngx_queue_head(frames);
+
+        if (q == ngx_queue_sentinel(frames)) {
+            break;
+        }
+
+        ngx_queue_remove(q);
+
+        f = ngx_queue_data(q, ngx_quic_frame_t, queue);
+
+        ngx_quic_free_frame(c, f);
+    } while (1);
+}
+
+
+static ngx_int_t
+ngx_quic_send_frames(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx,
+    ngx_queue_t *frames)
+{
+    u_char                 *p;
+    size_t                  pad_len;
+    ssize_t                 len;
+    ngx_str_t               out, res;
+    ngx_msec_t              now;
+    ngx_queue_t            *q;
+    ngx_quic_frame_t       *f, *start;
+    ngx_quic_header_t       pkt;
+    ngx_quic_connection_t  *qc;
+    static u_char           src[NGX_QUIC_MAX_UDP_PAYLOAD_SIZE];
+    static u_char           dst[NGX_QUIC_MAX_UDP_PAYLOAD_SIZE];
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic ngx_quic_send_frames");
+
+    q = ngx_queue_head(frames);
+    start = ngx_queue_data(q, ngx_quic_frame_t, queue);
+
+    ngx_memzero(&pkt, sizeof(ngx_quic_header_t));
+
+    now = ngx_current_msec;
+
+    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_quic_log_frame(c->log, f, 1);
+
+        len = ngx_quic_create_frame(p, f);
+        if (len == -1) {
+            ngx_quic_free_frames(c, frames);
+            return NGX_ERROR;
+        }
+
+        if (f->need_ack) {
+            pkt.need_ack = 1;
+        }
+
+        p += len;
+        f->pnum = ctx->pnum;
+        f->last = now;
+        f->plen = 0;
+    }
+
+    out.len = p - out.data;
+
+    qc = ngx_quic_get_connection(c);
+
+    pkt.keys = qc->keys;
+
+    pkt.flags = NGX_QUIC_PKT_FIXED_BIT;
+
+    if (start->level == ssl_encryption_initial) {
+        pkt.flags |= NGX_QUIC_PKT_LONG | NGX_QUIC_PKT_INITIAL;
+
+    } else if (start->level == ssl_encryption_handshake) {
+        pkt.flags |= NGX_QUIC_PKT_LONG | NGX_QUIC_PKT_HANDSHAKE;
+
+    } else {
+        if (qc->key_phase) {
+            pkt.flags |= NGX_QUIC_PKT_KPHASE;
+        }
+    }
+
+    ngx_quic_set_packet_number(&pkt, ctx);
+
+    pkt.version = qc->version;
+    pkt.log = c->log;
+    pkt.level = start->level;
+    pkt.dcid = qc->scid;
+    pkt.scid = qc->dcid;
+
+    if (start->level == ssl_encryption_initial && pkt.need_ack) {
+        pad_len = NGX_QUIC_MIN_INITIAL_SIZE - EVP_GCM_TLS_TAG_LEN
+                  - ngx_quic_create_header(&pkt, NULL, out.len, NULL);
+        pad_len = ngx_min(pad_len, NGX_QUIC_MIN_INITIAL_SIZE);
+
+    } else {
+        pad_len = 4;
+    }
+
+    if (out.len < pad_len) {
+        ngx_memset(p, NGX_QUIC_FT_PADDING, pad_len - out.len);
+        out.len = pad_len;
+    }
+
+    pkt.payload = out;
+
+    res.data = dst;
+
+    ngx_log_debug6(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic packet tx %s bytes:%ui"
+                   " need_ack:%d number:%L encoded nl:%d trunc:0x%xD",
+                   ngx_quic_level_name(start->level), out.len, pkt.need_ack,
+                   pkt.number, pkt.num_len, pkt.trunc);
+
+    if (ngx_quic_encrypt(&pkt, &res) != NGX_OK) {
+        ngx_quic_free_frames(c, frames);
+        return NGX_ERROR;
+    }
+
+    len = c->send(c, res.data, res.len);
+    if (len == NGX_ERROR || (size_t) len != res.len) {
+        ngx_quic_free_frames(c, frames);
+        return NGX_ERROR;
+    }
+
+    /* len == NGX_OK || NGX_AGAIN */
+    ctx->pnum++;
+
+    if (pkt.need_ack) {
+        /* move frames into the sent queue to wait for ack */
+
+        if (qc->closing) {
+            /* if we are closing, any ack will be discarded */
+            ngx_quic_free_frames(c, frames);
+
+        } else {
+            ngx_queue_add(&ctx->sent, frames);
+            if (qc->pto.timer_set) {
+                ngx_del_timer(&qc->pto);
+            }
+            ngx_add_timer(&qc->pto, ngx_quic_pto(c, ctx));
+
+            start->plen = len;
+        }
+
+        qc->congestion.in_flight += len;
+
+        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic congestion send if:%uz",
+                       qc->congestion.in_flight);
+    } else {
+        /* no ack is expected for this frames, so we can free them */
+        ngx_quic_free_frames(c, frames);
+    }
+
+    return NGX_OK;
+}
+
+
+static void
+ngx_quic_set_packet_number(ngx_quic_header_t *pkt, ngx_quic_send_ctx_t *ctx)
+{
+    uint64_t  delta;
+
+    delta = ctx->pnum - ctx->largest_ack;
+    pkt->number = ctx->pnum;
+
+    if (delta <= 0x7F) {
+        pkt->num_len = 1;
+        pkt->trunc = ctx->pnum & 0xff;
+
+    } else if (delta <= 0x7FFF) {
+        pkt->num_len = 2;
+        pkt->flags |= 0x1;
+        pkt->trunc = ctx->pnum & 0xffff;
+
+    } else if (delta <= 0x7FFFFF) {
+        pkt->num_len = 3;
+        pkt->flags |= 0x2;
+        pkt->trunc = ctx->pnum & 0xffffff;
+
+    } else {
+        pkt->num_len = 4;
+        pkt->flags |= 0x3;
+        pkt->trunc = ctx->pnum & 0xffffffff;
+    }
+}
+
+
+static void
+ngx_quic_pto_handler(ngx_event_t *ev)
+{
+    ngx_uint_t              i;
+    ngx_queue_t            *q;
+    ngx_connection_t       *c;
+    ngx_quic_frame_t       *start;
+    ngx_quic_send_ctx_t    *ctx;
+    ngx_quic_connection_t  *qc;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, ev->log, 0, "quic pto timer");
+
+    c = ev->data;
+    qc = ngx_quic_get_connection(c);
+
+    qc->pto_count++;
+
+    for (i = 0; i < NGX_QUIC_SEND_CTX_LAST; i++) {
+
+        ctx = &qc->send_ctx[i];
+
+        if (ngx_queue_empty(&ctx->sent)) {
+            continue;
+        }
+
+        q = ngx_queue_head(&ctx->sent);
+        start = ngx_queue_data(q, ngx_quic_frame_t, queue);
+
+        if (start->pnum <= ctx->largest_ack
+            && ctx->largest_ack != NGX_QUIC_UNSET_PN)
+        {
+            continue;
+        }
+
+        ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic pto pnum:%uL pto_count:%ui level:%d",
+                       start->pnum, qc->pto_count, start->level);
+
+        ngx_quic_resend_frames(c, ctx);
+    }
+
+    ngx_quic_connstate_dbg(c);
+}
+
+
+static void
+ngx_quic_push_handler(ngx_event_t *ev)
+{
+    ngx_connection_t  *c;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, ev->log, 0, "quic push timer");
+
+    c = ev->data;
+
+    if (ngx_quic_output(c) != NGX_OK) {
+        ngx_quic_close_connection(c, NGX_ERROR);
+        return;
+    }
+
+    ngx_quic_connstate_dbg(c);
+}
+
+
+static
+void ngx_quic_lost_handler(ngx_event_t *ev)
+{
+    ngx_connection_t  *c;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, ev->log, 0, "quic lost timer");
+
+    c = ev->data;
+
+    if (ngx_quic_detect_lost(c) != NGX_OK) {
+        ngx_quic_close_connection(c, NGX_ERROR);
+    }
+
+    ngx_quic_connstate_dbg(c);
+}
+
+
+static ngx_int_t
+ngx_quic_detect_lost(ngx_connection_t *c)
+{
+    ngx_uint_t              i;
+    ngx_msec_t              now, wait, min_wait, thr;
+    ngx_queue_t            *q;
+    ngx_quic_frame_t       *start;
+    ngx_quic_send_ctx_t    *ctx;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+    now = ngx_current_msec;
+
+    min_wait = 0;
+
+    thr = NGX_QUIC_TIME_THR * ngx_max(qc->latest_rtt, qc->avg_rtt);
+    thr = ngx_max(thr, NGX_QUIC_TIME_GRANULARITY);
+
+    for (i = 0; i < NGX_QUIC_SEND_CTX_LAST; i++) {
+
+        ctx = &qc->send_ctx[i];
+
+        if (ctx->largest_ack == NGX_QUIC_UNSET_PN) {
+            continue;
+        }
+
+        while (!ngx_queue_empty(&ctx->sent)) {
+
+            q = ngx_queue_head(&ctx->sent);
+            start = ngx_queue_data(q, ngx_quic_frame_t, queue);
+
+            if (start->pnum > ctx->largest_ack) {
+                break;
+            }
+
+            wait = start->last + thr - now;
+
+            ngx_log_debug4(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                           "quic detect_lost pnum:%uL thr:%M wait:%i level:%d",
+                           start->pnum, thr, (ngx_int_t) wait, start->level);
+
+            if ((ngx_msec_int_t) wait > 0
+                && ctx->largest_ack - start->pnum < NGX_QUIC_PKT_THR)
+            {
+
+                if (min_wait == 0 || wait < min_wait) {
+                    min_wait = wait;
+                }
+
+                break;
+            }
+
+            ngx_quic_resend_frames(c, ctx);
+        }
+    }
+
+    /* no more preceeding packets */
+
+    if (min_wait == 0) {
+        qc->pto.handler = ngx_quic_pto_handler;
+        return NGX_OK;
+    }
+
+    qc->pto.handler = ngx_quic_lost_handler;
+
+    if (qc->pto.timer_set) {
+        ngx_del_timer(&qc->pto);
+    }
+
+    ngx_add_timer(&qc->pto, min_wait);
+
+    return NGX_OK;
+}
+
+
+static void
+ngx_quic_resend_frames(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx)
+{
+    size_t                  n;
+    ngx_buf_t              *b;
+    ngx_queue_t            *q;
+    ngx_quic_frame_t       *f, *start;
+    ngx_quic_stream_t      *sn;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+    q = ngx_queue_head(&ctx->sent);
+    start = ngx_queue_data(q, ngx_quic_frame_t, queue);
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic resend packet pnum:%uL", start->pnum);
+
+    ngx_quic_congestion_lost(c, start);
+
+    do {
+        f = ngx_queue_data(q, ngx_quic_frame_t, queue);
+
+        if (f->pnum != start->pnum) {
+            break;
+        }
+
+        q = ngx_queue_next(q);
+
+        ngx_queue_remove(&f->queue);
+
+        switch (f->type) {
+        case NGX_QUIC_FT_ACK:
+        case NGX_QUIC_FT_ACK_ECN:
+            /* force generation of most recent acknowledgment */
+            ctx->send_ack = NGX_QUIC_MAX_ACK_GAP;
+            ngx_quic_free_frame(c, f);
+            break;
+
+        case NGX_QUIC_FT_PING:
+        case NGX_QUIC_FT_PATH_RESPONSE:
+        case NGX_QUIC_FT_CONNECTION_CLOSE:
+            ngx_quic_free_frame(c, f);
+            break;
+
+        case NGX_QUIC_FT_MAX_DATA:
+            f->u.max_data.max_data = qc->streams.recv_max_data;
+            ngx_quic_queue_frame(qc, f);
+            break;
+
+        case NGX_QUIC_FT_MAX_STREAMS:
+        case NGX_QUIC_FT_MAX_STREAMS2:
+            f->u.max_streams.limit = f->u.max_streams.bidi
+                                     ? qc->streams.client_max_streams_bidi
+                                     : qc->streams.client_max_streams_uni;
+            ngx_quic_queue_frame(qc, f);
+            break;
+
+        case NGX_QUIC_FT_MAX_STREAM_DATA:
+            sn = ngx_quic_find_stream(&qc->streams.tree,
+                                      f->u.max_stream_data.id);
+            if (sn == NULL) {
+                ngx_quic_free_frame(c, f);
+                break;
+            }
+
+            b = sn->b;
+            n = sn->fs.received + (b->pos - b->start) + (b->end - b->last);
+
+            if (f->u.max_stream_data.limit < n) {
+                f->u.max_stream_data.limit = n;
+            }
+
+            ngx_quic_queue_frame(qc, f);
+            break;
+
+        case NGX_QUIC_FT_STREAM0:
+        case NGX_QUIC_FT_STREAM1:
+        case NGX_QUIC_FT_STREAM2:
+        case NGX_QUIC_FT_STREAM3:
+        case NGX_QUIC_FT_STREAM4:
+        case NGX_QUIC_FT_STREAM5:
+        case NGX_QUIC_FT_STREAM6:
+        case NGX_QUIC_FT_STREAM7:
+            sn = ngx_quic_find_stream(&qc->streams.tree, f->u.stream.stream_id);
+
+            if (sn && sn->c->write->error) {
+                /* RESET_STREAM was sent */
+                ngx_quic_free_frame(c, f);
+                break;
+            }
+
+            /* fall through */
+
+        default:
+            ngx_queue_insert_tail(&ctx->frames, &f->queue);
+        }
+
+    } while (q != ngx_queue_sentinel(&ctx->sent));
+
+    if (qc->closing) {
+        return;
+    }
+
+    ngx_post_event(&qc->push, &ngx_posted_events);
+}
+
+
+ngx_connection_t *
+ngx_quic_open_stream(ngx_connection_t *c, ngx_uint_t bidi)
+{
+    size_t                  rcvbuf_size;
+    uint64_t                id;
+    ngx_quic_stream_t      *qs, *sn;
+    ngx_quic_connection_t  *qc;
+
+    qs = c->quic;
+    qc = ngx_quic_get_connection(qs->parent);
+
+    if (bidi) {
+        if (qc->streams.server_streams_bidi
+            >= qc->streams.server_max_streams_bidi)
+        {
+            ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                           "quic too many server bidi streams:%uL",
+                           qc->streams.server_streams_bidi);
+            return NULL;
+        }
+
+        id = (qc->streams.server_streams_bidi << 2)
+             | NGX_QUIC_STREAM_SERVER_INITIATED;
+
+        ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic creating server bidi stream"
+                       " streams:%uL max:%uL id:0x%xL",
+                       qc->streams.server_streams_bidi,
+                       qc->streams.server_max_streams_bidi, id);
+
+        qc->streams.server_streams_bidi++;
+        rcvbuf_size = qc->tp.initial_max_stream_data_bidi_local;
+
+    } else {
+        if (qc->streams.server_streams_uni
+            >= qc->streams.server_max_streams_uni)
+        {
+            ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                           "quic too many server uni streams:%uL",
+                           qc->streams.server_streams_uni);
+            return NULL;
+        }
+
+        id = (qc->streams.server_streams_uni << 2)
+             | NGX_QUIC_STREAM_SERVER_INITIATED
+             | NGX_QUIC_STREAM_UNIDIRECTIONAL;
+
+        ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic creating server uni stream"
+                       " streams:%uL max:%uL id:0x%xL",
+                       qc->streams.server_streams_uni,
+                       qc->streams.server_max_streams_uni, id);
+
+        qc->streams.server_streams_uni++;
+        rcvbuf_size = 0;
+    }
+
+    sn = ngx_quic_create_stream(qs->parent, id, rcvbuf_size);
+    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_client_stream(ngx_connection_t *c, uint64_t id)
+{
+    size_t                  n;
+    uint64_t                min_id;
+    ngx_quic_stream_t      *sn;
+    ngx_quic_connection_t  *qc;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic stream id:0x%xL is new", id);
+
+    qc = ngx_quic_get_connection(c);
+
+    if (id & NGX_QUIC_STREAM_UNIDIRECTIONAL) {
+
+        if (id & NGX_QUIC_STREAM_SERVER_INITIATED) {
+            if ((id >> 2) < qc->streams.server_streams_uni) {
+                return NGX_QUIC_STREAM_GONE;
+            }
+
+            qc->error = NGX_QUIC_ERR_STREAM_STATE_ERROR;
+            return NULL;
+        }
+
+        if ((id >> 2) < qc->streams.client_streams_uni) {
+            return NGX_QUIC_STREAM_GONE;
+        }
+
+        if ((id >> 2) >= qc->streams.client_max_streams_uni) {
+            qc->error = NGX_QUIC_ERR_STREAM_LIMIT_ERROR;
+            return NULL;
+        }
+
+        min_id = (qc->streams.client_streams_uni << 2)
+                 | NGX_QUIC_STREAM_UNIDIRECTIONAL;
+        qc->streams.client_streams_uni = (id >> 2) + 1;
+        n = qc->tp.initial_max_stream_data_uni;
+
+    } else {
+
+        if (id & NGX_QUIC_STREAM_SERVER_INITIATED) {
+            if ((id >> 2) < qc->streams.server_streams_bidi) {
+                return NGX_QUIC_STREAM_GONE;
+            }
+
+            qc->error = NGX_QUIC_ERR_STREAM_STATE_ERROR;
+            return NULL;
+        }
+
+        if ((id >> 2) < qc->streams.client_streams_bidi) {
+            return NGX_QUIC_STREAM_GONE;
+        }
+
+        if ((id >> 2) >= qc->streams.client_max_streams_bidi) {
+            qc->error = NGX_QUIC_ERR_STREAM_LIMIT_ERROR;
+            return NULL;
+        }
+
+        min_id = (qc->streams.client_streams_bidi << 2);
+        qc->streams.client_streams_bidi = (id >> 2) + 1;
+        n = qc->tp.initial_max_stream_data_bidi_remote;
+    }
+
+    if (n < NGX_QUIC_STREAM_BUFSIZE) {
+        n = NGX_QUIC_STREAM_BUFSIZE;
+    }
+
+    /*
+     *   2.1.  Stream Types and Identifiers
+     *
+     *   Within each type, streams are created with numerically increasing
+     *   stream IDs.  A stream ID that is used out of order results in all
+     *   streams of that type with lower-numbered stream IDs also being
+     *   opened.
+     */
+
+    for ( /* void */ ; min_id < id; min_id += 0x04) {
+
+        sn = ngx_quic_create_stream(c, min_id, n);
+        if (sn == NULL) {
+            return NULL;
+        }
+
+        sn->c->listening->handler(sn->c);
+    }
+
+    return ngx_quic_create_stream(c, id, n);
+}
+
+
+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;
+    ngx_quic_connection_t  *qc;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic stream id:0x%xL create", id);
+
+    qc = ngx_quic_get_connection(c);
+
+    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;
+    }
+
+    ngx_queue_init(&sn->fs.frames);
+
+    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->quic = sn;
+    sn->c->type = SOCK_STREAM;
+    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->local_socklen = c->local_socklen;
+    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 = log;
+    sn->c->write->log = log;
+
+    log->connection = sn->c->number;
+
+    if ((id & NGX_QUIC_STREAM_UNIDIRECTIONAL) == 0
+        || (id & NGX_QUIC_STREAM_SERVER_INITIATED))
+    {
+        sn->c->write->ready = 1;
+    }
+
+    if (id & NGX_QUIC_STREAM_UNIDIRECTIONAL) {
+        if (id & NGX_QUIC_STREAM_SERVER_INITIATED) {
+            sn->send_max_data = qc->ctp.initial_max_stream_data_uni;
+        }
+
+    } else {
+        if (id & NGX_QUIC_STREAM_SERVER_INITIATED) {
+            sn->send_max_data = qc->ctp.initial_max_stream_data_bidi_remote;
+        } else {
+            sn->send_max_data = qc->ctp.initial_max_stream_data_bidi_local;
+        }
+    }
+
+    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(&qc->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_connection_t       *pc;
+    ngx_quic_frame_t       *frame;
+    ngx_quic_stream_t      *qs;
+    ngx_quic_connection_t  *qc;
+
+    qs = c->quic;
+    b = qs->b;
+    pc = qs->parent;
+    qc = ngx_quic_get_connection(pc);
+    rev = c->read;
+
+    if (rev->error) {
+        return NGX_ERROR;
+    }
+
+    ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic stream recv id:0x%xL eof:%d avail:%z",
+                   qs->id, 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_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic stream id:0x%xL recv() not ready", qs->id);
+        return NGX_AGAIN;
+    }
+
+    len = ngx_min(b->last - b->pos, (ssize_t) size);
+
+    ngx_memcpy(buf, b->pos, len);
+
+    b->pos += len;
+    qc->streams.received += len;
+
+    if (b->pos == b->last) {
+        b->pos = b->start;
+        b->last = b->start;
+        rev->ready = rev->pending_eof;
+    }
+
+    ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic stream id:0x%xL recv len:%z of size:%uz",
+                   qs->id, len, size);
+
+    if (!rev->pending_eof) {
+        frame = ngx_quic_alloc_frame(pc, 0);
+        if (frame == NULL) {
+            return NGX_ERROR;
+        }
+
+        frame->level = ssl_encryption_application;
+        frame->type = NGX_QUIC_FT_MAX_STREAM_DATA;
+        frame->u.max_stream_data.id = qs->id;
+        frame->u.max_stream_data.limit = qs->fs.received + (b->pos - b->start)
+                                         + (b->end - b->last);
+
+        ngx_quic_queue_frame(qc, frame);
+    }
+
+    if ((qc->streams.recv_max_data / 2) < qc->streams.received) {
+
+        frame = ngx_quic_alloc_frame(pc, 0);
+
+        if (frame == NULL) {
+            return NGX_ERROR;
+        }
+
+        qc->streams.recv_max_data *= 2;
+
+        frame->level = ssl_encryption_application;
+        frame->type = NGX_QUIC_FT_MAX_DATA;
+        frame->u.max_data.max_data = qc->streams.recv_max_data;
+
+        ngx_quic_queue_frame(qc, frame);
+
+        ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic stream id:0x%xL recv: increased max_data:%uL",
+                       qs->id, qc->streams.recv_max_data);
+    }
+
+    return len;
+}
+
+
+static ssize_t
+ngx_quic_stream_send(ngx_connection_t *c, u_char *buf, size_t size)
+{
+    ngx_buf_t    b;
+    ngx_chain_t  cl;
+
+    ngx_memzero(&b, sizeof(ngx_buf_t));
+
+    b.memory = 1;
+    b.pos = buf;
+    b.last = buf + size;
+
+    cl.buf = &b;
+    cl.next = NULL;
+
+    if (ngx_quic_stream_send_chain(c, &cl, 0) == NGX_CHAIN_ERROR) {
+        return NGX_ERROR;
+    }
+
+    if (b.pos == buf) {
+        return NGX_AGAIN;
+    }
+
+    return b.pos - buf;
+}
+
+
+static ngx_chain_t *
+ngx_quic_stream_send_chain(ngx_connection_t *c, ngx_chain_t *in, off_t limit)
+{
+    u_char                 *p;
+    size_t                  n, max, max_frame, max_flow, max_limit, len;
+#if (NGX_DEBUG)
+    size_t                  sent;
+#endif
+    ngx_buf_t              *b;
+#if (NGX_DEBUG)
+    ngx_uint_t              nframes;
+#endif
+    ngx_event_t            *wev;
+    ngx_chain_t            *cl;
+    ngx_connection_t       *pc;
+    ngx_quic_frame_t       *frame;
+    ngx_quic_stream_t      *qs;
+    ngx_quic_connection_t  *qc;
+
+    qs = c->quic;
+    pc = qs->parent;
+    qc = ngx_quic_get_connection(pc);
+    wev = c->write;
+
+    if (wev->error) {
+        return NGX_CHAIN_ERROR;
+    }
+
+    max_frame = ngx_quic_max_stream_frame(qc);
+    max_flow = ngx_quic_max_stream_flow(c);
+    max_limit = limit;
+
+#if (NGX_DEBUG)
+    sent = 0;
+    nframes = 0;
+#endif
+
+    for ( ;; ) {
+        max = ngx_min(max_frame, max_flow);
+
+        if (limit) {
+            max = ngx_min(max, max_limit);
+        }
+
+        for (cl = in, n = 0; in; in = in->next) {
+
+            if (!ngx_buf_in_memory(in->buf)) {
+                continue;
+            }
+
+            n += ngx_buf_size(in->buf);
+
+            if (n > max) {
+                n = max;
+                break;
+            }
+        }
+
+        if (n == 0) {
+            wev->ready = (max_flow ? 1 : 0);
+            break;
+        }
+
+        frame = ngx_quic_alloc_frame(pc, n);
+        if (frame == NULL) {
+            return NGX_CHAIN_ERROR;
+        }
+
+        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 = n;
+        frame->u.stream.data = frame->data;
+
+        c->sent += n;
+        qc->streams.sent += n;
+        max_flow -= n;
+
+        if (limit) {
+            max_limit -= n;
+        }
+
+#if (NGX_DEBUG)
+        sent += n;
+        nframes++;
+#endif
+
+        for (p = frame->data; n > 0; cl = cl->next) {
+            b = cl->buf;
+
+            if (!ngx_buf_in_memory(b)) {
+                continue;
+            }
+
+            len = ngx_min(n, (size_t) (b->last - b->pos));
+            p = ngx_cpymem(p, b->pos, len);
+
+            b->pos += len;
+            n -= len;
+        }
+
+        ngx_quic_queue_frame(qc, frame);
+    }
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic send_chain sent:%uz nframes:%ui", sent, nframes);
+
+    return in;
+}
+
+
+static size_t
+ngx_quic_max_stream_frame(ngx_quic_connection_t *qc)
+{
+    /*
+     * we need to fit at least 1 frame into a packet, thus account head/tail;
+     * 25 = 1 + 8x3 is max header for STREAM frame, with 1 byte for frame type
+     */
+
+    return qc->ctp.max_udp_payload_size - NGX_QUIC_MAX_SHORT_HEADER - 25
+           - EVP_GCM_TLS_TAG_LEN;
+}
+
+
+static size_t
+ngx_quic_max_stream_flow(ngx_connection_t *c)
+{
+    size_t                  size;
+    uint64_t                sent, unacked;
+    ngx_quic_stream_t      *qs;
+    ngx_quic_connection_t  *qc;
+
+    qs = c->quic;
+    qc = ngx_quic_get_connection(qs->parent);
+
+    size = NGX_QUIC_STREAM_BUFSIZE;
+    sent = c->sent;
+    unacked = sent - qs->acked;
+
+    if (qc->streams.send_max_data == 0) {
+        qc->streams.send_max_data = qc->ctp.initial_max_data;
+    }
+
+    if (unacked >= NGX_QUIC_STREAM_BUFSIZE) {
+        ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic send flow hit buffer size");
+        return 0;
+    }
+
+    if (unacked + size > NGX_QUIC_STREAM_BUFSIZE) {
+        size = NGX_QUIC_STREAM_BUFSIZE - unacked;
+    }
+
+    if (qc->streams.sent >= qc->streams.send_max_data) {
+        ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic send flow hit MAX_DATA");
+        return 0;
+    }
+
+    if (qc->streams.sent + size > qc->streams.send_max_data) {
+        size = qc->streams.send_max_data - qc->streams.sent;
+    }
+
+    if (sent >= qs->send_max_data) {
+        ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic send flow hit MAX_STREAM_DATA");
+        return 0;
+    }
+
+    if (sent + size > qs->send_max_data) {
+        size = qs->send_max_data - sent;
+    }
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic send flow:%uz", size);
+
+    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->quic;
+    pc = qs->parent;
+    qc = ngx_quic_get_connection(pc);
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic stream id:0x%xL cleanup", qs->id);
+
+    ngx_rbtree_delete(&qc->streams.tree, &qs->node);
+    ngx_quic_free_frames(pc, &qs->fs.frames);
+
+    if (qc->closing) {
+        /* schedule handler call to continue ngx_quic_close_connection() */
+        ngx_post_event(pc->read, &ngx_posted_events);
+        return;
+    }
+
+    if ((qs->id & NGX_QUIC_STREAM_SERVER_INITIATED) == 0
+        || (qs->id & NGX_QUIC_STREAM_UNIDIRECTIONAL) == 0)
+    {
+        if (!c->read->pending_eof && !c->read->error) {
+            frame = ngx_quic_alloc_frame(pc, 0);
+            if (frame == NULL) {
+                return;
+            }
+
+            frame->level = ssl_encryption_application;
+            frame->type = NGX_QUIC_FT_STOP_SENDING;
+            frame->u.stop_sending.id = qs->id;
+            frame->u.stop_sending.error_code = 0x100; /* HTTP/3 no error */
+
+            ngx_quic_queue_frame(qc, frame);
+        }
+    }
+
+    if ((qs->id & NGX_QUIC_STREAM_SERVER_INITIATED) == 0) {
+        frame = ngx_quic_alloc_frame(pc, 0);
+        if (frame == NULL) {
+            return;
+        }
+
+        frame->level = ssl_encryption_application;
+        frame->type = NGX_QUIC_FT_MAX_STREAMS;
+
+        if (qs->id & NGX_QUIC_STREAM_UNIDIRECTIONAL) {
+            frame->u.max_streams.limit = ++qc->streams.client_max_streams_uni;
+            frame->u.max_streams.bidi = 0;
+
+        } else {
+            frame->u.max_streams.limit = ++qc->streams.client_max_streams_bidi;
+            frame->u.max_streams.bidi = 1;
+        }
+
+        ngx_quic_queue_frame(qc, frame);
+
+        if (qs->id & NGX_QUIC_STREAM_UNIDIRECTIONAL) {
+            /* do not send fin for client unidirectional streams */
+            return;
+        }
+    }
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic stream id:0x%xL send fin", qs->id);
+
+    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_quic_queue_frame(qc, frame);
+
+    (void) ngx_quic_output(pc);
+}
+
+
+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 = ngx_quic_get_connection(c);
+
+    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);
+
+#ifdef NGX_QUIC_DEBUG_FRAMES_ALLOC
+        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic reuse frame n:%ui", qc->nframes);
+#endif
+
+    } 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
+
+#ifdef NGX_QUIC_DEBUG_FRAMES_ALLOC
+        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic alloc frame n:%ui", qc->nframes);
+#endif
+    }
+
+    ngx_memzero(frame, sizeof(ngx_quic_frame_t));
+
+    frame->data = p;
+
+    return frame;
+}
+
+
+static void
+ngx_quic_congestion_ack(ngx_connection_t *c, ngx_quic_frame_t *f)
+{
+    ngx_msec_t              timer;
+    ngx_quic_congestion_t  *cg;
+    ngx_quic_connection_t  *qc;
+
+    if (f->plen == 0) {
+        return;
+    }
+
+    qc = ngx_quic_get_connection(c);
+    cg = &qc->congestion;
+
+    cg->in_flight -= f->plen;
+
+    timer = f->last - cg->recovery_start;
+
+    if ((ngx_msec_int_t) timer <= 0) {
+        ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic congestion ack recovery win:%uz ss:%z if:%uz",
+                       cg->window, cg->ssthresh, cg->in_flight);
+
+        return;
+    }
+
+    if (cg->window < cg->ssthresh) {
+        cg->window += f->plen;
+
+        ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic congestion slow start win:%uz ss:%z if:%uz",
+                       cg->window, cg->ssthresh, cg->in_flight);
+
+    } else {
+        cg->window += qc->tp.max_udp_payload_size * f->plen / cg->window;
+
+        ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic congestion avoidance win:%uz ss:%z if:%uz",
+                       cg->window, cg->ssthresh, cg->in_flight);
+    }
+
+    /* prevent recovery_start from wrapping */
+
+    timer = cg->recovery_start - ngx_current_msec + qc->tp.max_idle_timeout * 2;
+
+    if ((ngx_msec_int_t) timer < 0) {
+        cg->recovery_start = ngx_current_msec - qc->tp.max_idle_timeout * 2;
+    }
+}
+
+
+static void
+ngx_quic_congestion_lost(ngx_connection_t *c, ngx_quic_frame_t *f)
+{
+    ngx_msec_t              timer;
+    ngx_quic_congestion_t  *cg;
+    ngx_quic_connection_t  *qc;
+
+    if (f->plen == 0) {
+        return;
+    }
+
+    qc = ngx_quic_get_connection(c);
+    cg = &qc->congestion;
+
+    cg->in_flight -= f->plen;
+    f->plen = 0;
+
+    timer = f->last - cg->recovery_start;
+
+    if ((ngx_msec_int_t) timer <= 0) {
+        ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic congestion lost recovery win:%uz ss:%z if:%uz",
+                       cg->window, cg->ssthresh, cg->in_flight);
+
+        return;
+    }
+
+    cg->recovery_start = ngx_current_msec;
+    cg->window /= 2;
+
+    if (cg->window < qc->tp.max_udp_payload_size * 2) {
+        cg->window = qc->tp.max_udp_payload_size * 2;
+    }
+
+    cg->ssthresh = cg->window;
+
+    ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic congestion lost win:%uz ss:%z if:%uz",
+                   cg->window, cg->ssthresh, cg->in_flight);
+}
+
+
+static void
+ngx_quic_free_frame(ngx_connection_t *c, ngx_quic_frame_t *frame)
+{
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    if (frame->data) {
+        ngx_free(frame->data);
+        frame->data = NULL;
+    }
+
+    ngx_queue_insert_head(&qc->free_frames, &frame->queue);
+
+#ifdef NGX_QUIC_DEBUG_FRAMES_ALLOC
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic free frame n:%ui", qc->nframes);
+#endif
+}
+
+
+uint32_t
+ngx_quic_version(ngx_connection_t *c)
+{
+    uint32_t                version;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    version = qc->version;
+
+    return (version & 0xff000000) == 0xff000000 ? version & 0xff : version;
+}
new file mode 100644
--- /dev/null
+++ b/src/event/ngx_event_quic.h
@@ -0,0 +1,167 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_EVENT_QUIC_H_INCLUDED_
+#define _NGX_EVENT_QUIC_H_INCLUDED_
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+
+/* Supported drafts: 27, 28, 29 */
+#ifndef NGX_QUIC_DRAFT_VERSION
+#define NGX_QUIC_DRAFT_VERSION               29
+#endif
+
+#define NGX_QUIC_MAX_SHORT_HEADER            25 /* 1 flags + 20 dcid + 4 pn */
+#define NGX_QUIC_MAX_LONG_HEADER             56
+    /* 1 flags + 4 version + 2 x (1 + 20) s/dcid + 4 pn + 4 len + token len */
+
+#define NGX_QUIC_MAX_UDP_PAYLOAD_SIZE        65527
+#define NGX_QUIC_MAX_UDP_PAYLOAD_OUT         1252
+#define NGX_QUIC_MAX_UDP_PAYLOAD_OUT6        1232
+
+#define NGX_QUIC_DEFAULT_ACK_DELAY_EXPONENT  3
+#define NGX_QUIC_DEFAULT_MAX_ACK_DELAY       25
+#define NGX_QUIC_DEFAULT_SRT_KEY_LEN         32
+
+#define NGX_QUIC_RETRY_TIMEOUT               3000
+#define NGX_QUIC_RETRY_LIFETIME              30000
+#define NGX_QUIC_RETRY_BUFFER_SIZE           128
+    /* 1 flags + 4 version + 3 x (1 + 20) s/o/dcid + itag + token(44) */
+#define NGX_QUIC_MAX_TOKEN_SIZE              32
+    /* sizeof(struct in6_addr) + sizeof(ngx_msec_t) up to AES-256 block size */
+
+/* quic-recovery, section 6.2.2, kInitialRtt */
+#define NGX_QUIC_INITIAL_RTT                 333 /* ms */
+
+/* quic-recovery, section 6.1.1, Packet Threshold */
+#define NGX_QUIC_PKT_THR                     3 /* packets */
+/* quic-recovery, section 6.1.2, Time Threshold */
+#define NGX_QUIC_TIME_THR                    1.125
+#define NGX_QUIC_TIME_GRANULARITY            1 /* ms */
+
+#define NGX_QUIC_CC_MIN_INTERVAL             1000 /* 1s */
+
+#define NGX_QUIC_MIN_INITIAL_SIZE            1200
+
+#define NGX_QUIC_STREAM_SERVER_INITIATED     0x01
+#define NGX_QUIC_STREAM_UNIDIRECTIONAL       0x02
+
+#define NGX_QUIC_STREAM_BUFSIZE              65536
+
+#define NGX_QUIC_MAX_CID_LEN                 20
+#define NGX_QUIC_SERVER_CID_LEN              NGX_QUIC_MAX_CID_LEN
+
+#define NGX_QUIC_SR_TOKEN_LEN                16
+
+#define NGX_QUIC_MAX_SERVER_IDS              8
+
+#define ngx_quic_get_connection(c)           ((ngx_quic_connection_t *)(c)->udp)
+
+
+typedef struct {
+    /* configurable */
+    ngx_msec_t                 max_idle_timeout;
+    ngx_msec_t                 max_ack_delay;
+
+    size_t                     max_udp_payload_size;
+    size_t                     initial_max_data;
+    size_t                     initial_max_stream_data_bidi_local;
+    size_t                     initial_max_stream_data_bidi_remote;
+    size_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                 active_connection_id_limit;
+    ngx_flag_t                 disable_active_migration;
+    ngx_str_t                  original_dcid;
+    ngx_str_t                  initial_scid;
+    ngx_str_t                  retry_scid;
+    u_char                     sr_token[NGX_QUIC_SR_TOKEN_LEN];
+
+    /* TODO */
+    void                      *preferred_address;
+} ngx_quic_tp_t;
+
+
+typedef struct {
+    ngx_ssl_t                 *ssl;
+    ngx_quic_tp_t              tp;
+    ngx_flag_t                 retry;
+    ngx_flag_t                 require_alpn;
+    u_char                     token_key[32]; /* AES 256 */
+    ngx_str_t                  sr_token_key; /* stateless reset token key */
+} ngx_quic_conf_t;
+
+
+typedef struct {
+    uint64_t                   sent;
+    uint64_t                   received;
+    ngx_queue_t                frames;   /* reorder queue */
+    size_t                     total;    /* size of buffered data */
+} ngx_quic_frames_stream_t;
+
+
+struct ngx_quic_stream_s {
+    ngx_rbtree_node_t          node;
+    ngx_connection_t          *parent;
+    ngx_connection_t          *c;
+    uint64_t                   id;
+    uint64_t                   acked;
+    uint64_t                   send_max_data;
+    ngx_buf_t                 *b;
+    ngx_quic_frames_stream_t   fs;
+};
+
+
+typedef struct ngx_quic_keys_s  ngx_quic_keys_t;
+
+
+void ngx_quic_run(ngx_connection_t *c, ngx_quic_conf_t *conf);
+ngx_connection_t *ngx_quic_open_stream(ngx_connection_t *c, ngx_uint_t bidi);
+void ngx_quic_finalize_connection(ngx_connection_t *c, ngx_uint_t err,
+    const char *reason);
+uint32_t ngx_quic_version(ngx_connection_t *c);
+ngx_int_t ngx_quic_get_packet_dcid(ngx_log_t *log, u_char *data, size_t len,
+    ngx_str_t *dcid);
+
+
+/********************************* DEBUG *************************************/
+
+/* #define NGX_QUIC_DEBUG_PACKETS */      /* dump packet contents */
+/* #define NGX_QUIC_DEBUG_FRAMES */       /* dump frames contents */
+/* #define NGX_QUIC_DEBUG_FRAMES_ALLOC */ /* log frames alloc/reuse/free */
+/* #define NGX_QUIC_DEBUG_CRYPTO */
+
+#if (NGX_DEBUG)
+
+#define ngx_quic_hexdump(log, fmt, data, len)                                 \
+    ngx_quic_hexdump_real(log, fmt, (u_char *) data, (size_t) len)
+
+static ngx_inline
+void ngx_quic_hexdump_real(ngx_log_t *log, const char *label, u_char *data,
+    size_t len)
+{
+    ngx_int_t  m;
+    u_char     buf[2048];
+
+    if (log->log_level & NGX_LOG_DEBUG_EVENT) {
+        m = ngx_hex_dump(buf, data, (len > 1024) ? 1024 : len) - buf;
+        ngx_log_debug5(NGX_LOG_DEBUG_EVENT, log, 0,
+                      "%s len:%uz data:%*s%s",
+                      label, len, m, buf, len < 2048 ? "" : "...");
+    }
+}
+
+#else
+
+#define ngx_quic_hexdump(log, fmt, data, len)
+
+#endif
+
+#endif /* _NGX_EVENT_QUIC_H_INCLUDED_ */
new file mode 100644
--- /dev/null
+++ b/src/event/ngx_event_quic_protection.c
@@ -0,0 +1,1185 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_event.h>
+#include <ngx_event_quic_transport.h>
+#include <ngx_event_quic_protection.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;
+
+
+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;
+
+
+struct ngx_quic_keys_s {
+    ngx_quic_secrets_t        secrets[NGX_QUIC_ENCRYPTION_LAST];
+    ngx_quic_secrets_t        next_key;
+    ngx_uint_t                cipher;
+};
+
+
+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,
+    uint64_t *largest_pn);
+static void ngx_quic_compute_nonce(u_char *nonce, size_t len, uint64_t pn);
+static ngx_int_t ngx_quic_ciphers(ngx_uint_t id,
+    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 ngx_int_t ngx_quic_create_packet(ngx_quic_header_t *pkt,
+    ngx_str_t *res);
+static ngx_int_t ngx_quic_create_retry_packet(ngx_quic_header_t *pkt,
+    ngx_str_t *res);
+
+
+static ngx_int_t
+ngx_quic_ciphers(ngx_uint_t id, ngx_quic_ciphers_t *ciphers,
+    enum ssl_encryption_level_t level)
+{
+    ngx_int_t  len;
+
+    if (level == ssl_encryption_initial) {
+        id = NGX_AES_128_GCM_SHA256;
+    }
+
+    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_keys_set_initial_secret(ngx_pool_t *pool, ngx_quic_keys_t *keys,
+    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;
+    ngx_quic_secret_t  *client, *server;
+
+    static const uint8_t salt[20] =
+#if (NGX_QUIC_DRAFT_VERSION >= 29)
+        "\xaf\xbf\xec\x28\x99\x93\xd2\x4c\x9e\x97"
+        "\x86\xf1\x9c\x61\x11\xe0\x43\x90\xa8\x99";
+#else
+        "\xc3\xee\xf7\x12\xc7\x2e\xbb\x5a\x11\xa7"
+        "\xd2\x43\x2b\xb4\x63\x65\xbe\xf9\xf5\x02";
+#endif
+
+    client = &keys->secrets[ssl_encryption_initial].client;
+    server = &keys->secrets[ssl_encryption_initial].server;
+
+    /* 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_log_debug0(NGX_LOG_DEBUG_EVENT, pool->log, 0,
+                  "quic ngx_quic_set_initial_secret");
+#ifdef NGX_QUIC_DEBUG_CRYPTO
+    ngx_quic_hexdump(pool->log, "quic salt", salt, sizeof(salt));
+    ngx_quic_hexdump(pool->log, "quic initial secret", is, is_len);
+#endif
+
+    /* 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;
+    }
+
+#ifdef NGX_QUIC_DEBUG_CRYPTO
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, pool->log, 0, "quic expand %V", label);
+    ngx_quic_hexdump(pool->log, "quic key", out->data, out->len);
+#endif
+
+    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_keys_set_encryption_secret(ngx_pool_t *pool, ngx_uint_t is_write,
+    ngx_quic_keys_t *keys, enum ssl_encryption_level_t level,
+    const SSL_CIPHER *cipher, const uint8_t *secret, size_t secret_len)
+{
+    ngx_int_t            key_len;
+    ngx_uint_t           i;
+    ngx_quic_secret_t   *peer_secret;
+    ngx_quic_ciphers_t   ciphers;
+
+    peer_secret = is_write ? &keys->secrets[level].server
+                           : &keys->secrets[level].client;
+
+    /*
+     * SSL_CIPHER_get_protocol_id() is not universally available,
+     * casting to uint16_t works for both OpenSSL and BoringSSL
+     */
+    keys->cipher = (uint16_t) SSL_CIPHER_get_id(cipher);
+
+    key_len = ngx_quic_ciphers(keys->cipher, &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_quic_keys_t *
+ngx_quic_keys_new(ngx_pool_t *pool)
+{
+    return ngx_pcalloc(pool, sizeof(ngx_quic_keys_t));
+}
+
+
+ngx_uint_t
+ngx_quic_keys_available(ngx_quic_keys_t *keys,
+    enum ssl_encryption_level_t level)
+{
+    return keys->secrets[level].client.key.len != 0;
+}
+
+
+void
+ngx_quic_keys_discard(ngx_quic_keys_t *keys,
+     enum ssl_encryption_level_t level)
+{
+    keys->secrets[level].client.key.len = 0;
+}
+
+
+void
+ngx_quic_keys_switch(ngx_connection_t *c, ngx_quic_keys_t *keys)
+{
+    ngx_quic_secrets_t  *current, *next, tmp;
+
+    current = &keys->secrets[ssl_encryption_application];
+    next = &keys->next_key;
+
+    tmp = *current;
+    *current = *next;
+    *next = tmp;
+}
+
+
+ngx_int_t
+ngx_quic_keys_update(ngx_connection_t *c, ngx_quic_keys_t *keys)
+{
+    ngx_uint_t           i;
+    ngx_quic_ciphers_t   ciphers;
+    ngx_quic_secrets_t  *current, *next;
+
+    current = &keys->secrets[ssl_encryption_application];
+    next = &keys->next_key;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, "quic key update");
+
+    if (ngx_quic_ciphers(keys->cipher, &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 ngx_int_t
+ngx_quic_create_packet(ngx_quic_header_t *pkt, ngx_str_t *res)
+{
+    u_char              *pnp, *sample;
+    ngx_str_t            ad, out;
+    ngx_uint_t           i;
+    ngx_quic_secret_t   *secret;
+    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_header(pkt, ad.data, out.len, &pnp);
+
+    out.data = res->data + ad.len;
+
+#ifdef NGX_QUIC_DEBUG_CRYPTO
+    ngx_quic_hexdump(pkt->log, "quic ad", ad.data, ad.len);
+#endif
+
+    if (ngx_quic_ciphers(pkt->keys->cipher, &ciphers, pkt->level) == NGX_ERROR)
+    {
+        return NGX_ERROR;
+    }
+
+    secret = &pkt->keys->secrets[pkt->level].server;
+
+    ngx_memcpy(nonce, secret->iv.data, secret->iv.len);
+    ngx_quic_compute_nonce(nonce, sizeof(nonce), pkt->number);
+
+    if (ngx_quic_tls_seal(ciphers.c, 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, secret, mask, sample)
+        != NGX_OK)
+    {
+        return NGX_ERROR;
+    }
+
+    /* quic-tls: 5.4.1.  Header Protection Application */
+    ad.data[0] ^= mask[0] & ngx_quic_pkt_hp_mask(pkt->flags);
+
+    for (i = 0; i < pkt->num_len; i++) {
+        pnp[i] ^= mask[i + 1];
+    }
+
+    res->len = ad.len + out.len;
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_create_retry_packet(ngx_quic_header_t *pkt, ngx_str_t *res)
+{
+    u_char              *start;
+    ngx_str_t            ad, itag;
+    ngx_quic_secret_t    secret;
+    ngx_quic_ciphers_t   ciphers;
+
+    /* 5.8.  Retry Packet Integrity */
+    static u_char     key[16] =
+#if (NGX_QUIC_DRAFT_VERSION >= 29)
+        "\xcc\xce\x18\x7e\xd0\x9a\x09\xd0\x57\x28\x15\x5a\x6c\xb9\x6b\xe1";
+#else
+        "\x4d\x32\xec\xdb\x2a\x21\x33\xc8\x41\xe4\x04\x3d\xf2\x7d\x44\x30";
+#endif
+    static u_char     nonce[12] =
+#if (NGX_QUIC_DRAFT_VERSION >= 29)
+        "\xe5\x49\x30\xf9\x7f\x21\x36\xf0\x53\x0a\x8c\x1c";
+#else
+        "\x4d\x16\x11\xd0\x55\x13\xa5\x52\xc5\x87\xd5\x75";
+#endif
+    static ngx_str_t  in = ngx_string("");
+
+    ad.data = res->data;
+    ad.len = ngx_quic_create_retry_itag(pkt, ad.data, &start);
+
+    itag.data = ad.data + ad.len;
+    itag.len = EVP_GCM_TLS_TAG_LEN;
+
+#ifdef NGX_QUIC_DEBUG_CRYPTO
+    ngx_quic_hexdump(pkt->log, "quic retry itag", ad.data, ad.len);
+#endif
+
+    if (ngx_quic_ciphers(0, &ciphers, pkt->level) == NGX_ERROR) {
+        return NGX_ERROR;
+    }
+
+    secret.key.len = sizeof(key);
+    secret.key.data = key;
+    secret.iv.len = sizeof(nonce);
+
+    if (ngx_quic_tls_seal(ciphers.c, &secret, &itag, nonce, &in, &ad, pkt->log)
+        != NGX_OK)
+    {
+        return NGX_ERROR;
+    }
+
+    res->len = itag.data + itag.len - start;
+    res->data = start;
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_quic_new_sr_token(ngx_connection_t *c, ngx_str_t *cid, ngx_str_t *secret,
+    u_char *token)
+{
+    uint8_t       *p;
+    size_t         is_len, key_len, info_len;
+    ngx_str_t      label;
+    const EVP_MD  *digest;
+    uint8_t       info[20];
+    uint8_t       is[SHA256_DIGEST_LENGTH];
+    uint8_t       key[SHA256_DIGEST_LENGTH];
+
+    /* 10.4.2.  Calculating a Stateless Reset Token */
+
+    digest = EVP_sha256();
+    ngx_str_set(&label, "sr_token_key");
+
+    if (ngx_hkdf_extract(is, &is_len, digest, secret->data, secret->len,
+                         cid->data, cid->len)
+       != NGX_OK)
+    {
+        ngx_ssl_error(NGX_LOG_INFO, c->log, 0,
+                      "ngx_hkdf_extract(%V) failed", &label);
+        return NGX_ERROR;
+    }
+
+    key_len = SHA256_DIGEST_LENGTH;
+
+    info_len = 2 + 1 + label.len + 1;
+
+    info[0] = 0;
+    info[1] = key_len;
+    info[2] = label.len;
+
+    p = ngx_cpymem(&info[3], label.data, label.len);
+    *p = '\0';
+
+    if (ngx_hkdf_expand(key, key_len, digest, is, is_len, info, info_len)
+        != NGX_OK)
+    {
+        ngx_ssl_error(NGX_LOG_INFO, c->log, 0,
+                      "ngx_hkdf_expand(%V) failed", &label);
+        return NGX_ERROR;
+    }
+
+    ngx_memcpy(token, key, NGX_QUIC_SR_TOKEN_LEN);
+
+#if (NGX_DEBUG)
+    ngx_quic_hexdump(c->log, "quic stateless reset token", token,
+                     (size_t) NGX_QUIC_SR_TOKEN_LEN);
+#endif
+
+    return NGX_OK;
+}
+
+
+static uint64_t
+ngx_quic_parse_pn(u_char **pos, ngx_int_t len, u_char *mask,
+    uint64_t *largest_pn)
+{
+    u_char    *p;
+    uint64_t   truncated_pn, expected_pn, candidate_pn;
+    uint64_t   pn_nbits, pn_win, pn_hwin, pn_mask;
+
+    pn_nbits = ngx_min(len * 8, 62);
+
+    p = *pos;
+    truncated_pn = *p++ ^ *mask++;
+
+    while (--len) {
+        truncated_pn = (truncated_pn << 8) + (*p++ ^ *mask++);
+    }
+
+    *pos = p;
+
+    expected_pn = *largest_pn + 1;
+    pn_win = 1ULL << pn_nbits;
+    pn_hwin = pn_win / 2;
+    pn_mask = pn_win - 1;
+
+    candidate_pn = (expected_pn & ~pn_mask) | truncated_pn;
+
+    if ((int64_t) candidate_pn <= (int64_t) (expected_pn - pn_hwin)
+        && candidate_pn < (1ULL << 62) - pn_win)
+    {
+        candidate_pn += pn_win;
+
+    } else if (candidate_pn > expected_pn + pn_hwin
+               && candidate_pn >= pn_win)
+    {
+        candidate_pn -= pn_win;
+    }
+
+    *largest_pn = ngx_max((int64_t) *largest_pn, (int64_t) candidate_pn);
+
+    return candidate_pn;
+}
+
+
+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);
+}
+
+
+ngx_int_t
+ngx_quic_encrypt(ngx_quic_header_t *pkt, ngx_str_t *res)
+{
+    if (ngx_quic_pkt_retry(pkt->flags)) {
+        return ngx_quic_create_retry_packet(pkt, res);
+    }
+
+    return ngx_quic_create_packet(pkt, res);
+}
+
+
+ngx_int_t
+ngx_quic_decrypt(ngx_quic_header_t *pkt, uint64_t *largest_pn)
+{
+    u_char              *p, *sample;
+    size_t               len;
+    uint64_t             pn, lpn;
+    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(pkt->keys->cipher, &ciphers, pkt->level) == NGX_ERROR)
+    {
+        return NGX_ERROR;
+    }
+
+    secret = &pkt->keys->secrets[pkt->level].client;
+
+    p = pkt->raw->pos;
+    len = pkt->data + pkt->len - p;
+
+    /* 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
+     */
+
+    if (len < EVP_GCM_TLS_TAG_LEN + 4) {
+        return NGX_DECLINED;
+    }
+
+    sample = p + 4;
+
+    /* header protection */
+
+    if (ngx_quic_tls_hp(pkt->log, ciphers.hp, secret, mask, sample)
+        != NGX_OK)
+    {
+        return NGX_DECLINED;
+    }
+
+    pkt->flags ^= mask[0] & ngx_quic_pkt_hp_mask(pkt->flags);
+
+    if (ngx_quic_short_pkt(pkt->flags)) {
+        key_phase = (pkt->flags & NGX_QUIC_PKT_KPHASE) != 0;
+
+        if (key_phase != pkt->key_phase) {
+            secret = &pkt->keys->next_key.client;
+            pkt->key_update = 1;
+        }
+    }
+
+    lpn = *largest_pn;
+
+    pnl = (pkt->flags & 0x03) + 1;
+    pn = ngx_quic_parse_pn(&p, pnl, &mask[1], &lpn);
+
+    pkt->pn = pn;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, pkt->log, 0,
+                   "quic packet rx clearflags:%xd", pkt->flags);
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, pkt->log, 0,
+                   "quic packet rx number:%uL len:%xi", pn, pnl);
+
+    /* packet protection */
+
+    in.data = p;
+    in.len = len - pnl;
+
+    ad.len = p - pkt->data;
+    ad.data = pkt->plaintext;
+
+    ngx_memcpy(ad.data, pkt->data, ad.len);
+    ad.data[0] = pkt->flags;
+
+    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);
+
+#ifdef NGX_QUIC_DEBUG_CRYPTO
+    ngx_quic_hexdump(pkt->log, "quic ad", ad.data, ad.len);
+#endif
+
+    pkt->payload.len = in.len - EVP_GCM_TLS_TAG_LEN;
+    pkt->payload.data = pkt->plaintext + ad.len;
+
+    rc = ngx_quic_tls_open(ciphers.c, secret, &pkt->payload,
+                           nonce, &in, &ad, pkt->log);
+    if (rc != NGX_OK) {
+        return NGX_DECLINED;
+    }
+
+    if (pkt->payload.len == 0) {
+        /*
+         * An endpoint MUST treat receipt of a packet containing no
+         * frames as a connection error of type PROTOCOL_VIOLATION.
+         */
+        ngx_log_error(NGX_LOG_INFO, pkt->log, 0, "quic zero-length packet");
+        pkt->error = NGX_QUIC_ERR_PROTOCOL_VIOLATION;
+        return NGX_ERROR;
+    }
+
+    if (pkt->flags & ngx_quic_pkt_rb_mask(pkt->flags)) {
+        /*
+         * An endpoint MUST treat receipt of a packet that has
+         * a non-zero value for these bits, after removing both
+         * packet and header protection, as a connection error
+         * of type PROTOCOL_VIOLATION.
+         */
+        ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                      "quic reserved bit set in packet");
+        pkt->error = NGX_QUIC_ERR_PROTOCOL_VIOLATION;
+        return NGX_ERROR;
+    }
+
+#if defined(NGX_QUIC_DEBUG_CRYPTO) && defined(NGX_QUIC_DEBUG_PACKETS)
+    ngx_quic_hexdump(pkt->log, "quic packet payload",
+                     pkt->payload.data, pkt->payload.len);
+#endif
+
+    *largest_pn = lpn;
+
+    return NGX_OK;
+}
+
new file mode 100644
--- /dev/null
+++ b/src/event/ngx_event_quic_protection.h
@@ -0,0 +1,38 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_EVENT_QUIC_PROTECTION_H_INCLUDED_
+#define _NGX_EVENT_QUIC_PROTECTION_H_INCLUDED_
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+
+#define NGX_QUIC_ENCRYPTION_LAST  ((ssl_encryption_application) + 1)
+
+
+ngx_quic_keys_t *ngx_quic_keys_new(ngx_pool_t *pool);
+ngx_int_t ngx_quic_keys_set_initial_secret(ngx_pool_t *pool,
+    ngx_quic_keys_t *keys, ngx_str_t *secret);
+int ngx_quic_keys_set_encryption_secret(ngx_pool_t *pool, ngx_uint_t is_write,
+    ngx_quic_keys_t *keys, enum ssl_encryption_level_t level,
+    const SSL_CIPHER *cipher, const uint8_t *secret, size_t secret_len);
+ngx_uint_t ngx_quic_keys_available(ngx_quic_keys_t *keys,
+     enum ssl_encryption_level_t level);
+void ngx_quic_keys_discard(ngx_quic_keys_t *keys,
+     enum ssl_encryption_level_t level);
+void ngx_quic_keys_switch(ngx_connection_t *c, ngx_quic_keys_t *keys);
+ngx_int_t ngx_quic_keys_update(ngx_connection_t *c, ngx_quic_keys_t *keys);
+
+ngx_int_t ngx_quic_new_sr_token(ngx_connection_t *c, ngx_str_t *cid,
+    ngx_str_t *key, u_char *token);
+
+ngx_int_t ngx_quic_encrypt(ngx_quic_header_t *pkt, ngx_str_t *res);
+ngx_int_t ngx_quic_decrypt(ngx_quic_header_t *pkt, uint64_t *largest_pn);
+
+
+#endif /* _NGX_EVENT_QUIC_PROTECTION_H_INCLUDED_ */
new file mode 100644
--- /dev/null
+++ b/src/event/ngx_event_quic_transport.c
@@ -0,0 +1,1949 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_event.h>
+#include <ngx_event_quic_transport.h>
+
+
+#define NGX_QUIC_LONG_DCID_LEN_OFFSET  5
+#define NGX_QUIC_LONG_DCID_OFFSET      6
+#define NGX_QUIC_SHORT_DCID_OFFSET     1
+
+
+#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)
+
+#define NGX_QUIC_VERSION(c)       (0xff000000 + (c))
+
+
+static u_char *ngx_quic_parse_int(u_char *pos, u_char *end, uint64_t *out);
+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_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 ngx_int_t ngx_quic_parse_short_header(ngx_quic_header_t *pkt,
+    size_t dcid_len);
+static ngx_int_t ngx_quic_parse_long_header(ngx_quic_header_t *pkt);
+static ngx_int_t ngx_quic_supported_version(uint32_t version);
+static ngx_int_t ngx_quic_parse_long_header_v1(ngx_quic_header_t *pkt);
+
+static size_t ngx_quic_create_long_header(ngx_quic_header_t *pkt, u_char *out,
+    size_t pkt_len, u_char **pnp);
+static size_t ngx_quic_create_short_header(ngx_quic_header_t *pkt, u_char *out,
+    size_t pkt_len, u_char **pnp);
+
+static ngx_int_t ngx_quic_frame_allowed(ngx_quic_header_t *pkt,
+    ngx_uint_t frame_type);
+static size_t ngx_quic_create_ack(u_char *p, ngx_quic_ack_frame_t *ack);
+static size_t ngx_quic_create_stop_sending(u_char *p,
+    ngx_quic_stop_sending_frame_t *ss);
+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_new_token(u_char *p,
+    ngx_quic_new_token_frame_t *token);
+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_max_data(u_char *p,
+    ngx_quic_max_data_frame_t *md);
+static size_t ngx_quic_create_path_response(u_char *p,
+    ngx_quic_path_challenge_frame_t *pc);
+static size_t ngx_quic_create_new_connection_id(u_char *p,
+    ngx_quic_new_conn_id_frame_t *rcid);
+static size_t ngx_quic_create_retire_connection_id(u_char *p,
+    ngx_quic_retire_cid_frame_t *rcid);
+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);
+
+
+uint32_t  ngx_quic_versions[] = {
+#if (NGX_QUIC_DRAFT_VERSION >= 29)
+    /* pretend we support all versions in range draft-29..v1 */
+    NGX_QUIC_VERSION(29),
+    NGX_QUIC_VERSION(30),
+    NGX_QUIC_VERSION(31),
+    NGX_QUIC_VERSION(32),
+    /* QUICv1 */
+    0x00000001
+#else
+    NGX_QUIC_VERSION(NGX_QUIC_DRAFT_VERSION)
+#endif
+};
+
+#define NGX_QUIC_NVERSIONS \
+    (sizeof(ngx_quic_versions) / sizeof(ngx_quic_versions[0]))
+
+
+/* literal errors indexed by corresponding value */
+static char *ngx_quic_errors[] = {
+    "NO_ERROR",
+    "INTERNAL_ERROR",
+    "CONNECTION_REFUSED",
+    "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",
+    "APPLICATION_ERROR",
+    "CRYPTO_BUFFER_EXCEEDED",
+    "KEY_UPDATE_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) {
+        return NULL;
+    }
+
+    p = pos;
+    len = 1 << (*p >> 6);
+
+    value = *p++ & 0x3f;
+
+    if ((size_t)(end - p) < (len - 1)) {
+        return NULL;
+    }
+
+    while (--len) {
+        value = (value << 8) + *p++;
+    }
+
+    *out = value;
+
+    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_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   bits, len;
+
+    p = *pos;
+    bits = 0;
+
+    while (value >> ((8 << bits) - 2)) {
+        bits++;
+    }
+
+    len = (1 << bits);
+
+    while (len--) {
+        *p++ = value >> (len * 8);
+    }
+
+    **pos |= bits << 6;
+    *pos = p;
+}
+
+
+u_char *
+ngx_quic_error_text(uint64_t error_code)
+{
+    if (error_code >= NGX_QUIC_ERR_CRYPTO_ERROR) {
+        return (u_char *) "handshake error";
+    }
+
+    if (error_code >= NGX_QUIC_ERR_LAST) {
+        return (u_char *) "unknown error";
+    }
+
+    return (u_char *) ngx_quic_errors[error_code];
+}
+
+
+ngx_int_t
+ngx_quic_parse_packet(ngx_quic_header_t *pkt)
+{
+    if (!ngx_quic_long_pkt(pkt->flags)) {
+        pkt->level = ssl_encryption_application;
+
+        if (ngx_quic_parse_short_header(pkt, NGX_QUIC_SERVER_CID_LEN) != NGX_OK)
+        {
+            return NGX_DECLINED;
+        }
+
+        return NGX_OK;
+    }
+
+    if (ngx_quic_parse_long_header(pkt) != NGX_OK) {
+        return NGX_DECLINED;
+    }
+
+    if (!ngx_quic_supported_version(pkt->version)) {
+        return NGX_ABORT;
+    }
+
+    if (ngx_quic_parse_long_header_v1(pkt) != NGX_OK) {
+        return NGX_DECLINED;
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_parse_short_header(ngx_quic_header_t *pkt, size_t dcid_len)
+{
+    u_char  *p, *end;
+
+    p = pkt->raw->pos;
+    end = pkt->data + pkt->len;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, pkt->log, 0,
+                   "quic packet rx short flags:%xd", pkt->flags);
+
+    if (!(pkt->flags & NGX_QUIC_PKT_FIXED_BIT)) {
+        ngx_log_error(NGX_LOG_INFO, pkt->log, 0, "quic fixed bit is not set");
+        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,
+                      "quic packet is too small to read dcid");
+        return NGX_ERROR;
+    }
+
+    pkt->raw->pos = p;
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_parse_long_header(ngx_quic_header_t *pkt)
+{
+    u_char   *p, *end;
+    uint8_t   idlen;
+
+    p = pkt->raw->pos;
+    end = pkt->data + pkt->len;
+
+    p = ngx_quic_read_uint32(p, end, &pkt->version);
+    if (p == NULL) {
+        ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                      "quic packet is too small to read version");
+        return NGX_ERROR;
+    }
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, pkt->log, 0,
+                   "quic packet rx long flags:%xd version:%xD",
+                   pkt->flags, pkt->version);
+
+    if (!(pkt->flags & NGX_QUIC_PKT_FIXED_BIT)) {
+        ngx_log_error(NGX_LOG_INFO, pkt->log, 0, "quic fixed bit is not set");
+        return NGX_ERROR;
+    }
+
+    p = ngx_quic_read_uint8(p, end, &idlen);
+    if (p == NULL) {
+        ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                      "quic packet is too small to read dcid len");
+        return NGX_ERROR;
+    }
+
+    if (idlen > NGX_QUIC_CID_LEN_MAX) {
+        ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                      "quic packet dcid is too long");
+        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,
+                      "quic 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,
+                      "quic packet is too small to read scid len");
+        return NGX_ERROR;
+    }
+
+    if (idlen > NGX_QUIC_CID_LEN_MAX) {
+        ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                      "quic packet scid is too long");
+        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,
+                      "quic packet is too small to read scid");
+        return NGX_ERROR;
+    }
+
+    pkt->raw->pos = p;
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_supported_version(uint32_t version)
+{
+    ngx_uint_t  i;
+
+    for (i = 0; i < NGX_QUIC_NVERSIONS; i++) {
+        if (ngx_quic_versions[i] == version) {
+            return 1;
+        }
+    }
+
+    return 0;
+}
+
+
+static ngx_int_t
+ngx_quic_parse_long_header_v1(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 long header";
+
+    if (ngx_quic_pkt_in(pkt->flags)) {
+
+        if (pkt->len < NGX_QUIC_MIN_INITIAL_SIZE) {
+            ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                          "quic UDP datagram is too small for initial packet");
+            return NGX_DECLINED;
+        }
+
+        p = ngx_quic_parse_int(p, end, &varint);
+        if (p == NULL) {
+            ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                          "quic 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,
+                          "quic packet too small to read token data");
+            return NGX_ERROR;
+        }
+
+        pkt->level = ssl_encryption_initial;
+
+    } else if (ngx_quic_pkt_zrtt(pkt->flags)) {
+        pkt->level = ssl_encryption_early_data;
+
+    } else if (ngx_quic_pkt_hs(pkt->flags)) {
+        pkt->level = ssl_encryption_handshake;
+
+    } else {
+         ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                       "quic bad packet type");
+         return NGX_DECLINED;
+    }
+
+    p = ngx_quic_parse_int(p, end, &varint);
+    if (p == NULL) {
+        ngx_log_error(NGX_LOG_INFO, pkt->log, 0, "quic bad packet length");
+        return NGX_ERROR;
+    }
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, pkt->log, 0,
+                   "quic packet rx %s len:%uL",
+                   ngx_quic_level_name(pkt->level), varint);
+
+    if (varint > (uint64_t) ((pkt->data + pkt->len) - p)) {
+        ngx_log_error(NGX_LOG_INFO, pkt->log, 0, "quic truncated %s packet",
+                      ngx_quic_level_name(pkt->level));
+        return NGX_ERROR;
+    }
+
+    pkt->raw->pos = p;
+    pkt->len = p + varint - pkt->data;
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_quic_get_packet_dcid(ngx_log_t *log, u_char *data, size_t n,
+    ngx_str_t *dcid)
+{
+    size_t  len, offset;
+
+    if (n == 0) {
+        goto failed;
+    }
+
+    if (ngx_quic_long_pkt(*data)) {
+        if (n < NGX_QUIC_LONG_DCID_LEN_OFFSET + 1) {
+            goto failed;
+        }
+
+        len = data[NGX_QUIC_LONG_DCID_LEN_OFFSET];
+        offset = NGX_QUIC_LONG_DCID_OFFSET;
+
+    } else {
+        len = NGX_QUIC_SERVER_CID_LEN;
+        offset = NGX_QUIC_SHORT_DCID_OFFSET;
+    }
+
+    if (n < len + offset) {
+        goto failed;
+    }
+
+    dcid->len = len;
+    dcid->data = &data[offset];
+
+    return NGX_OK;
+
+failed:
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, log, 0, "quic malformed packet");
+
+    return NGX_ERROR;
+}
+
+
+size_t
+ngx_quic_create_version_negotiation(ngx_quic_header_t *pkt, u_char *out)
+{
+    u_char      *p, *start;
+    ngx_uint_t   i;
+
+    p = start = out;
+
+    *p++ = pkt->flags;
+
+    /*
+     * The Version field of a Version Negotiation packet
+     * MUST be set to 0x00000000
+     */
+    p = ngx_quic_write_uint32(p, 0);
+
+    *p++ = pkt->dcid.len;
+    p = ngx_cpymem(p, pkt->dcid.data, pkt->dcid.len);
+
+    *p++ = pkt->scid.len;
+    p = ngx_cpymem(p, pkt->scid.data, pkt->scid.len);
+
+    for (i = 0; i < NGX_QUIC_NVERSIONS; i++) {
+        p = ngx_quic_write_uint32(p, ngx_quic_versions[i]);
+    }
+
+    return p - start;
+}
+
+
+size_t
+ngx_quic_create_header(ngx_quic_header_t *pkt, u_char *out, size_t pkt_len,
+    u_char **pnp)
+{
+    return ngx_quic_short_pkt(pkt->flags)
+           ? ngx_quic_create_short_header(pkt, out, pkt_len, pnp)
+           : ngx_quic_create_long_header(pkt, out, pkt_len, pnp);
+}
+
+
+static 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;
+
+    if (out == NULL) {
+        return 5 + 2 + pkt->dcid.len + pkt->scid.len
+               + ngx_quic_varint_len(pkt_len + pkt->num_len) + pkt->num_len
+               + (pkt->level == ssl_encryption_initial ? 1 : 0);
+    }
+
+    p = start = out;
+
+    *p++ = pkt->flags;
+
+    p = ngx_quic_write_uint32(p, pkt->version);
+
+    *p++ = pkt->dcid.len;
+    p = ngx_cpymem(p, pkt->dcid.data, pkt->dcid.len);
+
+    *p++ = pkt->scid.len;
+    p = ngx_cpymem(p, pkt->scid.data, pkt->scid.len);
+
+    if (pkt->level == ssl_encryption_initial) {
+        ngx_quic_build_int(&p, 0);
+    }
+
+    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;
+}
+
+
+static 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;
+
+    if (out == NULL) {
+        return 1 + pkt->dcid.len + pkt->num_len;
+    }
+
+    p = start = out;
+
+    *p++ = pkt->flags;
+
+    p = ngx_cpymem(p, pkt->dcid.data, pkt->dcid.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_retry_itag(ngx_quic_header_t *pkt, u_char *out,
+    u_char **start)
+{
+    u_char  *p;
+
+    p = out;
+
+    *p++ = pkt->odcid.len;
+    p = ngx_cpymem(p, pkt->odcid.data, pkt->odcid.len);
+
+    *start = p;
+
+    *p++ = 0xff;
+
+    p = ngx_quic_write_uint32(p, pkt->version);
+
+    *p++ = pkt->dcid.len;
+    p = ngx_cpymem(p, pkt->dcid.data, pkt->dcid.len);
+
+    *p++ = pkt->scid.len;
+    p = ngx_cpymem(p, pkt->scid.data, pkt->scid.len);
+
+    p = ngx_cpymem(p, pkt->token.data, pkt->token.len);
+
+    return p - out;
+}
+
+
+#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;
+    uint64_t     varint;
+    ngx_uint_t   i;
+
+    p = start;
+
+    p = ngx_quic_parse_int(p, end, &varint);
+    if (p == NULL) {
+        pkt->error = NGX_QUIC_ERR_FRAME_ENCODING_ERROR;
+        ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                     "quic failed to obtain quic frame type");
+        return NGX_ERROR;
+    }
+
+    f->type = varint;
+
+    if (ngx_quic_frame_allowed(pkt, f->type) != NGX_OK) {
+        pkt->error = NGX_QUIC_ERR_PROTOCOL_VIOLATION;
+        return NGX_ERROR;
+    }
+
+    switch (f->type) {
+
+    case NGX_QUIC_FT_CRYPTO:
+
+        p = ngx_quic_parse_int(p, end, &f->u.crypto.offset);
+        if (p == NULL) {
+            goto error;
+        }
+
+        p = ngx_quic_parse_int(p, end, &f->u.crypto.length);
+        if (p == NULL) {
+            goto error;
+        }
+
+        p = ngx_quic_read_bytes(p, end, f->u.crypto.length, &f->u.crypto.data);
+        if (p == NULL) {
+            goto error;
+        }
+
+        break;
+
+    case NGX_QUIC_FT_PADDING:
+
+        while (p < end && *p == NGX_QUIC_FT_PADDING) {
+            p++;
+        }
+
+        break;
+
+    case NGX_QUIC_FT_ACK:
+    case NGX_QUIC_FT_ACK_ECN:
+
+        if (!((p = ngx_quic_parse_int(p, end, &f->u.ack.largest))
+              && (p = ngx_quic_parse_int(p, end, &f->u.ack.delay))
+              && (p = ngx_quic_parse_int(p, end, &f->u.ack.range_count))
+              && (p = ngx_quic_parse_int(p, end, &f->u.ack.first_range))))
+        {
+            goto 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(p, end, &varint);
+            if (p) {
+                p = ngx_quic_parse_int(p, end, &varint);
+            }
+
+            if (p == NULL) {
+                goto error;
+            }
+        }
+
+        f->u.ack.ranges_end = p;
+
+        if (f->type == NGX_QUIC_FT_ACK_ECN) {
+
+            if (!((p = ngx_quic_parse_int(p, end, &f->u.ack.ect0))
+                  && (p = ngx_quic_parse_int(p, end, &f->u.ack.ect1))
+                  && (p = ngx_quic_parse_int(p, end, &f->u.ack.ce))))
+            {
+                goto error;
+            }
+
+            ngx_log_debug3(NGX_LOG_DEBUG_EVENT, pkt->log, 0,
+                           "quic ACK ECN counters ect0:%uL ect1:%uL ce:%uL",
+                           f->u.ack.ect0, f->u.ack.ect1, f->u.ack.ce);
+        }
+
+        break;
+
+    case NGX_QUIC_FT_PING:
+        break;
+
+    case NGX_QUIC_FT_NEW_CONNECTION_ID:
+
+        p = ngx_quic_parse_int(p, end, &f->u.ncid.seqnum);
+        if (p == NULL) {
+            goto error;
+        }
+
+        p = ngx_quic_parse_int(p, end, &f->u.ncid.retire);
+        if (p == NULL) {
+            goto error;
+        }
+
+        if (f->u.ncid.retire > f->u.ncid.seqnum) {
+            goto error;
+        }
+
+        p = ngx_quic_read_uint8(p, end, &f->u.ncid.len);
+        if (p == NULL) {
+            goto error;
+        }
+
+        if (f->u.ncid.len < 1 || f->u.ncid.len > NGX_QUIC_CID_LEN_MAX) {
+            goto error;
+        }
+
+        p = ngx_quic_copy_bytes(p, end, f->u.ncid.len, f->u.ncid.cid);
+        if (p == NULL) {
+            goto error;
+        }
+
+        p = ngx_quic_copy_bytes(p, end, NGX_QUIC_SR_TOKEN_LEN, f->u.ncid.srt);
+        if (p == NULL) {
+            goto error;
+        }
+
+        break;
+
+    case NGX_QUIC_FT_RETIRE_CONNECTION_ID:
+
+        p = ngx_quic_parse_int(p, end, &f->u.retire_cid.sequence_number);
+        if (p == NULL) {
+            goto error;
+        }
+
+        break;
+
+    case NGX_QUIC_FT_CONNECTION_CLOSE:
+    case NGX_QUIC_FT_CONNECTION_CLOSE_APP:
+
+        p = ngx_quic_parse_int(p, end, &f->u.close.error_code);
+        if (p == NULL) {
+            goto error;
+        }
+
+        if (f->type == NGX_QUIC_FT_CONNECTION_CLOSE) {
+            p = ngx_quic_parse_int(p, end, &f->u.close.frame_type);
+            if (p == NULL) {
+                goto error;
+            }
+        }
+
+        p = ngx_quic_parse_int(p, end, &varint);
+        if (p == NULL) {
+            goto 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) {
+            goto error;
+        }
+
+        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:
+
+        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) {
+            goto error;
+        }
+
+        if (f->type & 0x04) {
+            p = ngx_quic_parse_int(p, end, &f->u.stream.offset);
+            if (p == NULL) {
+                goto 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) {
+                goto 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) {
+            goto error;
+        }
+
+#ifdef NGX_QUIC_DEBUG_FRAMES
+        ngx_quic_hexdump(pkt->log, "quic STREAM frame",
+                         f->u.stream.data, f->u.stream.length);
+#endif
+        break;
+
+    case NGX_QUIC_FT_MAX_DATA:
+
+        p = ngx_quic_parse_int(p, end, &f->u.max_data.max_data);
+        if (p == NULL) {
+            goto error;
+        }
+
+        break;
+
+    case NGX_QUIC_FT_RESET_STREAM:
+
+        if (!((p = ngx_quic_parse_int(p, end, &f->u.reset_stream.id))
+              && (p = ngx_quic_parse_int(p, end, &f->u.reset_stream.error_code))
+              && (p = ngx_quic_parse_int(p, end,
+                                         &f->u.reset_stream.final_size))))
+        {
+            goto error;
+        }
+
+        break;
+
+    case NGX_QUIC_FT_STOP_SENDING:
+
+        p = ngx_quic_parse_int(p, end, &f->u.stop_sending.id);
+        if (p == NULL) {
+            goto error;
+        }
+
+        p = ngx_quic_parse_int(p, end, &f->u.stop_sending.error_code);
+        if (p == NULL) {
+            goto error;
+        }
+
+        break;
+
+    case NGX_QUIC_FT_STREAMS_BLOCKED:
+    case NGX_QUIC_FT_STREAMS_BLOCKED2:
+
+        p = ngx_quic_parse_int(p, end, &f->u.streams_blocked.limit);
+        if (p == NULL) {
+            goto error;
+        }
+
+        f->u.streams_blocked.bidi =
+                              (f->type == NGX_QUIC_FT_STREAMS_BLOCKED) ? 1 : 0;
+        break;
+
+    case NGX_QUIC_FT_MAX_STREAMS:
+    case NGX_QUIC_FT_MAX_STREAMS2:
+
+        p = ngx_quic_parse_int(p, end, &f->u.max_streams.limit);
+        if (p == NULL) {
+            goto error;
+        }
+
+        f->u.max_streams.bidi = (f->type == NGX_QUIC_FT_MAX_STREAMS) ? 1 : 0;
+
+        break;
+
+    case NGX_QUIC_FT_MAX_STREAM_DATA:
+
+        p = ngx_quic_parse_int(p, end, &f->u.max_stream_data.id);
+        if (p == NULL) {
+            goto error;
+        }
+
+        p = ngx_quic_parse_int(p, end,  &f->u.max_stream_data.limit);
+        if (p == NULL) {
+            goto error;
+        }
+
+        break;
+
+    case NGX_QUIC_FT_DATA_BLOCKED:
+
+        p = ngx_quic_parse_int(p, end, &f->u.data_blocked.limit);
+        if (p == NULL) {
+            goto error;
+        }
+
+        break;
+
+    case NGX_QUIC_FT_STREAM_DATA_BLOCKED:
+
+        p = ngx_quic_parse_int(p, end, &f->u.stream_data_blocked.id);
+        if (p == NULL) {
+            goto error;
+        }
+
+        p = ngx_quic_parse_int(p, end, &f->u.stream_data_blocked.limit);
+        if (p == NULL) {
+            goto error;
+        }
+
+        break;
+
+    case NGX_QUIC_FT_PATH_CHALLENGE:
+
+        p = ngx_quic_copy_bytes(p, end, 8, f->u.path_challenge.data);
+        if (p == NULL) {
+            goto error;
+        }
+
+        break;
+
+    case NGX_QUIC_FT_PATH_RESPONSE:
+
+        p = ngx_quic_copy_bytes(p, end, 8, f->u.path_response.data);
+        if (p == NULL) {
+            goto error;
+        }
+
+        break;
+
+    default:
+        ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                      "quic unknown frame type 0x%xi", f->type);
+        return NGX_ERROR;
+    }
+
+    f->level = pkt->level;
+
+    return p - start;
+
+error:
+
+    pkt->error = NGX_QUIC_ERR_FRAME_ENCODING_ERROR;
+
+    ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                  "quic failed to parse frame type:0x%xi", f->type);
+
+    return NGX_ERROR;
+}
+
+
+static ngx_int_t
+ngx_quic_frame_allowed(ngx_quic_header_t *pkt, ngx_uint_t frame_type)
+{
+    uint8_t  ptype;
+
+    /* frame permissions per packet: 4 bits: IH01: 12.4, Table 3 */
+    static uint8_t ngx_quic_frame_masks[] = {
+         /* PADDING  */              0xF,
+         /* PING */                  0xF,
+         /* ACK */                   0xD,
+         /* ACK_ECN */               0xD,
+         /* RESET_STREAM */          0x3,
+         /* STOP_SENDING */          0x3,
+         /* CRYPTO */                0xD,
+         /* NEW_TOKEN */             0x0, /* only sent by server */
+         /* STREAM0 */               0x3,
+         /* STREAM1 */               0x3,
+         /* STREAM2 */               0x3,
+         /* STREAM3 */               0x3,
+         /* STREAM4 */               0x3,
+         /* STREAM5 */               0x3,
+         /* STREAM6 */               0x3,
+         /* STREAM7 */               0x3,
+         /* MAX_DATA */              0x3,
+         /* MAX_STREAM_DATA */       0x3,
+         /* MAX_STREAMS */           0x3,
+         /* MAX_STREAMS2 */          0x3,
+         /* DATA_BLOCKED */          0x3,
+         /* STREAM_DATA_BLOCKED */   0x3,
+         /* STREAMS_BLOCKED */       0x3,
+         /* STREAMS_BLOCKED2 */      0x3,
+         /* NEW_CONNECTION_ID */     0x3,
+         /* RETIRE_CONNECTION_ID */  0x3,
+         /* PATH_CHALLENGE */        0x3,
+         /* PATH_RESPONSE */         0x3,
+#if (NGX_QUIC_DRAFT_VERSION >= 28)
+         /* CONNECTION_CLOSE */      0xF,
+         /* CONNECTION_CLOSE2 */     0x3,
+#else
+         /* CONNECTION_CLOSE */      0xD,
+         /* CONNECTION_CLOSE2 */     0x1,
+#endif
+         /* HANDSHAKE_DONE */        0x0, /* only sent by server */
+    };
+
+    if (ngx_quic_long_pkt(pkt->flags)) {
+
+        if (ngx_quic_pkt_in(pkt->flags)) {
+            ptype = 8; /* initial */
+
+        } else if (ngx_quic_pkt_hs(pkt->flags)) {
+            ptype = 4; /* handshake */
+
+        } else {
+            ptype = 2; /* zero-rtt */
+        }
+
+    } else {
+        ptype = 1; /* application data */
+    }
+
+    if (ptype & ngx_quic_frame_masks[frame_type]) {
+        return NGX_OK;
+    }
+
+    ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                  "quic frame type 0x%xi is not "
+                  "allowed in packet with flags 0x%xd",
+                  frame_type, pkt->flags);
+
+    return NGX_DECLINED;
+}
+
+
+ssize_t
+ngx_quic_parse_ack_range(ngx_log_t *log, 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, log, 0,
+                      "quic 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, log, 0,
+                      "quic failed to parse ack frame range");
+        return NGX_ERROR;
+    }
+
+    return p - start;
+}
+
+
+size_t
+ngx_quic_create_ack_range(u_char *p, uint64_t gap, uint64_t range)
+{
+    size_t   len;
+    u_char  *start;
+
+    if (p == NULL) {
+        len = ngx_quic_varint_len(gap);
+        len += ngx_quic_varint_len(range);
+        return len;
+    }
+
+    start = p;
+
+    ngx_quic_build_int(&p, gap);
+    ngx_quic_build_int(&p, 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_STOP_SENDING:
+        return ngx_quic_create_stop_sending(p, &f->u.stop_sending);
+
+    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_NEW_TOKEN:
+        return ngx_quic_create_new_token(p, &f->u.token);
+
+    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:
+    case NGX_QUIC_FT_CONNECTION_CLOSE_APP:
+        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);
+
+    case NGX_QUIC_FT_MAX_DATA:
+        return ngx_quic_create_max_data(p, &f->u.max_data);
+
+    case NGX_QUIC_FT_PATH_RESPONSE:
+        return ngx_quic_create_path_response(p, &f->u.path_response);
+
+    case NGX_QUIC_FT_NEW_CONNECTION_ID:
+        return ngx_quic_create_new_connection_id(p, &f->u.ncid);
+
+    case NGX_QUIC_FT_RETIRE_CONNECTION_ID:
+        return ngx_quic_create_retire_connection_id(p, &f->u.retire_cid);
+
+    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;
+
+    if (p == NULL) {
+        len = ngx_quic_varint_len(NGX_QUIC_FT_ACK);
+        len += ngx_quic_varint_len(ack->largest);
+        len += ngx_quic_varint_len(ack->delay);
+        len += ngx_quic_varint_len(ack->range_count);
+        len += ngx_quic_varint_len(ack->first_range);
+        len += ack->ranges_end - ack->ranges_start;
+
+        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, ack->delay);
+    ngx_quic_build_int(&p, ack->range_count);
+    ngx_quic_build_int(&p, ack->first_range);
+    p = ngx_cpymem(p, ack->ranges_start, ack->ranges_end - ack->ranges_start);
+
+    return p - start;
+}
+
+
+static size_t
+ngx_quic_create_stop_sending(u_char *p, ngx_quic_stop_sending_frame_t *ss)
+{
+    size_t   len;
+    u_char  *start;
+
+    if (p == NULL) {
+        len = ngx_quic_varint_len(NGX_QUIC_FT_STOP_SENDING);
+        len += ngx_quic_varint_len(ss->id);
+        len += ngx_quic_varint_len(ss->error_code);
+        return len;
+    }
+
+    start = p;
+
+    ngx_quic_build_int(&p, NGX_QUIC_FT_STOP_SENDING);
+    ngx_quic_build_int(&p, ss->id);
+    ngx_quic_build_int(&p, ss->error_code);
+
+    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->length);
+        len += crypto->length;
+
+        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->length);
+    p = ngx_cpymem(p, crypto->data, crypto->length);
+
+    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_new_token(u_char *p, ngx_quic_new_token_frame_t *token)
+{
+    size_t   len;
+    u_char  *start;
+
+    if (p == NULL) {
+        len = ngx_quic_varint_len(NGX_QUIC_FT_NEW_TOKEN);
+        len += ngx_quic_varint_len(token->length);
+        len += token->length;
+
+        return len;
+    }
+
+    start = p;
+
+    ngx_quic_build_int(&p, NGX_QUIC_FT_NEW_TOKEN);
+    ngx_quic_build_int(&p, token->length);
+    p = ngx_cpymem(p, token->data, token->length);
+
+    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 (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;
+    ngx_str_t  str;
+
+    varint = 0;
+    ngx_str_null(&str);
+
+    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_UDP_PAYLOAD_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;
+
+    case NGX_QUIC_TP_INITIAL_SCID:
+
+        str.len = end - p;
+        str.data = p;
+        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_UDP_PAYLOAD_SIZE:
+        dst->max_udp_payload_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;
+
+    case NGX_QUIC_TP_INITIAL_SCID:
+        dst->initial_scid = str;
+        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)
+{
+    uint64_t   id, len;
+    ngx_int_t  rc;
+
+    while (p < end) {
+        p = ngx_quic_parse_int(p, end, &id);
+        if (p == NULL) {
+            ngx_log_error(NGX_LOG_INFO, log, 0,
+                          "quic failed to parse transport param id");
+            return NGX_ERROR;
+        }
+
+        switch (id) {
+        case NGX_QUIC_TP_ORIGINAL_DCID:
+        case NGX_QUIC_TP_PREFERRED_ADDRESS:
+        case NGX_QUIC_TP_RETRY_SCID:
+        case NGX_QUIC_TP_SR_TOKEN:
+            ngx_log_error(NGX_LOG_INFO, log, 0,
+                          "quic client sent forbidden transport param"
+                          " id:0x%xL", id);
+            return NGX_ERROR;
+        }
+
+        p = ngx_quic_parse_int(p, end, &len);
+        if (p == NULL) {
+            ngx_log_error(NGX_LOG_INFO, log, 0,
+                         "quic failed to parse"
+                         " transport param id:0x%xL 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,
+                          "quic failed to parse"
+                          " transport param id:0x%xL data", id);
+            return NGX_ERROR;
+        }
+
+        if (rc == NGX_DECLINED) {
+            ngx_log_error(NGX_LOG_INFO, log, 0,
+                          "quic unknown transport param id:0x%xL, skipped", id);
+        }
+
+        p += len;
+    }
+
+    if (p != end) {
+        ngx_log_error(NGX_LOG_INFO, log, 0,
+                      "quic trailing garbage in"
+                      " transport parameters: bytes:%ui",
+                      end - p);
+        return NGX_ERROR;
+    }
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, log, 0,
+                   "quic transport parameters parsed ok");
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, log, 0,
+                   "quic tp disable active migration: %ui",
+                   tp->disable_active_migration);
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, log, 0, "quic tp idle_timeout:%ui",
+                   tp->max_idle_timeout);
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, log, 0,
+                   "quic tp max_udp_payload_size:%ui",
+                   tp->max_udp_payload_size);
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, log, 0, "quic tp max_data:%ui",
+                   tp->initial_max_data);
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, log, 0,
+                   "quic tp max_stream_data_bidi_local:%ui",
+                   tp->initial_max_stream_data_bidi_local);
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, log, 0,
+                   "quic tp max_stream_data_bidi_remote:%ui",
+                   tp->initial_max_stream_data_bidi_remote);
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, log, 0,
+                   "quic tp max_stream_data_uni:%ui",
+                   tp->initial_max_stream_data_uni);
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, log, 0,
+                   "quic tp initial_max_streams_bidi:%ui",
+                   tp->initial_max_streams_bidi);
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, log, 0,
+                   "quic tp initial_max_streams_uni:%ui",
+                   tp->initial_max_streams_uni);
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, log, 0,
+                   "quic tp ack_delay_exponent:%ui",
+                   tp->ack_delay_exponent);
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, log, 0, "quic tp max_ack_delay:%ui",
+                   tp->max_ack_delay);
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, log, 0,
+                   "quic tp active_connection_id_limit:%ui",
+                   tp->active_connection_id_limit);
+
+#if (NGX_QUIC_DRAFT_VERSION >= 28)
+    ngx_quic_hexdump(log, "quic tp initial_source_connection_id:",
+                     tp->initial_scid.data, tp->initial_scid.len);
+#endif
+
+    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;
+}
+
+
+static size_t
+ngx_quic_create_max_data(u_char *p, ngx_quic_max_data_frame_t *md)
+{
+    size_t   len;
+    u_char  *start;
+
+    if (p == NULL) {
+        len = ngx_quic_varint_len(NGX_QUIC_FT_MAX_DATA);
+        len += ngx_quic_varint_len(md->max_data);
+        return len;
+    }
+
+    start = p;
+
+    ngx_quic_build_int(&p, NGX_QUIC_FT_MAX_DATA);
+    ngx_quic_build_int(&p, md->max_data);
+
+    return p - start;
+}
+
+
+static size_t
+ngx_quic_create_path_response(u_char *p, ngx_quic_path_challenge_frame_t *pc)
+{
+    size_t   len;
+    u_char  *start;
+
+    if (p == NULL) {
+        len = ngx_quic_varint_len(NGX_QUIC_FT_PATH_RESPONSE);
+        len += sizeof(pc->data);
+        return len;
+    }
+
+    start = p;
+
+    ngx_quic_build_int(&p, NGX_QUIC_FT_PATH_RESPONSE);
+    p = ngx_cpymem(p, &pc->data, sizeof(pc->data));
+
+    return p - start;
+}
+
+
+static size_t
+ngx_quic_create_new_connection_id(u_char *p, ngx_quic_new_conn_id_frame_t *ncid)
+{
+    size_t   len;
+    u_char  *start;
+
+    if (p == NULL) {
+        len = ngx_quic_varint_len(NGX_QUIC_FT_NEW_CONNECTION_ID);
+        len += ngx_quic_varint_len(ncid->seqnum);
+        len += ngx_quic_varint_len(ncid->retire);
+        len++;
+        len += ncid->len;
+        len += NGX_QUIC_SR_TOKEN_LEN;
+        return len;
+    }
+
+    start = p;
+
+    ngx_quic_build_int(&p, NGX_QUIC_FT_NEW_CONNECTION_ID);
+    ngx_quic_build_int(&p, ncid->seqnum);
+    ngx_quic_build_int(&p, ncid->retire);
+    *p++ = ncid->len;
+    p = ngx_cpymem(p, ncid->cid, ncid->len);
+    p = ngx_cpymem(p, ncid->srt, NGX_QUIC_SR_TOKEN_LEN);
+
+    return p - start;
+}
+
+
+static size_t
+ngx_quic_create_retire_connection_id(u_char *p,
+    ngx_quic_retire_cid_frame_t *rcid)
+{
+    size_t   len;
+    u_char  *start;
+
+    if (p == NULL) {
+        len = ngx_quic_varint_len(NGX_QUIC_FT_RETIRE_CONNECTION_ID);
+        len += ngx_quic_varint_len(rcid->sequence_number);
+        return len;
+    }
+
+    start = p;
+
+    ngx_quic_build_int(&p, NGX_QUIC_FT_RETIRE_CONNECTION_ID);
+    ngx_quic_build_int(&p, rcid->sequence_number);
+
+    return p - start;
+}
+
+
+ssize_t
+ngx_quic_create_transport_params(u_char *pos, u_char *end, ngx_quic_tp_t *tp,
+    size_t *clen)
+{
+    u_char  *p;
+    size_t   len;
+
+#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)
+
+#define ngx_quic_tp_strlen(id, value)                                         \
+    ngx_quic_varint_len(id)                                                   \
+    + ngx_quic_varint_len(value.len)                                          \
+    + value.len
+
+#define ngx_quic_tp_str(id, value)                                            \
+    do {                                                                      \
+        ngx_quic_build_int(&p, id);                                           \
+        ngx_quic_build_int(&p, value.len);                                    \
+        p = ngx_cpymem(p, value.data, value.len);                             \
+    } while (0)
+
+    p = pos;
+
+    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 (clen) {
+        *clen = len;
+    }
+
+    if (tp->disable_active_migration) {
+        len += ngx_quic_varint_len(NGX_QUIC_TP_DISABLE_ACTIVE_MIGRATION);
+        len += ngx_quic_varint_len(0);
+    }
+
+    len += ngx_quic_tp_len(NGX_QUIC_TP_ACTIVE_CONNECTION_ID_LIMIT,
+                           tp->active_connection_id_limit);
+
+#if (NGX_QUIC_DRAFT_VERSION >= 28)
+    len += ngx_quic_tp_strlen(NGX_QUIC_TP_ORIGINAL_DCID, tp->original_dcid);
+    len += ngx_quic_tp_strlen(NGX_QUIC_TP_INITIAL_SCID, tp->initial_scid);
+
+    if (tp->retry_scid.len) {
+        len += ngx_quic_tp_strlen(NGX_QUIC_TP_RETRY_SCID, tp->retry_scid);
+    }
+#else
+    if (tp->original_dcid.len) {
+        len += ngx_quic_tp_strlen(NGX_QUIC_TP_ORIGINAL_DCID, tp->original_dcid);
+    }
+#endif
+
+    len += ngx_quic_varint_len(NGX_QUIC_TP_SR_TOKEN);
+    len += ngx_quic_varint_len(NGX_QUIC_SR_TOKEN_LEN);
+    len += NGX_QUIC_SR_TOKEN_LEN;
+
+    if (pos == NULL) {
+        return len;
+    }
+
+    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);
+
+    if (tp->disable_active_migration) {
+        ngx_quic_build_int(&p, NGX_QUIC_TP_DISABLE_ACTIVE_MIGRATION);
+        ngx_quic_build_int(&p, 0);
+    }
+
+    ngx_quic_tp_vint(NGX_QUIC_TP_ACTIVE_CONNECTION_ID_LIMIT,
+                     tp->active_connection_id_limit);
+
+#if (NGX_QUIC_DRAFT_VERSION >= 28)
+    ngx_quic_tp_str(NGX_QUIC_TP_ORIGINAL_DCID, tp->original_dcid);
+    ngx_quic_tp_str(NGX_QUIC_TP_INITIAL_SCID, tp->initial_scid);
+
+    if (tp->retry_scid.len) {
+        ngx_quic_tp_str(NGX_QUIC_TP_RETRY_SCID, tp->retry_scid);
+    }
+#else
+    if (tp->original_dcid.len) {
+        ngx_quic_tp_str(NGX_QUIC_TP_ORIGINAL_DCID, tp->original_dcid);
+    }
+#endif
+
+    ngx_quic_build_int(&p, NGX_QUIC_TP_SR_TOKEN);
+    ngx_quic_build_int(&p, NGX_QUIC_SR_TOKEN_LEN);
+    p = ngx_cpymem(p, tp->sr_token, NGX_QUIC_SR_TOKEN_LEN);
+
+    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;
+    ngx_uint_t   type;
+
+    type = cl->app ? NGX_QUIC_FT_CONNECTION_CLOSE_APP
+                   : NGX_QUIC_FT_CONNECTION_CLOSE;
+
+    if (p == NULL) {
+        len = ngx_quic_varint_len(type);
+        len += ngx_quic_varint_len(cl->error_code);
+
+        if (!cl->app) {
+            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, type);
+    ngx_quic_build_int(&p, cl->error_code);
+
+    if (!cl->app) {
+        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,359 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_EVENT_QUIC_WIRE_H_INCLUDED_
+#define _NGX_EVENT_QUIC_WIRE_H_INCLUDED_
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+
+/* QUIC flags in first byte, see quic-transport 17.2 and 17.3 */
+
+#define NGX_QUIC_PKT_LONG       0x80  /* header form */
+#define NGX_QUIC_PKT_FIXED_BIT  0x40
+#define NGX_QUIC_PKT_TYPE       0x30  /* in long packet */
+#define NGX_QUIC_PKT_KPHASE     0x04  /* in short packet */
+
+#define ngx_quic_long_pkt(flags)  ((flags) & NGX_QUIC_PKT_LONG)
+#define ngx_quic_short_pkt(flags)  (((flags) & NGX_QUIC_PKT_LONG) == 0)
+
+/* Long packet types */
+#define NGX_QUIC_PKT_INITIAL    0x00
+#define NGX_QUIC_PKT_ZRTT       0x10
+#define NGX_QUIC_PKT_HANDSHAKE  0x20
+#define NGX_QUIC_PKT_RETRY      0x30
+
+#define ngx_quic_pkt_in(flags)                                                \
+    (((flags) & NGX_QUIC_PKT_TYPE) == NGX_QUIC_PKT_INITIAL)
+#define ngx_quic_pkt_zrtt(flags)                                              \
+    (((flags) & NGX_QUIC_PKT_TYPE) == NGX_QUIC_PKT_ZRTT)
+#define ngx_quic_pkt_hs(flags)                                                \
+    (((flags) & NGX_QUIC_PKT_TYPE) == NGX_QUIC_PKT_HANDSHAKE)
+#define ngx_quic_pkt_retry(flags)                                             \
+    (((flags) & NGX_QUIC_PKT_TYPE) == NGX_QUIC_PKT_RETRY)
+
+#define ngx_quic_pkt_rb_mask(flags)                                           \
+    (ngx_quic_long_pkt(flags) ? 0x0C : 0x18)
+#define ngx_quic_pkt_hp_mask(flags)                                           \
+    (ngx_quic_long_pkt(flags) ? 0x0F : 0x1F)
+
+#define ngx_quic_level_name(lvl)                                              \
+    (lvl == ssl_encryption_application) ? "app"                               \
+        : (lvl == ssl_encryption_initial) ? "init"                            \
+            : (lvl == ssl_encryption_handshake) ? "hs" : "early"
+
+
+/* 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_CLOSE_APP                 0x1D
+#define NGX_QUIC_FT_HANDSHAKE_DONE                       0x1E
+
+/* 22.4.  QUIC Transport Error Codes Registry */
+/* Keep in sync with ngx_quic_errors[] */
+#define NGX_QUIC_ERR_NO_ERROR                            0x00
+#define NGX_QUIC_ERR_INTERNAL_ERROR                      0x01
+#define NGX_QUIC_ERR_CONNECTION_REFUSED                  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
+#define NGX_QUIC_ERR_APPLICATION_ERROR                   0x0C
+#define NGX_QUIC_ERR_CRYPTO_BUFFER_EXCEEDED              0x0D
+#define NGX_QUIC_ERR_KEY_UPDATE_ERROR                    0x0E
+
+#define NGX_QUIC_ERR_LAST                                0x0F
+#define NGX_QUIC_ERR_CRYPTO_ERROR                       0x100
+
+#define NGX_QUIC_ERR_CRYPTO(e)  (NGX_QUIC_ERR_CRYPTO_ERROR + (e))
+
+
+/* Transport parameters */
+#define NGX_QUIC_TP_ORIGINAL_DCID                        0x00
+#define NGX_QUIC_TP_MAX_IDLE_TIMEOUT                     0x01
+#define NGX_QUIC_TP_SR_TOKEN                             0x02
+#define NGX_QUIC_TP_MAX_UDP_PAYLOAD_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
+#define NGX_QUIC_TP_INITIAL_SCID                         0x0F
+#define NGX_QUIC_TP_RETRY_SCID                           0x10
+
+#define NGX_QUIC_CID_LEN_MIN                                8
+#define NGX_QUIC_CID_LEN_MAX                               20
+
+#define NGX_QUIC_MAX_RANGES                                10
+
+
+typedef struct {
+    uint64_t                                    gap;
+    uint64_t                                    range;
+} ngx_quic_ack_range_t;
+
+
+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                                    seqnum;
+    uint64_t                                    retire;
+    uint8_t                                     len;
+    u_char                                      cid[NGX_QUIC_CID_LEN_MAX];
+    u_char                                      srt[NGX_QUIC_SR_TOKEN_LEN];
+} ngx_quic_new_conn_id_frame_t;
+
+
+typedef struct {
+    uint64_t                                    length;
+    u_char                                     *data;
+} ngx_quic_new_token_frame_t;
+
+/*
+ * common layout for CRYPTO and STREAM frames;
+ * conceptually, CRYPTO frame is also a stream
+ * frame lacking some properties
+ */
+typedef struct {
+    uint64_t                                    offset;
+    uint64_t                                    length;
+    u_char                                     *data;
+} ngx_quic_ordered_frame_t;
+
+typedef ngx_quic_ordered_frame_t  ngx_quic_crypto_frame_t;
+
+
+typedef struct {
+    /* initial fields same as in ngx_quic_ordered_frame_t */
+    uint64_t                                    offset;
+    uint64_t                                    length;
+    u_char                                     *data;
+
+    uint8_t                                     type;
+    uint64_t                                    stream_id;
+    unsigned                                    off:1;
+    unsigned                                    len:1;
+    unsigned                                    fin:1;
+} 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_uint_t                                  app;  /* unsigned  app:1; */
+} 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;
+    size_t                                      plen;
+    ngx_msec_t                                  first;
+    ngx_msec_t                                  last;
+    ssize_t                                     len;
+    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_ordered_frame_t                ord;
+        ngx_quic_new_conn_id_frame_t            ncid;
+        ngx_quic_new_token_frame_t              token;
+        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;
+};
+
+
+typedef struct {
+    ngx_log_t                                  *log;
+
+    ngx_quic_keys_t                            *keys;
+
+    ngx_msec_t                                  received;
+    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;
+    ngx_uint_t                                  error;
+
+    /* filled in by parser */
+    ngx_buf_t                                  *raw;   /* udp datagram */
+
+    u_char                                     *data;  /* quic packet */
+    size_t                                      len;
+
+    /* cleartext fields */
+    ngx_str_t                                   odcid; /* retry packet tag */
+    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;
+    unsigned                                    parsed:1;
+    unsigned                                    decrypted:1;
+} ngx_quic_header_t;
+
+
+u_char *ngx_quic_error_text(uint64_t error_code);
+
+ngx_int_t ngx_quic_parse_packet(ngx_quic_header_t *pkt);
+
+size_t ngx_quic_create_version_negotiation(ngx_quic_header_t *pkt, u_char *out);
+
+size_t ngx_quic_create_header(ngx_quic_header_t *pkt, u_char *out,
+    size_t pkt_len, u_char **pnp);
+
+size_t ngx_quic_create_retry_itag(ngx_quic_header_t *pkt, u_char *out,
+    u_char **start);
+
+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_log_t *log, u_char *start,
+    u_char *end, uint64_t *gap, uint64_t *range);
+size_t ngx_quic_create_ack_range(u_char *p, 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, size_t *clen);
+
+#endif /* _NGX_EVENT_QUIC_WIRE_H_INCLUDED_ */
--- a/src/event/ngx_event_udp.c
+++ b/src/event/ngx_event_udp.c
@@ -12,19 +12,12 @@
 
 #if !(NGX_WIN32)
 
-struct ngx_udp_connection_s {
-    ngx_rbtree_node_t   node;
-    ngx_connection_t   *connection;
-    ngx_buf_t          *buffer;
-};
-
-
 static void ngx_close_accepted_udp_connection(ngx_connection_t *c);
 static ssize_t ngx_udp_shared_recv(ngx_connection_t *c, u_char *buf,
     size_t size);
-static ngx_int_t ngx_insert_udp_connection(ngx_connection_t *c);
+static ngx_int_t ngx_create_udp_connection(ngx_connection_t *c);
 static ngx_connection_t *ngx_lookup_udp_connection(ngx_listening_t *ls,
-    struct sockaddr *sockaddr, socklen_t socklen,
+    ngx_str_t *key, struct sockaddr *sockaddr, socklen_t socklen,
     struct sockaddr *local_sockaddr, socklen_t local_socklen);
 
 
@@ -32,6 +25,7 @@ void
 ngx_event_recvmsg(ngx_event_t *ev)
 {
     ssize_t            n;
+    ngx_str_t          key;
     ngx_buf_t          buf;
     ngx_log_t         *log;
     ngx_err_t          err;
@@ -229,8 +223,18 @@ ngx_event_recvmsg(ngx_event_t *ev)
 
 #endif
 
-        c = ngx_lookup_udp_connection(ls, sockaddr, socklen, local_sockaddr,
-                                      local_socklen);
+        ngx_str_null(&key);
+
+#if (NGX_QUIC)
+        if (ls->quic) {
+            if (ngx_quic_get_packet_dcid(ev->log, buffer, n, &key) != NGX_OK) {
+                goto next;
+            }
+        }
+#endif
+
+        c = ngx_lookup_udp_connection(ls, &key, sockaddr, socklen,
+                                      local_sockaddr, local_socklen);
 
         if (c) {
 
@@ -403,7 +407,7 @@ ngx_event_recvmsg(ngx_event_t *ev)
         }
 #endif
 
-        if (ngx_insert_udp_connection(c) != NGX_OK) {
+        if (ngx_create_udp_connection(c) != NGX_OK) {
             ngx_close_accepted_udp_connection(c);
             return;
         }
@@ -492,8 +496,13 @@ ngx_udp_rbtree_insert_value(ngx_rbtree_n
             udpt = (ngx_udp_connection_t *) temp;
             ct = udpt->connection;
 
-            rc = ngx_cmp_sockaddr(c->sockaddr, c->socklen,
-                                  ct->sockaddr, ct->socklen, 1);
+            rc = ngx_memn2cmp(udp->key.data, udpt->key.data,
+                              udp->key.len, udpt->key.len);
+
+            if (rc == 0 && udp->key.len == 0) {
+                rc = ngx_cmp_sockaddr(c->sockaddr, c->socklen,
+                                      ct->sockaddr, ct->socklen, 1);
+            }
 
             if (rc == 0 && c->listening->wildcard) {
                 rc = ngx_cmp_sockaddr(c->local_sockaddr, c->local_socklen,
@@ -519,12 +528,18 @@ ngx_udp_rbtree_insert_value(ngx_rbtree_n
 
 
 static ngx_int_t
-ngx_insert_udp_connection(ngx_connection_t *c)
+ngx_create_udp_connection(ngx_connection_t *c)
 {
-    uint32_t               hash;
+    ngx_str_t              key;
     ngx_pool_cleanup_t    *cln;
     ngx_udp_connection_t  *udp;
 
+#if (NGX_QUIC)
+    if (c->listening->quic) {
+        return NGX_OK;
+    }
+#endif
+
     if (c->udp) {
         return NGX_OK;
     }
@@ -534,19 +549,6 @@ ngx_insert_udp_connection(ngx_connection
         return NGX_ERROR;
     }
 
-    udp->connection = c;
-
-    ngx_crc32_init(hash);
-    ngx_crc32_update(&hash, (u_char *) c->sockaddr, c->socklen);
-
-    if (c->listening->wildcard) {
-        ngx_crc32_update(&hash, (u_char *) c->local_sockaddr, c->local_socklen);
-    }
-
-    ngx_crc32_final(hash);
-
-    udp->node.key = hash;
-
     cln = ngx_pool_cleanup_add(c->pool, 0);
     if (cln == NULL) {
         return NGX_ERROR;
@@ -555,7 +557,9 @@ ngx_insert_udp_connection(ngx_connection
     cln->data = c;
     cln->handler = ngx_delete_udp_connection;
 
-    ngx_rbtree_insert(&c->listening->rbtree, &udp->node);
+    key.len = 0;
+
+    ngx_insert_udp_connection(c, udp, &key);
 
     c->udp = udp;
 
@@ -564,6 +568,34 @@ ngx_insert_udp_connection(ngx_connection
 
 
 void
+ngx_insert_udp_connection(ngx_connection_t *c, ngx_udp_connection_t *udp,
+    ngx_str_t *key)
+{
+    uint32_t  hash;
+
+    ngx_crc32_init(hash);
+
+    ngx_crc32_update(&hash, key->data, key->len);
+
+    if (key->len == 0) {
+        ngx_crc32_update(&hash, (u_char *) c->sockaddr, c->socklen);
+    }
+
+    if (c->listening->wildcard) {
+        ngx_crc32_update(&hash, (u_char *) c->local_sockaddr, c->local_socklen);
+    }
+
+    ngx_crc32_final(hash);
+
+    udp->connection = c;
+    udp->key = *key;
+    udp->node.key = hash;
+
+    ngx_rbtree_insert(&c->listening->rbtree, &udp->node);
+}
+
+
+void
 ngx_delete_udp_connection(void *data)
 {
     ngx_connection_t  *c = data;
@@ -579,8 +611,9 @@ ngx_delete_udp_connection(void *data)
 
 
 static ngx_connection_t *
-ngx_lookup_udp_connection(ngx_listening_t *ls, struct sockaddr *sockaddr,
-    socklen_t socklen, struct sockaddr *local_sockaddr, socklen_t local_socklen)
+ngx_lookup_udp_connection(ngx_listening_t *ls, ngx_str_t *key,
+    struct sockaddr *sockaddr, socklen_t socklen,
+    struct sockaddr *local_sockaddr, socklen_t local_socklen)
 {
     uint32_t               hash;
     ngx_int_t              rc;
@@ -608,7 +641,12 @@ ngx_lookup_udp_connection(ngx_listening_
     sentinel = ls->rbtree.sentinel;
 
     ngx_crc32_init(hash);
-    ngx_crc32_update(&hash, (u_char *) sockaddr, socklen);
+
+    ngx_crc32_update(&hash, key->data, key->len);
+
+    if (key->len == 0) {
+        ngx_crc32_update(&hash, (u_char *) sockaddr, socklen);
+    }
 
     if (ls->wildcard) {
         ngx_crc32_update(&hash, (u_char *) local_sockaddr, local_socklen);
@@ -634,8 +672,12 @@ ngx_lookup_udp_connection(ngx_listening_
 
         c = udp->connection;
 
-        rc = ngx_cmp_sockaddr(sockaddr, socklen,
-                              c->sockaddr, c->socklen, 1);
+        rc = ngx_memn2cmp(key->data, udp->key.data, key->len, udp->key.len);
+
+        if (rc == 0 && key->len == 0) {
+            rc = ngx_cmp_sockaddr(sockaddr, socklen,
+                                  c->sockaddr, c->socklen, 1);
+        }
 
         if (rc == 0 && ls->wildcard) {
             rc = ngx_cmp_sockaddr(local_sockaddr, local_socklen,
@@ -643,6 +685,37 @@ ngx_lookup_udp_connection(ngx_listening_
         }
 
         if (rc == 0) {
+            if (key->len) {
+                rc = ngx_cmp_sockaddr(sockaddr, socklen,
+                                      c->sockaddr, c->socklen, 1);
+
+                if (rc) {
+#if (NGX_DEBUG)
+                    if (c->log->log_level & NGX_LOG_DEBUG_EVENT) {
+                        ngx_str_t  addr;
+                        u_char     text[NGX_SOCKADDR_STRLEN];
+
+                        addr.data = text;
+                        addr.len = ngx_sock_ntop(sockaddr, socklen, text,
+                                                 NGX_SOCKADDR_STRLEN, 1);
+
+                        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                                       "client migrated to %V", &addr);
+                    }
+#endif
+
+                    if (c->socklen < socklen) {
+                        c->sockaddr = ngx_palloc(c->pool, socklen);
+                        if (c->sockaddr == NULL) {
+                            return c;
+                        }
+                    }
+
+                    ngx_memcpy(c->sockaddr, sockaddr, socklen);
+                    c->socklen = socklen;
+                }
+            }
+
             return c;
         }
 
--- a/src/http/modules/ngx_http_chunked_filter_module.c
+++ b/src/http/modules/ngx_http_chunked_filter_module.c
@@ -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,27 +162,65 @@ 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 (NGX_HTTP_V3)
+    if (r->http_version == NGX_HTTP_VERSION_30) {
+
+        if (cl->buf->last_buf) {
+            tl = ngx_http_v3_create_trailers(r);
+            if (tl == NULL) {
+                return NGX_ERROR;
+            }
+
+            cl->buf->last_buf = 0;
+
+            *ll = tl;
+        }
+
+    } else
+#endif
+
     if (cl->buf->last_buf) {
         tl = ngx_http_chunked_create_trailers(r, ctx);
         if (tl == NULL) {
new file mode 100644
--- /dev/null
+++ b/src/http/modules/ngx_http_quic_module.c
@@ -0,0 +1,374 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ * Copyright (C) Roman Arutyunyan
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_http.h>
+
+
+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_quic_add_variables(ngx_conf_t *cf);
+static void *ngx_http_quic_create_srv_conf(ngx_conf_t *cf);
+static char *ngx_http_quic_merge_srv_conf(ngx_conf_t *cf, void *parent,
+    void *child);
+static char *ngx_http_quic_max_ack_delay(ngx_conf_t *cf, void *post,
+    void *data);
+static char *ngx_http_quic_max_udp_payload_size(ngx_conf_t *cf, void *post,
+    void *data);
+
+
+static ngx_conf_post_t  ngx_http_quic_max_ack_delay_post =
+    { ngx_http_quic_max_ack_delay };
+static ngx_conf_post_t  ngx_http_quic_max_udp_payload_size_post =
+    { ngx_http_quic_max_udp_payload_size };
+static ngx_conf_num_bounds_t  ngx_http_quic_ack_delay_exponent_bounds =
+    { ngx_conf_check_num_bounds, 0, 20 };
+static ngx_conf_num_bounds_t  ngx_http_quic_active_connection_id_limit_bounds =
+    { ngx_conf_check_num_bounds, 2, -1 };
+
+
+static ngx_command_t  ngx_http_quic_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_quic_conf_t, tp.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_quic_conf_t, tp.max_ack_delay),
+      &ngx_http_quic_max_ack_delay_post },
+
+    { ngx_string("quic_max_udp_payload_size"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1,
+      ngx_conf_set_size_slot,
+      NGX_HTTP_SRV_CONF_OFFSET,
+      offsetof(ngx_quic_conf_t, tp.max_udp_payload_size),
+      &ngx_http_quic_max_udp_payload_size_post },
+
+    { ngx_string("quic_initial_max_data"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1,
+      ngx_conf_set_size_slot,
+      NGX_HTTP_SRV_CONF_OFFSET,
+      offsetof(ngx_quic_conf_t, tp.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_size_slot,
+      NGX_HTTP_SRV_CONF_OFFSET,
+      offsetof(ngx_quic_conf_t, tp.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_size_slot,
+      NGX_HTTP_SRV_CONF_OFFSET,
+      offsetof(ngx_quic_conf_t, tp.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_size_slot,
+      NGX_HTTP_SRV_CONF_OFFSET,
+      offsetof(ngx_quic_conf_t, tp.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_quic_conf_t, tp.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_quic_conf_t, tp.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_quic_conf_t, tp.ack_delay_exponent),
+      &ngx_http_quic_ack_delay_exponent_bounds },
+
+    { ngx_string("quic_disable_active_migration"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1,
+      ngx_conf_set_flag_slot,
+      NGX_HTTP_SRV_CONF_OFFSET,
+      offsetof(ngx_quic_conf_t, tp.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_quic_conf_t, tp.active_connection_id_limit),
+      &ngx_http_quic_active_connection_id_limit_bounds },
+
+    { ngx_string("quic_retry"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_FLAG,
+      ngx_conf_set_flag_slot,
+      NGX_HTTP_SRV_CONF_OFFSET,
+      offsetof(ngx_quic_conf_t, retry),
+      NULL },
+
+    { ngx_string("quic_stateless_reset_token_key"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_FLAG,
+      ngx_conf_set_str_slot,
+      NGX_HTTP_SRV_CONF_OFFSET,
+      offsetof(ngx_quic_conf_t, sr_token_key),
+      NULL },
+
+      ngx_null_command
+};
+
+
+static ngx_http_module_t  ngx_http_quic_module_ctx = {
+    ngx_http_quic_add_variables,           /* preconfiguration */
+    NULL,                                  /* postconfiguration */
+
+    NULL,                                  /* create main configuration */
+    NULL,                                  /* init main configuration */
+
+    ngx_http_quic_create_srv_conf,         /* create server configuration */
+    ngx_http_quic_merge_srv_conf,          /* merge server configuration */
+
+    NULL,                                  /* create location configuration */
+    NULL                                   /* merge location configuration */
+};
+
+
+ngx_module_t  ngx_http_quic_module = {
+    NGX_MODULE_V1,
+    &ngx_http_quic_module_ctx,             /* module context */
+    ngx_http_quic_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_quic_vars[] = {
+
+    { ngx_string("quic"), NULL, ngx_http_variable_quic, 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->quic) {
+
+        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_quic_add_variables(ngx_conf_t *cf)
+{
+    ngx_http_variable_t  *var, *v;
+
+    for (v = ngx_http_quic_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_quic_create_srv_conf(ngx_conf_t *cf)
+{
+    ngx_quic_conf_t  *conf;
+
+    conf = ngx_pcalloc(cf->pool, sizeof(ngx_quic_conf_t));
+    if (conf == NULL) {
+        return NULL;
+    }
+
+    /*
+     * set by ngx_pcalloc():
+     *
+     *     conf->tp.original_dcid = { 0, NULL };
+     *     conf->tp.initial_scid = { 0, NULL };
+     *     conf->tp.retry_scid = { 0, NULL };
+     *     conf->tp.sr_token = { 0 }
+     *     conf->tp.sr_enabled = 0
+     *     conf->tp.preferred_address = NULL
+     *     conf->sr_token_key = { 0, NULL }
+     */
+
+    conf->tp.max_idle_timeout = NGX_CONF_UNSET_MSEC;
+    conf->tp.max_ack_delay = NGX_CONF_UNSET_MSEC;
+    conf->tp.max_udp_payload_size = NGX_CONF_UNSET_SIZE;
+    conf->tp.initial_max_data = NGX_CONF_UNSET_SIZE;
+    conf->tp.initial_max_stream_data_bidi_local = NGX_CONF_UNSET_SIZE;
+    conf->tp.initial_max_stream_data_bidi_remote = NGX_CONF_UNSET_SIZE;
+    conf->tp.initial_max_stream_data_uni = NGX_CONF_UNSET_SIZE;
+    conf->tp.initial_max_streams_bidi = NGX_CONF_UNSET_UINT;
+    conf->tp.initial_max_streams_uni = NGX_CONF_UNSET_UINT;
+    conf->tp.ack_delay_exponent = NGX_CONF_UNSET_UINT;
+    conf->tp.disable_active_migration = NGX_CONF_UNSET;
+    conf->tp.active_connection_id_limit = NGX_CONF_UNSET_UINT;
+
+    conf->retry = NGX_CONF_UNSET;
+    conf->require_alpn = 1;
+
+    return conf;
+}
+
+
+static char *
+ngx_http_quic_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child)
+{
+    ngx_quic_conf_t *prev = parent;
+    ngx_quic_conf_t *conf = child;
+
+    ngx_http_ssl_srv_conf_t  *sscf;
+
+    ngx_conf_merge_msec_value(conf->tp.max_idle_timeout,
+                              prev->tp.max_idle_timeout, 60000);
+
+    ngx_conf_merge_msec_value(conf->tp.max_ack_delay,
+                              prev->tp.max_ack_delay,
+                              NGX_QUIC_DEFAULT_MAX_ACK_DELAY);
+
+    ngx_conf_merge_size_value(conf->tp.max_udp_payload_size,
+                              prev->tp.max_udp_payload_size,
+                              NGX_QUIC_MAX_UDP_PAYLOAD_SIZE);
+
+    ngx_conf_merge_size_value(conf->tp.initial_max_data,
+                              prev->tp.initial_max_data,
+                              16 * NGX_QUIC_STREAM_BUFSIZE);
+
+    ngx_conf_merge_size_value(conf->tp.initial_max_stream_data_bidi_local,
+                              prev->tp.initial_max_stream_data_bidi_local,
+                              NGX_QUIC_STREAM_BUFSIZE);
+
+    ngx_conf_merge_size_value(conf->tp.initial_max_stream_data_bidi_remote,
+                              prev->tp.initial_max_stream_data_bidi_remote,
+                              NGX_QUIC_STREAM_BUFSIZE);
+
+    ngx_conf_merge_size_value(conf->tp.initial_max_stream_data_uni,
+                              prev->tp.initial_max_stream_data_uni,
+                              NGX_QUIC_STREAM_BUFSIZE);
+
+    ngx_conf_merge_uint_value(conf->tp.initial_max_streams_bidi,
+                              prev->tp.initial_max_streams_bidi, 16);
+
+    ngx_conf_merge_uint_value(conf->tp.initial_max_streams_uni,
+                              prev->tp.initial_max_streams_uni, 16);
+
+    ngx_conf_merge_uint_value(conf->tp.ack_delay_exponent,
+                              prev->tp.ack_delay_exponent,
+                              NGX_QUIC_DEFAULT_ACK_DELAY_EXPONENT);
+
+    ngx_conf_merge_value(conf->tp.disable_active_migration,
+                              prev->tp.disable_active_migration, 0);
+
+    ngx_conf_merge_uint_value(conf->tp.active_connection_id_limit,
+                              prev->tp.active_connection_id_limit, 2);
+
+    ngx_conf_merge_value(conf->retry, prev->retry, 0);
+
+    if (conf->retry) {
+        if (RAND_bytes(conf->token_key, sizeof(conf->token_key)) <= 0) {
+            return NGX_CONF_ERROR;
+        }
+    }
+
+    ngx_conf_merge_str_value(conf->sr_token_key, prev->sr_token_key, "");
+
+    if (conf->sr_token_key.len == 0) {
+        conf->sr_token_key.len = NGX_QUIC_DEFAULT_SRT_KEY_LEN;
+
+        conf->sr_token_key.data = ngx_pnalloc(cf->pool, conf->sr_token_key.len);
+        if (conf->sr_token_key.data == NULL) {
+            return NGX_CONF_ERROR;
+        }
+
+        if (RAND_bytes(conf->sr_token_key.data, conf->sr_token_key.len) <= 0) {
+            return NGX_CONF_ERROR;
+        }
+    }
+
+    sscf = ngx_http_conf_get_module_srv_conf(cf, ngx_http_ssl_module);
+    conf->ssl = &sscf->ssl;
+
+    return NGX_CONF_OK;
+}
+
+
+static char *
+ngx_http_quic_max_ack_delay(ngx_conf_t *cf, void *post, void *data)
+{
+    ngx_msec_t *sp = data;
+
+    if (*sp > 16384) {
+        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
+                           "\"quic_max_ack_delay\" must be less than 16384");
+
+        return NGX_CONF_ERROR;
+    }
+
+    return NGX_CONF_OK;
+}
+
+
+static char *
+ngx_http_quic_max_udp_payload_size(ngx_conf_t *cf, void *post, void *data)
+{
+    size_t *sp = data;
+
+    if (*sp < NGX_QUIC_MIN_INITIAL_SIZE
+        || *sp > NGX_QUIC_MAX_UDP_PAYLOAD_SIZE)
+    {
+        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
+                           "\"quic_max_udp_payload_size\" must be between "
+                           "%d and %d",
+                           NGX_QUIC_MIN_INITIAL_SIZE,
+                           NGX_QUIC_MAX_UDP_PAYLOAD_SIZE);
+
+        return NGX_CONF_ERROR;
+    }
+
+    return NGX_CONF_OK;
+}
new file mode 100644
--- /dev/null
+++ b/src/http/modules/ngx_http_quic_module.h
@@ -0,0 +1,24 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ * Copyright (C) Roman Arutyunyan
+ */
+
+
+#ifndef _NGX_HTTP_QUIC_H_INCLUDED_
+#define _NGX_HTTP_QUIC_H_INCLUDED_
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_http.h>
+
+
+#define NGX_HTTP_QUIC_ALPN_ADVERTISE  "\x02hq"
+#define NGX_HTTP_QUIC_ALPN_DRAFT_FMT  "\x05hq-%02uD"
+
+
+extern ngx_module_t  ngx_http_quic_module;
+
+
+#endif /* _NGX_HTTP_QUIC_H_INCLUDED_ */
--- a/src/http/modules/ngx_http_ssl_module.c
+++ b/src/http/modules/ngx_http_ssl_module.c
@@ -418,12 +418,15 @@ ngx_http_ssl_alpn_select(ngx_ssl_conn_t 
     unsigned char *outlen, const unsigned char *in, unsigned int inlen,
     void *arg)
 {
+#if (NGX_HTTP_QUIC)
+    const char             *fmt;
+#endif
     unsigned int            srvlen;
     unsigned char          *srv;
 #if (NGX_DEBUG)
     unsigned int            i;
 #endif
-#if (NGX_HTTP_V2)
+#if (NGX_HTTP_V2 || NGX_HTTP_QUIC)
     ngx_http_connection_t  *hc;
 #endif
 #if (NGX_HTTP_V2 || NGX_DEBUG)
@@ -440,9 +443,11 @@ ngx_http_ssl_alpn_select(ngx_ssl_conn_t 
     }
 #endif
 
-#if (NGX_HTTP_V2)
+#if (NGX_HTTP_V2 || NGX_HTTP_QUIC)
     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;
@@ -450,6 +455,34 @@ ngx_http_ssl_alpn_select(ngx_ssl_conn_t 
 
     } else
 #endif
+#if (NGX_HTTP_QUIC)
+    if (hc->addr_conf->quic) {
+#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;
+            fmt = NGX_HTTP_V3_ALPN_DRAFT_FMT;
+
+        } else
+#endif
+        {
+            srv = (unsigned char *) NGX_HTTP_QUIC_ALPN_ADVERTISE;
+            srvlen = sizeof(NGX_HTTP_QUIC_ALPN_ADVERTISE) - 1;
+            fmt = NGX_HTTP_QUIC_ALPN_DRAFT_FMT;
+        }
+
+        /* QUIC draft */
+
+        if (ngx_quic_version(c) > 1) {
+            srv = ngx_pnalloc(c->pool, sizeof("\x05h3-xx") - 1);
+            if (srv == NULL) {
+                return SSL_TLSEXT_ERR_NOACK;
+            }
+            srvlen = ngx_sprintf(srv, fmt, ngx_quic_version(c)) - srv;
+        }
+
+    } else
+#endif
     {
         srv = (unsigned char *) NGX_HTTP_NPN_ADVERTISE;
         srvlen = sizeof(NGX_HTTP_NPN_ADVERTISE) - 1;
@@ -1284,6 +1317,7 @@ static ngx_int_t
 ngx_http_ssl_init(ngx_conf_t *cf)
 {
     ngx_uint_t                   a, p, s;
+    const char                  *name;
     ngx_http_conf_addr_t        *addr;
     ngx_http_conf_port_t        *port;
     ngx_http_ssl_srv_conf_t     *sscf;
@@ -1333,10 +1367,20 @@ 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.quic) {
                 continue;
             }
 
+            if (addr[a].opt.http3) {
+                name = "http3";
+
+            } else if (addr[a].opt.quic) {
+                name = "quic";
+
+            } else {
+                name = "ssl";
+            }
+
             cscf = addr[a].default_server;
             sscf = cscf->ctx->srv_conf[ngx_http_ssl_module.ctx_index];
 
@@ -1369,8 +1413,16 @@ ngx_http_ssl_init(ngx_conf_t *cf)
 
                 ngx_log_error(NGX_LOG_EMERG, cf->log, 0,
                               "no \"ssl_certificate\" is defined for "
-                              "the \"listen ... ssl\" directive in %s:%ui",
-                              cscf->file_name, cscf->line);
+                              "the \"listen ... %s\" directive in %s:%ui",
+                              name, cscf->file_name, cscf->line);
+                return NGX_ERROR;
+            }
+
+            if (addr[a].opt.quic && !(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 ... %s\" directives in %s:%ui",
+                              name, 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;
 
@@ -1196,9 +1200,15 @@ ngx_http_add_addresses(ngx_conf_t *cf, n
 #if (NGX_HTTP_SSL)
     ngx_uint_t             ssl;
 #endif
+#if (NGX_HTTP_QUIC)
+    ngx_uint_t             quic;
+#endif
 #if (NGX_HTTP_V2)
     ngx_uint_t             http2;
 #endif
+#if (NGX_HTTP_V3)
+    ngx_uint_t             http3;
+#endif
 
     /*
      * we cannot compare whole sockaddr struct's as kernel
@@ -1231,9 +1241,15 @@ ngx_http_add_addresses(ngx_conf_t *cf, n
 #if (NGX_HTTP_SSL)
         ssl = lsopt->ssl || addr[i].opt.ssl;
 #endif
+#if (NGX_HTTP_QUIC)
+        quic = lsopt->quic || addr[i].opt.quic;
+#endif
 #if (NGX_HTTP_V2)
         http2 = lsopt->http2 || addr[i].opt.http2;
 #endif
+#if (NGX_HTTP_V3)
+        http3 = lsopt->http3 || addr[i].opt.http3;
+#endif
 
         if (lsopt->set) {
 
@@ -1267,9 +1283,15 @@ ngx_http_add_addresses(ngx_conf_t *cf, n
 #if (NGX_HTTP_SSL)
         addr[i].opt.ssl = ssl;
 #endif
+#if (NGX_HTTP_QUIC)
+        addr[i].opt.quic = quic;
+#endif
 #if (NGX_HTTP_V2)
         addr[i].opt.http2 = http2;
 #endif
+#if (NGX_HTTP_V3)
+        addr[i].opt.http3 = http3;
+#endif
 
         return NGX_OK;
     }
@@ -1313,6 +1335,17 @@ ngx_http_add_address(ngx_conf_t *cf, ngx
 
 #endif
 
+#if (NGX_HTTP_QUIC && !defined NGX_OPENSSL_QUIC)
+
+    if (lsopt->quic) {
+        ngx_conf_log_error(NGX_LOG_WARN, cf, 0,
+                           "nginx was built with OpenSSL that lacks QUIC "
+                           "support, QUIC is not enabled for %V",
+                           &lsopt->addr_text);
+    }
+
+#endif
+
     addr = ngx_array_push(&port->addrs);
     if (addr == NULL) {
         return NGX_ERROR;
@@ -1735,6 +1768,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;
@@ -1770,6 +1804,12 @@ ngx_http_add_listening(ngx_conf_t *cf, n
     ls->reuseport = addr->opt.reuseport;
 #endif
 
+    ls->wildcard = addr->opt.wildcard;
+
+#if (NGX_HTTP_QUIC)
+    ls->quic = addr->opt.quic;
+#endif
+
     return ls;
 }
 
@@ -1799,9 +1839,15 @@ ngx_http_add_addrs(ngx_conf_t *cf, ngx_h
 #if (NGX_HTTP_SSL)
         addrs[i].conf.ssl = addr[i].opt.ssl;
 #endif
+#if (NGX_HTTP_QUIC)
+        addrs[i].conf.quic = addr[i].opt.quic;
+#endif
 #if (NGX_HTTP_V2)
         addrs[i].conf.http2 = addr[i].opt.http2;
 #endif
+#if (NGX_HTTP_V3)
+        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
@@ -1864,9 +1910,15 @@ ngx_http_add_addrs6(ngx_conf_t *cf, ngx_
 #if (NGX_HTTP_SSL)
         addrs6[i].conf.ssl = addr[i].opt.ssl;
 #endif
+#if (NGX_HTTP_QUIC)
+        addrs6[i].conf.quic = addr[i].opt.quic;
+#endif
 #if (NGX_HTTP_V2)
         addrs6[i].conf.http2 = addr[i].opt.http2;
 #endif
+#if (NGX_HTTP_V3)
+        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
@@ -47,6 +50,9 @@ typedef u_char *(*ngx_http_log_handler_p
 #if (NGX_HTTP_SSL)
 #include <ngx_http_ssl_module.h>
 #endif
+#if (NGX_HTTP_QUIC)
+#include <ngx_http_quic_module.h>
+#endif
 
 
 struct ngx_http_log_ctx_s {
@@ -60,6 +66,9 @@ struct ngx_http_chunked_s {
     ngx_uint_t           state;
     off_t                size;
     off_t                length;
+#if (NGX_HTTP_V3)
+    void                *h3_parse;
+#endif
 };
 
 
@@ -84,6 +93,7 @@ ngx_int_t ngx_http_add_listen(ngx_conf_t
 
 void ngx_http_init_connection(ngx_connection_t *c);
 void ngx_http_close_connection(ngx_connection_t *c);
+u_char *ngx_http_log_error(ngx_log_t *log, u_char *buf, size_t len);
 
 #if (NGX_HTTP_SSL && defined SSL_CTRL_SET_TLSEXT_HOSTNAME)
 int ngx_http_ssl_servername(ngx_ssl_conn_t *ssl_conn, int *ad, void *arg);
--- 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,33 @@ ngx_http_core_listen(ngx_conf_t *cf, ngx
 #endif
         }
 
+        if (ngx_strcmp(value[n].data, "quic") == 0) {
+#if (NGX_HTTP_QUIC)
+            lsopt.quic = 1;
+            lsopt.type = SOCK_DGRAM;
+            continue;
+#else
+            ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
+                               "the \"quic\" parameter requires "
+                               "ngx_http_quic_module");
+            return NGX_CONF_ERROR;
+#endif
+        }
+
+        if (ngx_strcmp(value[n].data, "http3") == 0) {
+#if (NGX_HTTP_V3)
+            lsopt.quic = 1;
+            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\": "
@@ -4187,6 +4215,22 @@ ngx_http_core_listen(ngx_conf_t *cf, ngx
         return NGX_CONF_ERROR;
     }
 
+#if (NGX_HTTP_SSL)
+
+#if (NGX_HTTP_V3)
+    if (lsopt.ssl && lsopt.http3) {
+        return "\"ssl\" parameter is incompatible with \"http3\"";
+    }
+#endif
+
+#if (NGX_HTTP_QUIC)
+    if (lsopt.ssl && lsopt.quic) {
+        return "\"ssl\" parameter is incompatible with \"quic\"";
+    }
+#endif
+
+#endif
+
     for (n = 0; n < u.naddrs; n++) {
         lsopt.sockaddr = u.addrs[n].sockaddr;
         lsopt.socklen = u.addrs[n].socklen;
--- a/src/http/ngx_http_core_module.h
+++ b/src/http/ngx_http_core_module.h
@@ -74,7 +74,9 @@ typedef struct {
     unsigned                   bind:1;
     unsigned                   wildcard:1;
     unsigned                   ssl:1;
+    unsigned                   quic:1;
     unsigned                   http2:1;
+    unsigned                   http3:1;
 #if (NGX_HAVE_INET6)
     unsigned                   ipv6only:1;
 #endif
@@ -86,6 +88,7 @@ typedef struct {
     int                        backlog;
     int                        rcvbuf;
     int                        sndbuf;
+    int                        type;
 #if (NGX_HAVE_SETFIB)
     int                        setfib;
 #endif
@@ -236,7 +239,9 @@ struct ngx_http_addr_conf_s {
     ngx_http_virtual_names_t  *virtual_names;
 
     unsigned                   ssl:1;
+    unsigned                   quic:1;
     unsigned                   http2:1;
+    unsigned                   http3:1;
     unsigned                   proxy_protocol:1;
 };
 
@@ -266,6 +271,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
@@ -143,7 +143,9 @@ ngx_http_parse_request_line(ngx_http_req
 
         /* HTTP methods: GET, HEAD, POST */
         case sw_start:
+            r->parse_start = p;
             r->request_start = p;
+            r->method_start = p;
 
             if (ch == CR || ch == LF) {
                 break;
@@ -158,7 +160,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) {
@@ -831,6 +833,10 @@ done:
         r->request_end = p;
     }
 
+    if (r->http_protocol.data) {
+        r->http_protocol.len = r->request_end - r->http_protocol.data;
+    }
+
     r->http_version = r->http_major * 1000 + r->http_minor;
     r->state = sw_start;
 
@@ -882,6 +888,7 @@ ngx_http_parse_header_line(ngx_http_requ
 
         /* first char */
         case sw_start:
+            r->parse_start = p;
             r->header_name_start = p;
             r->invalid_header = 0;
 
--- a/src/http/ngx_http_request.c
+++ b/src/http/ngx_http_request.c
@@ -55,7 +55,6 @@ static ngx_int_t ngx_http_post_action(ng
 static void ngx_http_close_request(ngx_http_request_t *r, ngx_int_t error);
 static void ngx_http_log_request(ngx_http_request_t *r);
 
-static u_char *ngx_http_log_error(ngx_log_t *log, u_char *buf, size_t len);
 static u_char *ngx_http_log_error_handler(ngx_http_request_t *r,
     ngx_http_request_t *sr, u_char *buf, size_t len);
 
@@ -303,6 +302,57 @@ ngx_http_init_connection(ngx_connection_
     /* the default server configuration for the address:port */
     hc->conf_ctx = hc->addr_conf->default_server->ctx;
 
+#if (NGX_HTTP_QUIC)
+
+    if (hc->addr_conf->quic) {
+        ngx_quic_conf_t           *qcf;
+        ngx_http_connection_t     *phc;
+        ngx_http_core_loc_conf_t  *clcf;
+
+        hc->ssl = 1;
+
+#if (NGX_HTTP_V3)
+
+        if (hc->addr_conf->http3) {
+            ngx_int_t  rc;
+
+            rc = ngx_http_v3_init_connection(c);
+
+            if (rc == NGX_ERROR) {
+                ngx_http_close_connection(c);
+                return;
+            }
+
+            if (rc == NGX_DONE) {
+                return;
+            }
+        }
+
+#endif
+
+        if (c->quic == NULL) {
+            c->log->connection = c->number;
+
+            qcf = ngx_http_get_module_srv_conf(hc->conf_ctx,
+                                               ngx_http_quic_module);
+            ngx_quic_run(c, qcf);
+            return;
+        }
+
+        phc = c->quic->parent->data;
+
+        if (phc->ssl_servername) {
+            hc->ssl_servername = phc->ssl_servername;
+            hc->conf_ctx = phc->conf_ctx;
+
+            clcf = ngx_http_get_module_loc_conf(hc->conf_ctx,
+                                                ngx_http_core_module);
+            ngx_set_connection_log(c, clcf->error_log);
+        }
+    }
+
+#endif
+
     ctx = ngx_palloc(c->pool, sizeof(ngx_http_log_ctx_t));
     if (ctx == NULL) {
         ngx_http_close_connection(c);
@@ -619,6 +669,12 @@ 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->addr_conf->http3) {
+        r->http_version = NGX_HTTP_VERSION_30;
+    }
+#endif
+
     r->headers_in.content_length_n = -1;
     r->headers_in.keep_alive_n = -1;
     r->headers_out.content_length_n = -1;
@@ -1084,7 +1140,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_request(r, r->header_in);
+            break;
+#endif
+
+        default: /* HTTP/1.x */
+            rc = ngx_http_parse_request_line(r, r->header_in);
+        }
 
         if (rc == NGX_OK) {
 
@@ -1092,17 +1157,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->parse_start;
 
             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;
-
-            if (r->http_protocol.data) {
-                r->http_protocol.len = r->request_end - r->http_protocol.data;
-            }
+            r->method_name.len = r->method_end - r->method_start;
+            r->method_name.data = r->method_start;
 
             if (ngx_http_process_request_uri(r) != NGX_OK) {
                 break;
@@ -1169,6 +1230,15 @@ ngx_http_process_request_line(ngx_event_
             break;
         }
 
+        if (rc == NGX_BUSY) {
+            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 */
@@ -1198,8 +1268,8 @@ ngx_http_process_request_line(ngx_event_
             }
 
             if (rv == NGX_DECLINED) {
-                r->request_line.len = r->header_in->end - r->request_start;
-                r->request_line.data = r->request_start;
+                r->request_line.len = r->header_in->end - r->parse_start;
+                r->request_line.data = r->parse_start;
 
                 ngx_log_error(NGX_LOG_INFO, c->log, 0,
                               "client sent too long URI");
@@ -1359,7 +1429,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 ( ;; ) {
 
@@ -1375,7 +1445,7 @@ ngx_http_process_request_headers(ngx_eve
                 }
 
                 if (rv == NGX_DECLINED) {
-                    p = r->header_name_start;
+                    p = r->parse_start;
 
                     r->lingering_close = 1;
 
@@ -1395,7 +1465,7 @@ ngx_http_process_request_headers(ngx_eve
 
                     ngx_log_error(NGX_LOG_INFO, c->log, 0,
                                 "client sent too long header line: \"%*s...\"",
-                                len, r->header_name_start);
+                                len, r->parse_start);
 
                     ngx_http_finalize_request(r,
                                             NGX_HTTP_REQUEST_HEADER_TOO_LARGE);
@@ -1413,21 +1483,32 @@ 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,
+                                          cscf->underscores_in_headers);
+            break;
+#endif
+
+        default: /* HTTP/1.x */
+            rc = ngx_http_parse_header_line(r, r->header_in,
+                                            cscf->underscores_in_headers);
+        }
 
         if (rc == NGX_OK) {
 
-            r->request_length += r->header_in->pos - r->header_name_start;
+            r->request_length += r->header_in->pos - r->parse_start;
 
             if (r->invalid_header && cscf->ignore_invalid_headers) {
 
                 /* there was error while a header line parsing */
 
                 ngx_log_error(NGX_LOG_INFO, c->log, 0,
-                              "client sent invalid header line: \"%*s\"",
-                              r->header_end - r->header_name_start,
-                              r->header_name_start);
+                              "client sent invalid header line: \"%*s: %*s\"",
+                              r->header_name_end - r->header_name_start,
+                              r->header_name_start,
+                              r->header_end - r->header_start, r->header_start);
                 continue;
             }
 
@@ -1443,11 +1524,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) {
@@ -1483,7 +1570,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->parse_start;
 
             r->http_state = NGX_HTTP_PROCESS_REQUEST_STATE;
 
@@ -1598,7 +1685,7 @@ ngx_http_alloc_large_header_buffer(ngx_h
         return NGX_OK;
     }
 
-    old = request_line ? r->request_start : r->header_name_start;
+    old = r->parse_start;
 
     cscf = ngx_http_get_module_srv_conf(r, ngx_http_core_module);
 
@@ -1676,6 +1763,14 @@ ngx_http_alloc_large_header_buffer(ngx_h
     b->pos = new + (r->header_in->pos - old);
     b->last = new + (r->header_in->pos - old);
 
+    r->parse_start = new;
+
+    r->header_in = b;
+
+    if (r->http_version > NGX_HTTP_VERSION_11) {
+        return NGX_OK;
+    }
+
     if (request_line) {
         r->request_start = new;
 
@@ -1724,8 +1819,6 @@ ngx_http_alloc_large_header_buffer(ngx_h
         r->header_end = new + (r->header_end - old);
     }
 
-    r->header_in = b;
-
     return NGX_OK;
 }
 
@@ -1946,13 +2039,46 @@ 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);
         return NGX_ERROR;
     }
 
+    if (r->headers_in.host == NULL && r->http_version == NGX_HTTP_VERSION_20) {
+        ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
+                      "client sent HTTP/2 request without "
+                      "\":authority\" or \"Host\" header");
+        ngx_http_finalize_request(r, NGX_HTTP_BAD_REQUEST);
+        return NGX_ERROR;
+    }
+
+    if (r->http_version == NGX_HTTP_VERSION_30) {
+        if (r->headers_in.server.len == 0) {
+            ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
+                          "client sent HTTP/3 request without "
+                          "\":authority\" or \"Host\" header");
+            ngx_http_finalize_request(r, NGX_HTTP_BAD_REQUEST);
+            return NGX_ERROR;
+        }
+
+        if (r->headers_in.host) {
+            if (r->headers_in.host->value.len != r->headers_in.server.len
+                || ngx_memcmp(r->headers_in.host->value.data,
+                              r->headers_in.server.data,
+                              r->headers_in.server.len)
+                   != 0)
+            {
+                ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
+                              "client sent HTTP/3 request with different "
+                              "values of \":authority\" and \"Host\" headers");
+                ngx_http_finalize_request(r, NGX_HTTP_BAD_REQUEST);
+                return NGX_ERROR;
+            }
+        }
+    }
+
     if (r->headers_in.content_length) {
         r->headers_in.content_length_n =
                             ngx_atoof(r->headers_in.content_length->value.data,
@@ -1991,6 +2117,12 @@ ngx_http_process_request_header(ngx_http
         }
     }
 
+#if (NGX_HTTP_V3)
+    if (r->http_version == NGX_HTTP_VERSION_30) {
+        r->headers_in.chunked = 1;
+    }
+#endif
+
     if (r->headers_in.connection_type == NGX_HTTP_CONNECTION_KEEP_ALIVE) {
         if (r->headers_in.keep_alive) {
             r->headers_in.keep_alive_n =
@@ -2714,6 +2846,13 @@ ngx_http_finalize_connection(ngx_http_re
     }
 #endif
 
+#if (NGX_HTTP_QUIC)
+    if (r->connection->quic) {
+        ngx_http_close_request(r, 0);
+        return;
+    }
+#endif
+
     clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
 
     if (r->main->count != 1) {
@@ -2923,6 +3062,19 @@ ngx_http_test_reading(ngx_http_request_t
 
 #endif
 
+#if (NGX_HTTP_QUIC)
+
+    if (c->quic) {
+        if (c->read->error) {
+            err = 0;
+            goto closed;
+        }
+
+        return;
+    }
+
+#endif
+
 #if (NGX_HAVE_KQUEUE)
 
     if (ngx_event_flags & NGX_USE_KQUEUE_EVENT) {
@@ -3420,11 +3572,13 @@ ngx_http_set_lingering_close(ngx_connect
         }
     }
 
-    if (ngx_shutdown_socket(c->fd, NGX_WRITE_SHUTDOWN) == -1) {
-        ngx_connection_error(c, ngx_socket_errno,
-                             ngx_shutdown_socket_n " failed");
-        ngx_http_close_request(r, 0);
-        return;
+    if (c->fd != NGX_INVALID_FILE) {
+        if (ngx_shutdown_socket(c->fd, NGX_WRITE_SHUTDOWN) == -1) {
+            ngx_connection_error(c, ngx_socket_errno,
+                                 ngx_shutdown_socket_n " failed");
+            ngx_http_close_request(r, 0);
+            return;
+        }
     }
 
     ngx_add_timer(rev, clcf->lingering_timeout);
@@ -3758,7 +3912,7 @@ ngx_http_close_connection(ngx_connection
 }
 
 
-static u_char *
+u_char *
 ngx_http_log_error(ngx_log_t *log, u_char *buf, size_t len)
 {
     u_char              *p;
@@ -3805,15 +3959,15 @@ ngx_http_log_error_handler(ngx_http_requ
     len -= p - buf;
     buf = p;
 
-    if (r->request_line.data == NULL && r->request_start) {
-        for (p = r->request_start; p < r->header_in->last; p++) {
+    if (r->request_line.data == NULL && r->parse_start) {
+        for (p = r->parse_start; p < r->header_in->last; p++) {
             if (*p == CR || *p == LF) {
                 break;
             }
         }
 
-        r->request_line.len = p - r->request_start;
-        r->request_line.data = r->request_start;
+        r->request_line.len = p - r->parse_start;
+        r->request_line.data = r->parse_start;
     }
 
     if (r->request_line.len) {
--- 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
@@ -577,12 +578,14 @@ struct ngx_http_request_s {
      * via ngx_http_ephemeral_t
      */
 
+    u_char                           *parse_start;
     u_char                           *uri_start;
     u_char                           *uri_end;
     u_char                           *uri_ext;
     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
@@ -333,11 +333,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;
             }
@@ -584,6 +583,12 @@ ngx_http_discard_request_body(ngx_http_r
     }
 #endif
 
+#if (NGX_HTTP_QUIC)
+    if (r->connection->quic) {
+        return NGX_OK;
+    }
+#endif
+
     if (ngx_http_test_expect(r) != NGX_OK) {
         return NGX_HTTP_INTERNAL_SERVER_ERROR;
     }
@@ -876,11 +881,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;
     }
@@ -955,6 +956,15 @@ ngx_http_request_body_length_filter(ngx_
             break;
         }
 
+        size = cl->buf->last - cl->buf->pos;
+
+        if (cl->buf->last_buf && (off_t) size < rb->rest) {
+            ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
+                          "client prematurely closed connection");
+            r->connection->error = 1;
+            return NGX_HTTP_BAD_REQUEST;
+        }
+
         tl = ngx_chain_get_free_buf(r->pool, &rb->free);
         if (tl == NULL) {
             return NGX_HTTP_INTERNAL_SERVER_ERROR;
@@ -972,8 +982,6 @@ ngx_http_request_body_length_filter(ngx_
         b->end = cl->buf->end;
         b->flush = r->request_body_no_buffering;
 
-        size = cl->buf->last - cl->buf->pos;
-
         if ((off_t) size < rb->rest) {
             cl->buf->pos = cl->buf->last;
             rb->rest -= size;
@@ -1045,7 +1053,16 @@ ngx_http_request_body_chunked_filter(ngx
                            cl->buf->file_pos,
                            cl->buf->file_last - cl->buf->file_pos);
 
-            rc = ngx_http_parse_chunked(r, cl->buf, rb->chunked);
+            switch (r->http_version) {
+#if (NGX_HTTP_V3)
+            case NGX_HTTP_VERSION_30:
+                rc = ngx_http_v3_parse_request_body(r, cl->buf, rb->chunked);
+                break;
+#endif
+
+            default: /* HTTP/1.x */
+                rc = ngx_http_parse_chunked(r, cl->buf, rb->chunked);
+            }
 
             if (rc == NGX_OK) {
 
@@ -1129,6 +1146,20 @@ ngx_http_request_body_chunked_filter(ngx
                 continue;
             }
 
+            if (rc == NGX_AGAIN && cl->buf->last_buf) {
+
+                /* last body buffer */
+
+                if (rb->chunked->length > 0) {
+                    ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
+                                  "client prematurely closed connection");
+                    r->connection->error = 1;
+                    return NGX_HTTP_BAD_REQUEST;
+                }
+
+                rc = NGX_DONE;
+            }
+
             if (rc == NGX_DONE) {
 
                 /* a whole response has been parsed successfully */
--- a/src/http/ngx_http_upstream.c
+++ b/src/http/ngx_http_upstream.c
@@ -523,6 +523,13 @@ ngx_http_upstream_init(ngx_http_request_
     }
 #endif
 
+#if (NGX_HTTP_QUIC)
+    if (c->quic) {
+        ngx_http_upstream_init_request(r);
+        return;
+    }
+#endif
+
     if (c->read->timer_set) {
         ngx_del_timer(c->read);
     }
@@ -1345,6 +1352,19 @@ ngx_http_upstream_check_broken_connectio
     }
 #endif
 
+#if (NGX_HTTP_QUIC)
+
+    if (c->quic) {
+        if (c->write->error) {
+            ngx_http_upstream_finalize_request(r, u,
+                                               NGX_HTTP_CLIENT_CLOSED_REQUEST);
+        }
+
+        return;
+    }
+
+#endif
+
 #if (NGX_HAVE_KQUEUE)
 
     if (ngx_event_flags & NGX_USE_KQUEUE_EVENT) {
new file mode 100644
--- /dev/null
+++ b/src/http/v3/ngx_http_v3.h
@@ -0,0 +1,205 @@
+
+/*
+ * 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_ADVERTISE                 "\x02h3"
+#define NGX_HTTP_V3_ALPN_DRAFT_FMT                 "\x05h3-%02uD"
+
+#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
+
+#define NGX_HTTP_V3_DEFAULT_MAX_FIELD_SIZE         4096
+#define NGX_HTTP_V3_DEFAULT_MAX_TABLE_CAPACITY     16384
+#define NGX_HTTP_V3_DEFAULT_MAX_BLOCKED_STREAMS    16
+#define NGX_HTTP_V3_DEFAULT_MAX_CONCURRENT_PUSHES  10
+
+/* HTTP/3 errors */
+#define NGX_HTTP_V3_ERR_NO_ERROR                   0x100
+#define NGX_HTTP_V3_ERR_GENERAL_PROTOCOL_ERROR     0x101
+#define NGX_HTTP_V3_ERR_INTERNAL_ERROR             0x102
+#define NGX_HTTP_V3_ERR_STREAM_CREATION_ERROR      0x103
+#define NGX_HTTP_V3_ERR_CLOSED_CRITICAL_STREAM     0x104
+#define NGX_HTTP_V3_ERR_FRAME_UNEXPECTED           0x105
+#define NGX_HTTP_V3_ERR_FRAME_ERROR                0x106
+#define NGX_HTTP_V3_ERR_EXCESSIVE_LOAD             0x107
+#define NGX_HTTP_V3_ERR_ID_ERROR                   0x108
+#define NGX_HTTP_V3_ERR_SETTINGS_ERROR             0x109
+#define NGX_HTTP_V3_ERR_MISSING_SETTINGS           0x10a
+#define NGX_HTTP_V3_ERR_REQUEST_REJECTED           0x10b
+#define NGX_HTTP_V3_ERR_REQUEST_CANCELLED          0x10c
+#define NGX_HTTP_V3_ERR_REQUEST_INCOMPLETE         0x10d
+#define NGX_HTTP_V3_ERR_CONNECT_ERROR              0x10f
+#define NGX_HTTP_V3_ERR_VERSION_FALLBACK           0x110
+
+/* QPACK errors */
+#define NGX_HTTP_V3_ERR_DECOMPRESSION_FAILED       0x200
+#define NGX_HTTP_V3_ERR_ENCODER_STREAM_ERROR       0x201
+#define NGX_HTTP_V3_ERR_DECODER_STREAM_ERROR       0x202
+
+
+#define ngx_http_v3_get_module_srv_conf(c, module)                            \
+    ngx_http_get_module_srv_conf(                                             \
+           ((ngx_http_v3_connection_t *) c->quic->parent->data)->hc.conf_ctx, \
+           module)
+
+#define ngx_http_v3_finalize_connection(c, code, reason)                      \
+    ngx_quic_finalize_connection(c->quic->parent, code, reason)
+
+
+typedef struct {
+    ngx_quic_tp_t                 quic;
+    size_t                        max_field_size;
+    size_t                        max_table_capacity;
+    ngx_uint_t                    max_blocked_streams;
+    ngx_uint_t                    max_concurrent_pushes;
+} ngx_http_v3_srv_conf_t;
+
+
+typedef struct {
+    ngx_flag_t                    push_preload;
+    ngx_flag_t                    push;
+    ngx_array_t                  *pushes;
+} ngx_http_v3_loc_conf_t;
+
+
+typedef struct {
+    ngx_str_t                     name;
+    ngx_str_t                     value;
+} ngx_http_v3_header_t;
+
+
+typedef struct {
+    ngx_http_v3_header_t        **elts;
+    ngx_uint_t                    nelts;
+    ngx_uint_t                    base;
+    size_t                        size;
+    size_t                        capacity;
+} ngx_http_v3_dynamic_table_t;
+
+
+typedef struct {
+    ngx_http_connection_t         hc;
+    ngx_http_v3_dynamic_table_t   table;
+
+    ngx_queue_t                   blocked;
+    ngx_uint_t                    nblocked;
+
+    ngx_queue_t                   pushing;
+    ngx_uint_t                    npushing;
+    uint64_t                      next_push_id;
+    uint64_t                      max_push_id;
+
+    ngx_uint_t                    settings_sent;
+                                               /* unsigned  settings_sent:1; */
+    ngx_connection_t             *known_streams[NGX_HTTP_V3_MAX_KNOWN_STREAM];
+} ngx_http_v3_connection_t;
+
+
+ngx_int_t ngx_http_v3_init_connection(ngx_connection_t *c);
+
+ngx_int_t ngx_http_v3_parse_request(ngx_http_request_t *r, ngx_buf_t *b);
+ngx_int_t ngx_http_v3_parse_header(ngx_http_request_t *r, ngx_buf_t *b,
+    ngx_uint_t allow_underscores);
+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);
+
+uintptr_t ngx_http_v3_encode_header_block_prefix(u_char *p,
+    ngx_uint_t insert_count, ngx_uint_t sign, ngx_uint_t delta_base);
+uintptr_t ngx_http_v3_encode_header_ri(u_char *p, ngx_uint_t dynamic,
+    ngx_uint_t index);
+uintptr_t ngx_http_v3_encode_header_lri(u_char *p, ngx_uint_t dynamic,
+    ngx_uint_t index, u_char *data, size_t len);
+uintptr_t ngx_http_v3_encode_header_l(u_char *p, ngx_str_t *name,
+    ngx_str_t *value);
+uintptr_t ngx_http_v3_encode_header_pbi(u_char *p, ngx_uint_t index);
+uintptr_t ngx_http_v3_encode_header_lpbi(u_char *p, ngx_uint_t index,
+    u_char *data, size_t len);
+
+ngx_connection_t *ngx_http_v3_create_push_stream(ngx_connection_t *c,
+    uint64_t push_id);
+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_int_t ngx_http_v3_lookup_static(ngx_connection_t *c, ngx_uint_t index,
+    ngx_str_t *name, ngx_str_t *value);
+ngx_int_t ngx_http_v3_lookup(ngx_connection_t *c, ngx_uint_t index,
+    ngx_str_t *name, ngx_str_t *value);
+ngx_int_t ngx_http_v3_decode_insert_count(ngx_connection_t *c,
+    ngx_uint_t *insert_count);
+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_set_max_push_id(ngx_connection_t *c,
+    uint64_t max_push_id);
+ngx_int_t ngx_http_v3_cancel_push(ngx_connection_t *c, uint64_t push_id);
+
+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_encode.c
@@ -0,0 +1,227 @@
+
+/*
+ * 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 4;
+        }
+
+        *p++ = 0x80 | (value >> 24);
+        *p++ = (value >> 16);
+        *p++ = (value >> 8);
+        *p++ = value;
+        return (uintptr_t) p;
+    }
+
+    if (p == NULL) {
+        return 8;
+    }
+
+    *p++ = 0xc0 | (value >> 56);
+    *p++ = (value >> 48);
+    *p++ = (value >> 40);
+    *p++ = (value >> 32);
+    *p++ = (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;
+
+    if (p == NULL) {
+        for (n = 2; value >= 128; n++) {
+            value >>= 7;
+        }
+
+        return n;
+    }
+
+    *p++ |= thresh;
+
+    while (value >= 128) {
+        *p++ = 0x80 | value;
+        value >>= 7;
+    }
+
+    *p++ = value;
+
+    return (uintptr_t) p;
+}
+
+
+uintptr_t
+ngx_http_v3_encode_header_block_prefix(u_char *p, ngx_uint_t insert_count,
+    ngx_uint_t sign, ngx_uint_t delta_base)
+{
+    if (p == NULL) {
+        return ngx_http_v3_encode_prefix_int(NULL, insert_count, 8)
+               + ngx_http_v3_encode_prefix_int(NULL, delta_base, 7);
+    }
+
+    *p = 0;
+    p = (u_char *) ngx_http_v3_encode_prefix_int(p, insert_count, 8);
+
+    *p = sign ? 0x80 : 0;
+    p = (u_char *) ngx_http_v3_encode_prefix_int(p, delta_base, 7);
+
+    return (uintptr_t) p;
+}
+
+
+uintptr_t
+ngx_http_v3_encode_header_ri(u_char *p, ngx_uint_t dynamic, ngx_uint_t index)
+{
+    /* Indexed Header Field */
+
+    if (p == NULL) {
+        return ngx_http_v3_encode_prefix_int(NULL, index, 6);
+    }
+
+    *p = dynamic ? 0x80 : 0xc0;
+
+    return ngx_http_v3_encode_prefix_int(p, index, 6);
+}
+
+
+uintptr_t
+ngx_http_v3_encode_header_lri(u_char *p, ngx_uint_t dynamic, ngx_uint_t index,
+    u_char *data, size_t len)
+{
+    /* Literal Header Field With Name Reference */
+
+    if (p == NULL) {
+        return ngx_http_v3_encode_prefix_int(NULL, index, 4)
+               + ngx_http_v3_encode_prefix_int(NULL, len, 7)
+               + len;
+    }
+
+    *p = dynamic ? 0x40 : 0x50;
+    p = (u_char *) ngx_http_v3_encode_prefix_int(p, index, 4);
+
+    *p = 0;
+    p = (u_char *) ngx_http_v3_encode_prefix_int(p, len, 7);
+
+    if (data) {
+        p = ngx_cpymem(p, data, len);
+    }
+
+    return (uintptr_t) p;
+}
+
+
+uintptr_t
+ngx_http_v3_encode_header_l(u_char *p, ngx_str_t *name, ngx_str_t *value)
+{
+    /* Literal Header Field Without Name Reference */
+
+    if (p == NULL) {
+        return ngx_http_v3_encode_prefix_int(NULL, name->len, 3)
+               + name->len
+               + ngx_http_v3_encode_prefix_int(NULL, value->len, 7)
+               + value->len;
+    }
+
+    *p = 0x20;
+    p = (u_char *) ngx_http_v3_encode_prefix_int(p, name->len, 3);
+
+    ngx_strlow(p, name->data, name->len);
+    p += name->len;
+
+    *p = 0;
+    p = (u_char *) ngx_http_v3_encode_prefix_int(p, value->len, 7);
+
+    p = ngx_cpymem(p, value->data, value->len);
+
+    return (uintptr_t) p;
+}
+
+
+uintptr_t
+ngx_http_v3_encode_header_pbi(u_char *p, ngx_uint_t index)
+{
+    /* Indexed Header Field With Post-Base Index */
+
+    if (p == NULL) {
+        return ngx_http_v3_encode_prefix_int(NULL, index, 4);
+    }
+
+    *p = 0x10;
+
+    return ngx_http_v3_encode_prefix_int(p, index, 4);
+}
+
+
+uintptr_t
+ngx_http_v3_encode_header_lpbi(u_char *p, ngx_uint_t index, u_char *data,
+    size_t len)
+{
+    /* Literal Header Field With Post-Base Name Reference */
+
+    if (p == NULL) {
+        return ngx_http_v3_encode_prefix_int(NULL, index, 3)
+               + ngx_http_v3_encode_prefix_int(NULL, len, 7)
+               + len;
+    }
+
+    *p = 0;
+    p = (u_char *) ngx_http_v3_encode_prefix_int(p, index, 3);
+
+    *p = 0;
+    p = (u_char *) ngx_http_v3_encode_prefix_int(p, len, 7);
+
+    if (data) {
+        p = ngx_cpymem(p, data, len);
+    }
+
+    return (uintptr_t) p;
+}
new file mode 100644
--- /dev/null
+++ b/src/http/v3/ngx_http_v3_module.c
@@ -0,0 +1,291 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ * Copyright (C) Roman Arutyunyan
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_http.h>
+
+
+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 void *ngx_http_v3_create_loc_conf(ngx_conf_t *cf);
+static char *ngx_http_v3_merge_loc_conf(ngx_conf_t *cf, void *parent,
+    void *child);
+static char *ngx_http_v3_push(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
+
+
+static ngx_command_t  ngx_http_v3_commands[] = {
+
+    { ngx_string("http3_max_field_size"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1,
+      ngx_conf_set_size_slot,
+      NGX_HTTP_SRV_CONF_OFFSET,
+      offsetof(ngx_http_v3_srv_conf_t, max_field_size),
+      NULL },
+
+    { ngx_string("http3_max_table_capacity"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1,
+      ngx_conf_set_size_slot,
+      NGX_HTTP_SRV_CONF_OFFSET,
+      offsetof(ngx_http_v3_srv_conf_t, max_table_capacity),
+      NULL },
+
+    { ngx_string("http3_max_blocked_streams"),
+      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, max_blocked_streams),
+      NULL },
+
+    { ngx_string("http3_max_concurrent_pushes"),
+      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, max_concurrent_pushes),
+      NULL },
+
+    { ngx_string("http3_push"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
+      ngx_http_v3_push,
+      NGX_HTTP_LOC_CONF_OFFSET,
+      0,
+      NULL },
+
+    { ngx_string("http3_push_preload"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
+      ngx_conf_set_flag_slot,
+      NGX_HTTP_LOC_CONF_OFFSET,
+      offsetof(ngx_http_v3_loc_conf_t, push_preload),
+      NULL },
+
+      ngx_null_command
+};
+
+
+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 */
+
+    ngx_http_v3_create_loc_conf,           /* create location configuration */
+    ngx_http_v3_merge_loc_conf             /* 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("http3"), NULL, ngx_http_variable_http3, 0, 0, 0 },
+
+      ngx_http_null_variable
+};
+
+
+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  *h3scf;
+
+    h3scf = ngx_pcalloc(cf->pool, sizeof(ngx_http_v3_srv_conf_t));
+    if (h3scf == NULL) {
+        return NULL;
+    }
+
+    h3scf->max_field_size = NGX_CONF_UNSET_SIZE;
+    h3scf->max_table_capacity = NGX_CONF_UNSET_SIZE;
+    h3scf->max_blocked_streams = NGX_CONF_UNSET_UINT;
+    h3scf->max_concurrent_pushes = NGX_CONF_UNSET_UINT;
+
+    return h3scf;
+}
+
+
+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_size_value(conf->max_field_size,
+                              prev->max_field_size,
+                              NGX_HTTP_V3_DEFAULT_MAX_FIELD_SIZE);
+
+    ngx_conf_merge_size_value(conf->max_table_capacity,
+                              prev->max_table_capacity,
+                              NGX_HTTP_V3_DEFAULT_MAX_TABLE_CAPACITY);
+
+    ngx_conf_merge_uint_value(conf->max_blocked_streams,
+                              prev->max_blocked_streams,
+                              NGX_HTTP_V3_DEFAULT_MAX_BLOCKED_STREAMS);
+
+    ngx_conf_merge_uint_value(conf->max_concurrent_pushes,
+                              prev->max_concurrent_pushes,
+                              NGX_HTTP_V3_DEFAULT_MAX_CONCURRENT_PUSHES);
+
+    return NGX_CONF_OK;
+}
+
+
+static void *
+ngx_http_v3_create_loc_conf(ngx_conf_t *cf)
+{
+    ngx_http_v3_loc_conf_t  *h3lcf;
+
+    h3lcf = ngx_pcalloc(cf->pool, sizeof(ngx_http_v3_loc_conf_t));
+    if (h3lcf == NULL) {
+        return NULL;
+    }
+
+    /*
+     * set by ngx_pcalloc():
+     *
+     *     h3lcf->pushes = NULL;
+     */
+
+    h3lcf->push_preload = NGX_CONF_UNSET;
+    h3lcf->push = NGX_CONF_UNSET;
+
+    return h3lcf;
+}
+
+
+static char *
+ngx_http_v3_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child)
+{
+    ngx_http_v3_loc_conf_t *prev = parent;
+    ngx_http_v3_loc_conf_t *conf = child;
+
+    ngx_conf_merge_value(conf->push, prev->push, 1);
+
+    if (conf->push && conf->pushes == NULL) {
+        conf->pushes = prev->pushes;
+    }
+
+    ngx_conf_merge_value(conf->push_preload, prev->push_preload, 0);
+
+    return NGX_CONF_OK;
+}
+
+
+static char *
+ngx_http_v3_push(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
+{
+    ngx_http_v3_loc_conf_t *h3lcf = conf;
+
+    ngx_str_t                         *value;
+    ngx_http_complex_value_t          *cv;
+    ngx_http_compile_complex_value_t   ccv;
+
+    value = cf->args->elts;
+
+    if (ngx_strcmp(value[1].data, "off") == 0) {
+
+        if (h3lcf->pushes) {
+            return "\"off\" parameter cannot be used with URI";
+        }
+
+        if (h3lcf->push == 0) {
+            return "is duplicate";
+        }
+
+        h3lcf->push = 0;
+        return NGX_CONF_OK;
+    }
+
+    if (h3lcf->push == 0) {
+        return "URI cannot be used with \"off\" parameter";
+    }
+
+    h3lcf->push = 1;
+
+    if (h3lcf->pushes == NULL) {
+        h3lcf->pushes = ngx_array_create(cf->pool, 1,
+                                         sizeof(ngx_http_complex_value_t));
+        if (h3lcf->pushes == NULL) {
+            return NGX_CONF_ERROR;
+        }
+    }
+
+    cv = ngx_array_push(h3lcf->pushes);
+    if (cv == NULL) {
+        return NGX_CONF_ERROR;
+    }
+
+    ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t));
+
+    ccv.cf = cf;
+    ccv.value = &value[1];
+    ccv.complex_value = cv;
+
+    if (ngx_http_compile_complex_value(&ccv) != NGX_OK) {
+        return NGX_CONF_ERROR;
+    }
+
+    return NGX_CONF_OK;
+}
new file mode 100644
--- /dev/null
+++ b/src/http/v3/ngx_http_v3_parse.c
@@ -0,0 +1,1642 @@
+
+/*
+ * Copyright (C) Roman Arutyunyan
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_http.h>
+
+
+#define ngx_http_v3_is_v2_frame(type)                                         \
+    ((type) == 0x02 || (type) == 0x06 || (type) == 0x08 || (type) == 0x09)
+
+
+static ngx_int_t ngx_http_v3_parse_lookup(ngx_connection_t *c,
+    ngx_uint_t dynamic, ngx_uint_t index, ngx_str_t *name, ngx_str_t *value);
+
+
+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)
+{
+    ngx_uint_t  mask;
+    enum {
+        sw_start = 0,
+        sw_value
+    };
+
+    switch (st->state) {
+
+    case sw_start:
+
+        mask = (1 << prefix) - 1;
+        st->value = ch & mask;
+
+        if (st->value != mask) {
+            goto done;
+        }
+
+        st->shift = 0;
+        st->state = sw_value;
+        break;
+
+    case sw_value:
+
+        st->value += (uint64_t) (ch & 0x7f) << st->shift;
+
+        if (st->shift == 56
+            && ((ch & 0x80) || (st->value & 0xc000000000000000)))
+        {
+            ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                          "client exceeded integer size limit");
+            return NGX_HTTP_V3_ERR_EXCESSIVE_LOAD;
+        }
+
+        if (ch & 0x80) {
+            st->shift += 7;
+            break;
+        }
+
+        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_type,
+        sw_length,
+        sw_skip,
+        sw_prefix,
+        sw_verify,
+        sw_header_rep,
+        sw_done
+    };
+
+    switch (st->state) {
+
+    case sw_start:
+
+        ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 parse headers");
+
+        st->state = sw_type;
+
+        /* fall through */
+
+    case sw_type:
+
+        rc = ngx_http_v3_parse_varlen_int(c, &st->vlint, ch);
+        if (rc != NGX_DONE) {
+            return rc;
+        }
+
+        st->type = st->vlint.value;
+
+        if (ngx_http_v3_is_v2_frame(st->type)) {
+            return NGX_HTTP_V3_ERR_FRAME_UNEXPECTED;
+        }
+
+        st->state = sw_length;
+        break;
+
+    case sw_length:
+
+        rc = ngx_http_v3_parse_varlen_int(c, &st->vlint, ch);
+        if (rc != NGX_DONE) {
+            return rc;
+        }
+
+        st->length = st->vlint.value;
+
+        ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 parse headers type:%ui, len:%ui",
+                       st->type, st->length);
+
+        if (st->type != NGX_HTTP_V3_FRAME_HEADERS) {
+            st->state = st->length > 0 ? sw_skip : sw_type;
+            break;
+        }
+
+        if (st->length == 0) {
+            return NGX_HTTP_V3_ERR_FRAME_ERROR;
+        }
+
+        st->state = sw_prefix;
+        break;
+
+    case sw_skip:
+
+        if (--st->length == 0) {
+            st->state = sw_type;
+        }
+
+        break;
+
+    case sw_prefix:
+
+        if (--st->length == 0) {
+            return NGX_HTTP_V3_ERR_FRAME_ERROR;
+        }
+
+        rc = ngx_http_v3_parse_header_block_prefix(c, &st->prefix, ch);
+        if (rc != NGX_DONE) {
+            return rc;
+        }
+
+        st->state = sw_verify;
+        break;
+
+    case sw_verify:
+
+        rc = ngx_http_v3_check_insert_count(c, st->prefix.insert_count);
+        if (rc != NGX_OK) {
+            return rc;
+        }
+
+        st->state = sw_header_rep;
+
+        /* fall through */
+
+    case sw_header_rep:
+
+        rc = ngx_http_v3_parse_header_rep(c, &st->header_rep, st->prefix.base,
+                                          ch);
+
+        if (--st->length == 0 && rc == NGX_AGAIN) {
+            return NGX_HTTP_V3_ERR_FRAME_ERROR;
+        }
+
+        if (rc != NGX_DONE) {
+            return rc;
+        }
+
+        if (st->length == 0) {
+            goto done;
+        }
+
+        return NGX_OK;
+    }
+
+    return NGX_AGAIN;
+
+done:
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 parse headers done");
+
+    if (st->prefix.insert_count > 0) {
+        if (ngx_http_v3_client_ack_header(c, c->quic->id) != NGX_OK) {
+            return NGX_ERROR;
+        }
+    }
+
+    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)
+{
+    ngx_int_t  rc;
+    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:
+
+        rc = ngx_http_v3_parse_prefix_int(c, &st->pint, 8, ch);
+        if (rc != NGX_DONE) {
+            return rc;
+        }
+
+        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:
+
+        rc = ngx_http_v3_parse_prefix_int(c, &st->pint, 7, ch);
+        if (rc != NGX_DONE) {
+            return rc;
+        }
+
+        st->delta_base = st->pint.value;
+        goto done;
+    }
+
+    return NGX_AGAIN;
+
+done:
+
+    rc = ngx_http_v3_decode_insert_count(c, &st->insert_count);
+    if (rc != NGX_OK) {
+        return rc;
+    }
+
+    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 "
+                  "insert_count:%ui, sign:%ui, delta_base:%ui, base:%ui",
+                  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_DONE) {
+        return rc;
+    }
+
+    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;
+    ngx_http_v3_srv_conf_t  *h3scf;
+    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;
+
+        h3scf = ngx_http_v3_get_module_srv_conf(c, ngx_http_v3_module);
+
+        if (n > h3scf->max_field_size) {
+            ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                          "client exceeded http3_max_field_size limit");
+            return NGX_HTTP_V3_ERR_EXCESSIVE_LOAD;
+        }
+
+        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_int_t  rc;
+    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:
+
+        rc = ngx_http_v3_parse_prefix_int(c, &st->pint, 6, ch);
+        if (rc != NGX_DONE) {
+            return rc;
+        }
+
+        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;
+    }
+
+    rc = ngx_http_v3_parse_lookup(c, st->dynamic, st->index, &st->name,
+                                  &st->value);
+    if (rc != NGX_OK) {
+        return rc;
+    }
+
+    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;
+    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:
+
+        rc = ngx_http_v3_parse_prefix_int(c, &st->pint, 4, ch);
+        if (rc != NGX_DONE) {
+            return rc;
+        }
+
+        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:
+
+        rc = ngx_http_v3_parse_prefix_int(c, &st->pint, 7, ch);
+        if (rc != NGX_DONE) {
+            return rc;
+        }
+
+        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_DONE) {
+            return rc;
+        }
+
+        st->value = st->literal.value;
+        goto done;
+    }
+
+    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;
+    }
+
+    rc = ngx_http_v3_parse_lookup(c, st->dynamic, st->index, &st->name, NULL);
+    if (rc != NGX_OK) {
+        return rc;
+    }
+
+    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:
+
+        rc = ngx_http_v3_parse_prefix_int(c, &st->pint, 3, ch);
+        if (rc != NGX_DONE) {
+            return rc;
+        }
+
+        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_DONE) {
+            return rc;
+        }
+
+        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:
+
+        rc = ngx_http_v3_parse_prefix_int(c, &st->pint, 7, ch);
+        if (rc != NGX_DONE) {
+            return rc;
+        }
+
+        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_DONE) {
+            return rc;
+        }
+
+        st->value = st->literal.value;
+        goto done;
+    }
+
+    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_int_t  rc;
+    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:
+
+        rc = ngx_http_v3_parse_prefix_int(c, &st->pint, 4, ch);
+        if (rc != NGX_DONE) {
+            return rc;
+        }
+
+        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);
+
+    rc = ngx_http_v3_parse_lookup(c, 1, st->base + st->index, &st->name,
+                                  &st->value);
+    if (rc != NGX_OK) {
+        return rc;
+    }
+
+    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;
+    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:
+
+        rc = ngx_http_v3_parse_prefix_int(c, &st->pint, 3, ch);
+        if (rc != NGX_DONE) {
+            return rc;
+        }
+
+        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:
+
+        rc = ngx_http_v3_parse_prefix_int(c, &st->pint, 7, ch);
+        if (rc != NGX_DONE) {
+            return rc;
+        }
+
+        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_DONE) {
+            return rc;
+        }
+
+        st->value = st->literal.value;
+        goto done;
+    }
+
+    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);
+
+    rc = ngx_http_v3_parse_lookup(c, 1, st->base + st->index, &st->name, NULL);
+    if (rc != NGX_OK) {
+        return rc;
+    }
+
+    st->state = sw_start;
+    return NGX_DONE;
+}
+
+
+static ngx_int_t
+ngx_http_v3_parse_lookup(ngx_connection_t *c, ngx_uint_t dynamic,
+    ngx_uint_t index, ngx_str_t *name, ngx_str_t *value)
+{
+    u_char  *p;
+
+    if (!dynamic) {
+        if (ngx_http_v3_lookup_static(c, index, name, value) != NGX_OK) {
+            return NGX_HTTP_V3_ERR_DECOMPRESSION_FAILED;
+        }
+
+        return NGX_OK;
+    }
+
+    if (ngx_http_v3_lookup(c, index, name, value) != NGX_OK) {
+        return NGX_HTTP_V3_ERR_DECOMPRESSION_FAILED;
+    }
+
+    if (name) {
+        p = ngx_pnalloc(c->pool, name->len + 1);
+        if (p == NULL) {
+            return NGX_ERROR;
+        }
+
+        ngx_memcpy(p, name->data, name->len);
+        p[name->len] = '\0';
+        name->data = p;
+    }
+
+    if (value) {
+        p = ngx_pnalloc(c->pool, value->len + 1);
+        if (p == NULL) {
+            return NGX_ERROR;
+        }
+
+        ngx_memcpy(p, value->data, value->len);
+        p[value->len] = '\0';
+        value->data = p;
+    }
+
+    return NGX_OK;
+}
+
+
+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_first_type,
+        sw_type,
+        sw_length,
+        sw_cancel_push,
+        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_first_type;
+
+        /* fall through */
+
+    case sw_first_type:
+    case sw_type:
+
+        rc = ngx_http_v3_parse_varlen_int(c, &st->vlint, ch);
+        if (rc != NGX_DONE) {
+            return rc;
+        }
+
+        st->type = st->vlint.value;
+
+        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 parse frame type:%ui", st->type);
+
+        if (ngx_http_v3_is_v2_frame(st->type)) {
+            return NGX_HTTP_V3_ERR_FRAME_UNEXPECTED;
+        }
+
+        if (st->state == sw_first_type
+            && st->type != NGX_HTTP_V3_FRAME_SETTINGS)
+        {
+            return NGX_HTTP_V3_ERR_MISSING_SETTINGS;
+        }
+
+        st->state = sw_length;
+        break;
+
+    case sw_length:
+
+        rc = ngx_http_v3_parse_varlen_int(c, &st->vlint, ch);
+        if (rc != NGX_DONE) {
+            return rc;
+        }
+
+        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_CANCEL_PUSH:
+            st->state = sw_cancel_push;
+            break;
+
+        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_cancel_push:
+
+        rc = ngx_http_v3_parse_varlen_int(c, &st->vlint, ch);
+
+        if (--st->length == 0 && rc == NGX_AGAIN) {
+            return NGX_HTTP_V3_ERR_FRAME_ERROR;
+        }
+
+        if (rc != NGX_DONE) {
+            return rc;
+        }
+
+        rc = ngx_http_v3_cancel_push(c, st->vlint.value);
+        if (rc != NGX_OK) {
+            return rc;
+        }
+
+        st->state = sw_type;
+        break;
+
+    case sw_settings:
+
+        rc = ngx_http_v3_parse_settings(c, &st->settings, ch);
+
+        if (--st->length == 0 && rc == NGX_AGAIN) {
+            return NGX_HTTP_V3_ERR_SETTINGS_ERROR;
+        }
+
+        if (rc != NGX_DONE) {
+            return rc;
+        }
+
+        if (st->length == 0) {
+            st->state = sw_type;
+        }
+
+        break;
+
+    case sw_max_push_id:
+
+        rc = ngx_http_v3_parse_varlen_int(c, &st->vlint, ch);
+
+        if (--st->length == 0 && rc == NGX_AGAIN) {
+            return NGX_HTTP_V3_ERR_FRAME_ERROR;
+        }
+
+        if (rc != NGX_DONE) {
+            return rc;
+        }
+
+        rc = ngx_http_v3_set_max_push_id(c, st->vlint.value);
+        if (rc != NGX_OK) {
+            return rc;
+        }
+
+        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)
+{
+    ngx_int_t  rc;
+    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:
+
+        rc = ngx_http_v3_parse_varlen_int(c, &st->vlint, ch);
+        if (rc != NGX_DONE) {
+            return rc;
+        }
+
+        st->id = st->vlint.value;
+        st->state = sw_value;
+        break;
+
+    case sw_value:
+
+        rc = ngx_http_v3_parse_varlen_int(c, &st->vlint, ch);
+        if (rc != NGX_DONE) {
+            return rc;
+        }
+
+        if (ngx_http_v3_set_param(c, st->id, st->vlint.value) != NGX_OK) {
+            return NGX_HTTP_V3_ERR_SETTINGS_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_DONE) {
+            return rc;
+        }
+
+        goto done;
+
+    case sw_iwnr:
+
+        rc = ngx_http_v3_parse_header_iwnr(c, &st->header, ch);
+        if (rc != NGX_DONE) {
+            return rc;
+        }
+
+        goto done;
+
+    case sw_capacity:
+
+        rc = ngx_http_v3_parse_prefix_int(c, &st->pint, 5, ch);
+        if (rc != NGX_DONE) {
+            return rc;
+        }
+
+        rc = ngx_http_v3_set_capacity(c, st->pint.value);
+        if (rc != NGX_OK) {
+            return rc;
+        }
+
+        goto done;
+
+    case sw_duplicate:
+
+        rc = ngx_http_v3_parse_prefix_int(c, &st->pint, 5, ch);
+        if (rc != NGX_DONE) {
+            return rc;
+        }
+
+        rc = ngx_http_v3_duplicate(c, st->pint.value);
+        if (rc != NGX_OK) {
+            return rc;
+        }
+
+        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_AGAIN;
+}
+
+
+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:
+
+        rc = ngx_http_v3_parse_prefix_int(c, &st->pint, 6, ch);
+        if (rc != NGX_DONE) {
+            return rc;
+        }
+
+        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:
+
+        rc = ngx_http_v3_parse_prefix_int(c, &st->pint, 7, ch);
+        if (rc != NGX_DONE) {
+            return rc;
+        }
+
+        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_DONE) {
+            return rc;
+        }
+
+        st->value = st->literal.value;
+        goto done;
+    }
+
+    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);
+
+    rc = ngx_http_v3_ref_insert(c, st->dynamic, st->index, &st->value);
+    if (rc != NGX_OK) {
+        return rc;
+    }
+
+    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:
+
+        rc = ngx_http_v3_parse_prefix_int(c, &st->pint, 5, ch);
+        if (rc != NGX_DONE) {
+            return rc;
+        }
+
+        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_DONE) {
+            return rc;
+        }
+
+        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:
+
+        rc = ngx_http_v3_parse_prefix_int(c, &st->pint, 7, ch);
+        if (rc != NGX_DONE) {
+            return rc;
+        }
+
+        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_DONE) {
+            return rc;
+        }
+
+        st->value = st->literal.value;
+        goto done;
+    }
+
+    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);
+
+    rc = ngx_http_v3_insert(c, &st->name, &st->value);
+    if (rc != NGX_OK) {
+        return rc;
+    }
+
+    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;
+
+    ngx_int_t  rc;
+    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:
+
+        rc = ngx_http_v3_parse_prefix_int(c, &st->pint, 7, ch);
+        if (rc != NGX_DONE) {
+            return rc;
+        }
+
+        rc = ngx_http_v3_ack_header(c, st->pint.value);
+        if (rc != NGX_OK) {
+            return rc;
+        }
+
+        goto done;
+
+    case sw_cancel_stream:
+
+        rc = ngx_http_v3_parse_prefix_int(c, &st->pint, 6, ch);
+        if (rc != NGX_DONE) {
+            return rc;
+        }
+
+        rc = ngx_http_v3_cancel_stream(c, st->pint.value);
+        if (rc != NGX_OK) {
+            return rc;
+        }
+
+        goto done;
+
+    case sw_inc_insert_count:
+
+        rc = ngx_http_v3_parse_prefix_int(c, &st->pint, 6, ch);
+        if (rc != NGX_DONE) {
+            return rc;
+        }
+
+        rc = ngx_http_v3_inc_insert_count(c, st->pint.value);
+        if (rc != NGX_OK) {
+            return rc;
+        }
+
+        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_AGAIN;
+}
+
+
+ngx_int_t
+ngx_http_v3_parse_data(ngx_connection_t *c, ngx_http_v3_parse_data_t *st,
+    u_char ch)
+{
+    ngx_int_t  rc;
+    enum {
+        sw_start = 0,
+        sw_type,
+        sw_length,
+        sw_skip
+    };
+
+    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:
+
+        rc = ngx_http_v3_parse_varlen_int(c, &st->vlint, ch);
+        if (rc != NGX_DONE) {
+            return rc;
+        }
+
+        st->type = st->vlint.value;
+
+        if (st->type == NGX_HTTP_V3_FRAME_HEADERS) {
+            /* trailers */
+            goto done;
+        }
+
+        if (ngx_http_v3_is_v2_frame(st->type)) {
+            return NGX_HTTP_V3_ERR_FRAME_UNEXPECTED;
+        }
+
+        st->state = sw_length;
+        break;
+
+    case sw_length:
+
+        rc = ngx_http_v3_parse_varlen_int(c, &st->vlint, ch);
+        if (rc != NGX_DONE) {
+            return rc;
+        }
+
+        st->length = st->vlint.value;
+
+        ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 parse data type:%ui, len:%ui",
+                       st->type, st->length);
+
+        if (st->type != NGX_HTTP_V3_FRAME_DATA && st->length > 0) {
+            st->state = sw_skip;
+            break;
+        }
+
+        st->state = sw_type;
+        return NGX_OK;
+
+    case sw_skip:
+
+        if (--st->length == 0) {
+            st->state = sw_type;
+        }
+
+        break;
+    }
+
+    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,167 @@
+
+/*
+ * 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                      shift;
+    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                      type;
+    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                      type;
+    ngx_uint_t                      length;
+    ngx_http_v3_parse_varlen_int_t  vlint;
+} ngx_http_v3_parse_data_t;
+
+
+/*
+ * Parse functions return codes:
+ *   NGX_DONE - parsing done
+ *   NGX_OK - sub-element done
+ *   NGX_AGAIN - more data expected
+ *   NGX_BUSY - waiting for external event
+ *   NGX_ERROR - internal error
+ *   NGX_HTTP_V3_ERROR_XXX - HTTP/3 or QPACK error
+ */
+
+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,1549 @@
+
+/*
+ * Copyright (C) Roman Arutyunyan
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_http.h>
+
+
+/* static table indices */
+#define NGX_HTTP_V3_HEADER_AUTHORITY                 0
+#define NGX_HTTP_V3_HEADER_PATH_ROOT                 1
+#define NGX_HTTP_V3_HEADER_CONTENT_LENGTH_ZERO       4
+#define NGX_HTTP_V3_HEADER_DATE                      6
+#define NGX_HTTP_V3_HEADER_LAST_MODIFIED             10
+#define NGX_HTTP_V3_HEADER_LOCATION                  12
+#define NGX_HTTP_V3_HEADER_METHOD_GET                17
+#define NGX_HTTP_V3_HEADER_SCHEME_HTTP               22
+#define NGX_HTTP_V3_HEADER_SCHEME_HTTPS              23
+#define NGX_HTTP_V3_HEADER_STATUS_200                25
+#define NGX_HTTP_V3_HEADER_ACCEPT_ENCODING           31
+#define NGX_HTTP_V3_HEADER_CONTENT_TYPE_TEXT_PLAIN   53
+#define NGX_HTTP_V3_HEADER_VARY_ACCEPT_ENCODING      59
+#define NGX_HTTP_V3_HEADER_ACCEPT_LANGUAGE           72
+#define NGX_HTTP_V3_HEADER_SERVER                    92
+#define NGX_HTTP_V3_HEADER_USER_AGENT                95
+
+
+static ngx_int_t ngx_http_v3_process_pseudo_header(ngx_http_request_t *r,
+    ngx_str_t *name, ngx_str_t *value);
+static ngx_int_t ngx_http_v3_push_resources(ngx_http_request_t *r,
+    ngx_chain_t ***out);
+static ngx_int_t ngx_http_v3_push_resource(ngx_http_request_t *r,
+    ngx_str_t *path, ngx_chain_t ***out);
+static ngx_int_t ngx_http_v3_create_push_request(
+    ngx_http_request_t *pr, ngx_str_t *path, uint64_t push_id);
+static ngx_int_t ngx_http_v3_set_push_header(ngx_http_request_t *r,
+    const char *name, ngx_str_t *value);
+static void ngx_http_v3_push_request_handler(ngx_event_t *ev);
+static ngx_chain_t *ngx_http_v3_create_push_promise(ngx_http_request_t *r,
+    ngx_str_t *path, uint64_t push_id);
+
+
+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_request(ngx_http_request_t *r, ngx_buf_t *b)
+{
+    size_t                        len;
+    u_char                       *p;
+    ngx_int_t                     rc, n;
+    ngx_str_t                    *name, *value;
+    ngx_connection_t             *c;
+    ngx_http_v3_parse_headers_t  *st;
+
+    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;
+        r->parse_start = b->pos;
+        r->state = 1;
+    }
+
+    while (b->pos < b->last) {
+        rc = ngx_http_v3_parse_headers(c, st, *b->pos);
+
+        if (rc > 0) {
+            ngx_http_v3_finalize_connection(c, rc,
+                                            "could not parse request headers");
+            goto failed;
+        }
+
+        if (rc == NGX_ERROR) {
+            goto failed;
+        }
+
+        if (rc == NGX_BUSY) {
+            return NGX_BUSY;
+        }
+
+        b->pos++;
+
+        if (rc == NGX_AGAIN) {
+            continue;
+        }
+
+        name = &st->header_rep.header.name;
+        value = &st->header_rep.header.value;
+
+        n = ngx_http_v3_process_pseudo_header(r, name, value);
+
+        if (n == NGX_ERROR) {
+            goto failed;
+        }
+
+        if (n == NGX_OK && rc == NGX_OK) {
+            continue;
+        }
+
+        ngx_str_set(&r->http_protocol, "HTTP/3.0");
+
+        len = (r->method_end - r->method_start) + 1
+            + (r->uri_end - r->uri_start) + 1
+            + sizeof("HTTP/3") - 1;
+
+        p = ngx_pnalloc(c->pool, len);
+        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;
+        r->state = 0;
+
+        return NGX_OK;
+    }
+
+    return NGX_AGAIN;
+
+failed:
+
+    return NGX_HTTP_PARSE_INVALID_REQUEST;
+}
+
+
+ngx_int_t
+ngx_http_v3_parse_header(ngx_http_request_t *r, ngx_buf_t *b,
+    ngx_uint_t allow_underscores)
+{
+    u_char                        ch;
+    ngx_int_t                     rc;
+    ngx_str_t                    *name, *value;
+    ngx_uint_t                    hash, i, n;
+    ngx_connection_t             *c;
+    ngx_http_v3_parse_headers_t  *st;
+    enum {
+        sw_start = 0,
+        sw_done,
+        sw_next,
+        sw_header
+    };
+
+    c = r->connection;
+    st = r->h3_parse;
+
+    switch (r->state) {
+
+    case sw_start:
+        r->parse_start = b->pos;
+
+        if (st->state) {
+            r->state = sw_next;
+            goto done;
+        }
+
+        name = &st->header_rep.header.name;
+
+        if (name->len && name->data[0] != ':') {
+            r->state = sw_done;
+            goto done;
+        }
+
+        /* fall through */
+
+    case sw_done:
+        ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 parse header done");
+        return NGX_HTTP_PARSE_HEADER_DONE;
+
+    case sw_next:
+        r->parse_start = b->pos;
+        r->invalid_header = 0;
+        break;
+
+    case sw_header:
+        break;
+    }
+
+    while (b->pos < b->last) {
+        rc = ngx_http_v3_parse_headers(c, st, *b->pos++);
+
+        if (rc > 0) {
+            ngx_http_v3_finalize_connection(c, rc,
+                                            "could not parse request headers");
+            return NGX_HTTP_PARSE_INVALID_HEADER;
+        }
+
+        if (rc == NGX_ERROR) {
+            return NGX_HTTP_PARSE_INVALID_HEADER;
+        }
+
+        if (rc == NGX_DONE) {
+            r->state = sw_done;
+            goto done;
+        }
+
+        if (rc == NGX_OK) {
+            r->state = sw_next;
+            goto done;
+        }
+    }
+
+    r->state = sw_header;
+    return NGX_AGAIN;
+
+done:
+
+    name = &st->header_rep.header.name;
+    value = &st->header_rep.header.value;
+
+    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;
+
+    hash = 0;
+    i = 0;
+
+    for (n = 0; n < name->len; n++) {
+        ch = name->data[n];
+
+        if (ch >= 'A' && ch <= 'Z') {
+            /*
+             * A request or response containing uppercase
+             * header field names MUST be treated as malformed
+             */
+            return NGX_HTTP_PARSE_INVALID_HEADER;
+        }
+
+        if (ch == '\0') {
+            return NGX_HTTP_PARSE_INVALID_HEADER;
+        }
+
+        if (ch == '_' && !allow_underscores) {
+            r->invalid_header = 1;
+            continue;
+        }
+
+        if ((ch < 'a' || ch > 'z')
+            && (ch < '0' || ch > '9')
+            && ch != '-' && ch != '_')
+        {
+            r->invalid_header = 1;
+            continue;
+        }
+
+        hash = ngx_hash(hash, ch);
+        r->lowcase_header[i++] = ch;
+        i &= (NGX_HTTP_LC_HEADER_LEN - 1);
+    }
+
+    r->header_hash = hash;
+    r->lowcase_index = i;
+
+    return NGX_OK;
+}
+
+
+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;
+    enum {
+        sw_start = 0,
+        sw_skip
+    };
+
+    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;
+        }
+
+        ctx->h3_parse = st;
+    }
+
+    while (b->pos < b->last && ctx->size == 0) {
+
+        rc = ngx_http_v3_parse_data(c, st, *b->pos++);
+
+        if (rc > 0) {
+            ngx_http_v3_finalize_connection(c, rc,
+                                            "could not parse request body");
+            goto failed;
+        }
+
+        if (rc == NGX_ERROR) {
+            goto failed;
+        }
+
+        if (rc == NGX_AGAIN) {
+            ctx->state = sw_skip;
+            continue;
+        }
+
+        if (rc == NGX_DONE) {
+            return NGX_DONE;
+        }
+
+        /* rc == NGX_OK */
+
+        ctx->size = st->length;
+        ctx->state = sw_start;
+    }
+
+    if (ctx->state == sw_skip) {
+        ctx->length = 1;
+        return NGX_AGAIN;
+    }
+
+    if (b->pos == b->last) {
+        ctx->length = ctx->size;
+        return NGX_AGAIN;
+    }
+
+    return NGX_OK;
+
+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_str_t                  host;
+    ngx_uint_t                 i, port;
+    ngx_chain_t               *out, *hl, *cl, **ll;
+    ngx_list_part_t           *part;
+    ngx_table_elt_t           *header;
+    ngx_connection_t          *c;
+    ngx_http_core_loc_conf_t  *clcf;
+    ngx_http_core_srv_conf_t  *cscf;
+    u_char                     addr[NGX_SOCKADDR_STRLEN];
+
+    c = r->connection;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 create header");
+
+    out = NULL;
+    ll = &out;
+
+    if ((c->quic->id & NGX_QUIC_STREAM_UNIDIRECTIONAL) == 0
+        && r->method != NGX_HTTP_HEAD)
+    {
+        if (ngx_http_v3_push_resources(r, &ll) != NGX_OK) {
+            return NULL;
+        }
+    }
+
+    len = ngx_http_v3_encode_header_block_prefix(NULL, 0, 0, 0);
+
+    if (r->headers_out.status == NGX_HTTP_OK) {
+        len += ngx_http_v3_encode_header_ri(NULL, 0,
+                                            NGX_HTTP_V3_HEADER_STATUS_200);
+
+    } else {
+        len += ngx_http_v3_encode_header_lri(NULL, 0,
+                                             NGX_HTTP_V3_HEADER_STATUS_200,
+                                             NULL, 3);
+    }
+
+    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_header_lri(NULL, 0,
+                                             NGX_HTTP_V3_HEADER_SERVER,
+                                             NULL, n);
+    }
+
+    if (r->headers_out.date == NULL) {
+        len += ngx_http_v3_encode_header_lri(NULL, 0, NGX_HTTP_V3_HEADER_DATE,
+                                             NULL, 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_header_lri(NULL, 0,
+                                    NGX_HTTP_V3_HEADER_CONTENT_TYPE_TEXT_PLAIN,
+                                    NULL, n);
+    }
+
+    if (r->headers_out.content_length == NULL) {
+        if (r->headers_out.content_length_n > 0) {
+            len += ngx_http_v3_encode_header_lri(NULL, 0,
+                                        NGX_HTTP_V3_HEADER_CONTENT_LENGTH_ZERO,
+                                        NULL, NGX_OFF_T_LEN);
+
+        } else if (r->headers_out.content_length_n == 0) {
+            len += ngx_http_v3_encode_header_ri(NULL, 0,
+                                       NGX_HTTP_V3_HEADER_CONTENT_LENGTH_ZERO);
+        }
+    }
+
+    if (r->headers_out.last_modified == NULL
+        && r->headers_out.last_modified_time != -1)
+    {
+        len += ngx_http_v3_encode_header_lri(NULL, 0,
+                                  NGX_HTTP_V3_HEADER_LAST_MODIFIED, NULL,
+                                  sizeof("Mon, 28 Sep 1970 06:00:00 GMT") - 1);
+    }
+
+    if (r->headers_out.location
+        && r->headers_out.location->value.len
+        && r->headers_out.location->value.data[0] == '/'
+        && clcf->absolute_redirect)
+    {
+        r->headers_out.location->hash = 0;
+
+        if (clcf->server_name_in_redirect) {
+            cscf = ngx_http_get_module_srv_conf(r, ngx_http_core_module);
+            host = cscf->server_name;
+
+        } else if (r->headers_in.server.len) {
+            host = r->headers_in.server;
+
+        } else {
+            host.len = NGX_SOCKADDR_STRLEN;
+            host.data = addr;
+
+            if (ngx_connection_local_sockaddr(c, &host, 0) != NGX_OK) {
+                return NULL;
+            }
+        }
+
+        port = ngx_inet_get_port(c->local_sockaddr);
+
+        n = sizeof("https://") - 1 + host.len
+            + r->headers_out.location->value.len;
+
+        if (clcf->port_in_redirect) {
+            port = (port == 443) ? 0 : port;
+
+        } else {
+            port = 0;
+        }
+
+        if (port) {
+            n += sizeof(":65535") - 1;
+        }
+
+        len += ngx_http_v3_encode_header_lri(NULL, 0,
+                                         NGX_HTTP_V3_HEADER_LOCATION, NULL, n);
+
+    } else {
+        ngx_str_null(&host);
+        port = 0;
+    }
+
+#if (NGX_HTTP_GZIP)
+    if (r->gzip_vary) {
+        if (clcf->gzip_vary) {
+            len += ngx_http_v3_encode_header_ri(NULL, 0,
+                                      NGX_HTTP_V3_HEADER_VARY_ACCEPT_ENCODING);
+
+        } 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_header_l(NULL, &header[i].key,
+                                           &header[i].value);
+    }
+
+    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 = (u_char *) ngx_http_v3_encode_header_block_prefix(b->last,
+                                                                0, 0, 0);
+
+    if (r->headers_out.status == NGX_HTTP_OK) {
+        b->last = (u_char *) ngx_http_v3_encode_header_ri(b->last, 0,
+                                                NGX_HTTP_V3_HEADER_STATUS_200);
+
+    } else {
+        b->last = (u_char *) ngx_http_v3_encode_header_lri(b->last, 0,
+                                                 NGX_HTTP_V3_HEADER_STATUS_200,
+                                                 NULL, 3);
+        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;
+        }
+
+        b->last = (u_char *) ngx_http_v3_encode_header_lri(b->last, 0,
+                                                     NGX_HTTP_V3_HEADER_SERVER,
+                                                     p, n);
+    }
+
+    if (r->headers_out.date == NULL) {
+        b->last = (u_char *) ngx_http_v3_encode_header_lri(b->last, 0,
+                                                     NGX_HTTP_V3_HEADER_DATE,
+                                                     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;
+        }
+
+        b->last = (u_char *) ngx_http_v3_encode_header_lri(b->last, 0,
+                                    NGX_HTTP_V3_HEADER_CONTENT_TYPE_TEXT_PLAIN,
+                                    NULL, n);
+
+        p = b->last;
+        b->last = ngx_cpymem(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_cpymem(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 == NULL) {
+        if (r->headers_out.content_length_n > 0) {
+            p = ngx_sprintf(b->last, "%O", r->headers_out.content_length_n);
+            n = p - b->last;
+
+            b->last = (u_char *) ngx_http_v3_encode_header_lri(b->last, 0,
+                                        NGX_HTTP_V3_HEADER_CONTENT_LENGTH_ZERO,
+                                        NULL, n);
+
+            b->last = ngx_sprintf(b->last, "%O",
+                                  r->headers_out.content_length_n);
+
+        } else if (r->headers_out.content_length_n == 0) {
+            b->last = (u_char *) ngx_http_v3_encode_header_ri(b->last, 0,
+                                       NGX_HTTP_V3_HEADER_CONTENT_LENGTH_ZERO);
+        }
+    }
+
+    if (r->headers_out.last_modified == NULL
+        && r->headers_out.last_modified_time != -1)
+    {
+        b->last = (u_char *) ngx_http_v3_encode_header_lri(b->last, 0,
+                                  NGX_HTTP_V3_HEADER_LAST_MODIFIED, NULL,
+                                  sizeof("Mon, 28 Sep 1970 06:00:00 GMT") - 1);
+
+        b->last = ngx_http_time(b->last, r->headers_out.last_modified_time);
+    }
+
+    if (host.data) {
+        n = sizeof("https://") - 1 + host.len
+            + r->headers_out.location->value.len;
+
+        if (port) {
+            n += ngx_sprintf(b->last, ":%ui", port) - b->last;
+        }
+
+        b->last = (u_char *) ngx_http_v3_encode_header_lri(b->last, 0,
+                                                   NGX_HTTP_V3_HEADER_LOCATION,
+                                                   NULL, n);
+
+        p = b->last;
+        b->last = ngx_cpymem(b->last, "https://", sizeof("https://") - 1);
+        b->last = ngx_cpymem(b->last, host.data, host.len);
+
+        if (port) {
+            b->last = ngx_sprintf(b->last, ":%ui", port);
+        }
+
+        b->last = ngx_cpymem(b->last, r->headers_out.location->value.data,
+                             r->headers_out.location->value.len);
+
+        /* update r->headers_out.location->value for possible logging */
+
+        r->headers_out.location->value.len = b->last - p;
+        r->headers_out.location->value.data = p;
+        ngx_str_set(&r->headers_out.location->key, "Location");
+    }
+
+#if (NGX_HTTP_GZIP)
+    if (r->gzip_vary) {
+        b->last = (u_char *) ngx_http_v3_encode_header_ri(b->last, 0,
+                                      NGX_HTTP_V3_HEADER_VARY_ACCEPT_ENCODING);
+    }
+#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 = (u_char *) ngx_http_v3_encode_header_l(b->last,
+                                                         &header[i].key,
+                                                         &header[i].value);
+    }
+
+    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 = ngx_http_v3_encode_varlen_int(NULL, NGX_HTTP_V3_FRAME_HEADERS)
+          + ngx_http_v3_encode_varlen_int(NULL, n);
+
+    b = ngx_create_temp_buf(c->pool, len);
+    if (b == NULL) {
+        return NULL;
+    }
+
+    b->last = (u_char *) ngx_http_v3_encode_varlen_int(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;
+
+    *ll = hl;
+    ll = &cl->next;
+
+    if (r->headers_out.content_length_n >= 0 && !r->header_only) {
+        len = ngx_http_v3_encode_varlen_int(NULL, NGX_HTTP_V3_FRAME_DATA)
+              + ngx_http_v3_encode_varlen_int(NULL,
+                                              r->headers_out.content_length_n);
+
+        b = ngx_create_temp_buf(c->pool, len);
+        if (b == NULL) {
+            return NULL;
+        }
+
+        b->last = (u_char *) ngx_http_v3_encode_varlen_int(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);
+
+        cl = ngx_alloc_chain_link(c->pool);
+        if (cl == NULL) {
+            return NULL;
+        }
+
+        cl->buf = b;
+        cl->next = NULL;
+
+        *ll = cl;
+    }
+
+    return out;
+}
+
+
+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;
+}
+
+
+static ngx_int_t
+ngx_http_v3_push_resources(ngx_http_request_t *r, ngx_chain_t ***out)
+{
+    u_char                     *start, *end, *last;
+    ngx_str_t                   path;
+    ngx_int_t                   rc;
+    ngx_uint_t                  i, push;
+    ngx_table_elt_t           **h;
+    ngx_http_v3_loc_conf_t     *h3lcf;
+    ngx_http_complex_value_t   *pushes;
+
+    h3lcf = ngx_http_get_module_loc_conf(r, ngx_http_v3_module);
+
+    if (h3lcf->pushes) {
+        pushes = h3lcf->pushes->elts;
+
+        for (i = 0; i < h3lcf->pushes->nelts; i++) {
+
+            if (ngx_http_complex_value(r, &pushes[i], &path) != NGX_OK) {
+                return NGX_ERROR;
+            }
+
+            if (path.len == 0) {
+                continue;
+            }
+
+            if (path.len == 3 && ngx_strncmp(path.data, "off", 3) == 0) {
+                continue;
+            }
+
+            rc = ngx_http_v3_push_resource(r, &path, out);
+
+            if (rc == NGX_ERROR) {
+                return NGX_ERROR;
+            }
+
+            if (rc == NGX_ABORT) {
+                return NGX_OK;
+            }
+
+            /* NGX_OK, NGX_DECLINED */
+        }
+    }
+
+    if (!h3lcf->push_preload) {
+        return NGX_OK;
+    }
+
+    h = r->headers_out.link.elts;
+
+    for (i = 0; i < r->headers_out.link.nelts; i++) {
+
+        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
+                       "http3 parse link: \"%V\"", &h[i]->value);
+
+        start = h[i]->value.data;
+        end = h[i]->value.data + h[i]->value.len;
+
+    next_link:
+
+        while (start < end && *start == ' ') { start++; }
+
+        if (start == end || *start++ != '<') {
+            continue;
+        }
+
+        while (start < end && *start == ' ') { start++; }
+
+        for (last = start; last < end && *last != '>'; last++) {
+            /* void */
+        }
+
+        if (last == start || last == end) {
+            continue;
+        }
+
+        path.len = last - start;
+        path.data = start;
+
+        start = last + 1;
+
+        while (start < end && *start == ' ') { start++; }
+
+        if (start == end) {
+            continue;
+        }
+
+        if (*start == ',') {
+            start++;
+            goto next_link;
+        }
+
+        if (*start++ != ';') {
+            continue;
+        }
+
+        last = ngx_strlchr(start, end, ',');
+
+        if (last == NULL) {
+            last = end;
+        }
+
+        push = 0;
+
+        for ( ;; ) {
+
+            while (start < last && *start == ' ') { start++; }
+
+            if (last - start >= 6
+                && ngx_strncasecmp(start, (u_char *) "nopush", 6) == 0)
+            {
+                start += 6;
+
+                if (start == last || *start == ' ' || *start == ';') {
+                    push = 0;
+                    break;
+                }
+
+                goto next_param;
+            }
+
+            if (last - start >= 11
+                && ngx_strncasecmp(start, (u_char *) "rel=preload", 11) == 0)
+            {
+                start += 11;
+
+                if (start == last || *start == ' ' || *start == ';') {
+                    push = 1;
+                }
+
+                goto next_param;
+            }
+
+            if (last - start >= 4
+                && ngx_strncasecmp(start, (u_char *) "rel=", 4) == 0)
+            {
+                start += 4;
+
+                while (start < last && *start == ' ') { start++; }
+
+                if (start == last || *start++ != '"') {
+                    goto next_param;
+                }
+
+                for ( ;; ) {
+
+                    while (start < last && *start == ' ') { start++; }
+
+                    if (last - start >= 7
+                        && ngx_strncasecmp(start, (u_char *) "preload", 7) == 0)
+                    {
+                        start += 7;
+
+                        if (start < last && (*start == ' ' || *start == '"')) {
+                            push = 1;
+                            break;
+                        }
+                    }
+
+                    while (start < last && *start != ' ' && *start != '"') {
+                        start++;
+                    }
+
+                    if (start == last) {
+                        break;
+                    }
+
+                    if (*start == '"') {
+                        break;
+                    }
+
+                    start++;
+                }
+            }
+
+        next_param:
+
+            start = ngx_strlchr(start, last, ';');
+
+            if (start == NULL) {
+                break;
+            }
+
+            start++;
+        }
+
+        if (push) {
+            while (path.len && path.data[path.len - 1] == ' ') {
+                path.len--;
+            }
+        }
+
+        if (push && path.len
+            && !(path.len > 1 && path.data[0] == '/' && path.data[1] == '/'))
+        {
+            rc = ngx_http_v3_push_resource(r, &path, out);
+
+            if (rc == NGX_ERROR) {
+                return NGX_ERROR;
+            }
+
+            if (rc == NGX_ABORT) {
+                return NGX_OK;
+            }
+
+            /* NGX_OK, NGX_DECLINED */
+        }
+
+        if (last < end) {
+            start = last + 1;
+            goto next_link;
+        }
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_http_v3_push_resource(ngx_http_request_t *r, ngx_str_t *path,
+    ngx_chain_t ***ll)
+{
+    uint64_t                   push_id;
+    ngx_int_t                  rc;
+    ngx_chain_t               *cl;
+    ngx_connection_t          *c;
+    ngx_http_v3_srv_conf_t    *h3scf;
+    ngx_http_v3_connection_t  *h3c;
+
+    c = r->connection;
+    h3c = c->quic->parent->data;
+    h3scf = ngx_http_get_module_srv_conf(r, ngx_http_v3_module);
+
+    ngx_log_debug5(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 push \"%V\" pushing:%ui/%ui id:%uL/%uL",
+                   path, h3c->npushing, h3scf->max_concurrent_pushes,
+                   h3c->next_push_id, h3c->max_push_id);
+
+    if (!ngx_path_separator(path->data[0])) {
+        ngx_log_error(NGX_LOG_WARN, c->log, 0,
+                      "non-absolute path \"%V\" not pushed", path);
+        return NGX_DECLINED;
+    }
+
+    if (h3c->next_push_id > h3c->max_push_id) {
+        ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 abort pushes due to max_push_id");
+        return NGX_ABORT;
+    }
+
+    if (h3c->npushing >= h3scf->max_concurrent_pushes) {
+        ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 abort pushes due to max_concurrent_pushes");
+        return NGX_ABORT;
+    }
+
+    push_id = h3c->next_push_id++;
+
+    rc = ngx_http_v3_create_push_request(r, path, push_id);
+    if (rc != NGX_OK) {
+        return rc;
+    }
+
+    cl = ngx_http_v3_create_push_promise(r, path, push_id);
+    if (cl == NULL) {
+        return NGX_ERROR;
+    }
+
+    for (**ll = cl; **ll; *ll = &(**ll)->next);
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_http_v3_create_push_request(ngx_http_request_t *pr, ngx_str_t *path,
+    uint64_t push_id)
+{
+    ngx_pool_t                *pool;
+    ngx_connection_t          *c, *pc;
+    ngx_http_request_t        *r;
+    ngx_http_log_ctx_t        *ctx;
+    ngx_http_connection_t     *hc;
+    ngx_http_core_srv_conf_t  *cscf;
+    ngx_http_v3_connection_t  *h3c;
+
+    pc = pr->connection;
+
+    r = NULL;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, pc->log, 0,
+                   "http3 create push request id:%uL", push_id);
+
+    c = ngx_http_v3_create_push_stream(pc, push_id);
+    if (c == NULL) {
+        return NGX_ABORT;
+    }
+
+    hc = ngx_palloc(c->pool, sizeof(ngx_http_connection_t));
+    if (hc == NULL) {
+        goto failed;
+    }
+
+    h3c = c->quic->parent->data;
+    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) {
+        goto failed;
+    }
+
+    ctx->connection = c;
+    ctx->request = NULL;
+    ctx->current_request = NULL;
+
+    c->log->handler = ngx_http_log_error;
+    c->log->data = ctx;
+    c->log->action = "processing pushed request headers";
+
+    c->log_error = NGX_ERROR_INFO;
+
+    r = ngx_http_create_request(c);
+    if (r == NULL) {
+        goto failed;
+    }
+
+    c->data = r;
+
+    ngx_str_set(&r->http_protocol, "HTTP/3.0");
+
+    r->method_name = ngx_http_core_get_method;
+    r->method = NGX_HTTP_GET;
+
+    cscf = ngx_http_get_module_srv_conf(r, ngx_http_core_module);
+
+    r->header_in = ngx_create_temp_buf(r->pool,
+                                       cscf->client_header_buffer_size);
+    if (r->header_in == NULL) {
+        goto failed;
+    }
+
+    if (ngx_list_init(&r->headers_in.headers, r->pool, 4,
+                      sizeof(ngx_table_elt_t))
+        != NGX_OK)
+    {
+        goto failed;
+    }
+
+    r->headers_in.connection_type = NGX_HTTP_CONNECTION_CLOSE;
+
+    r->schema.data = ngx_pstrdup(r->pool, &pr->schema);
+    if (r->schema.data == NULL) {
+        goto failed;
+    }
+
+    r->schema.len = pr->schema.len;
+
+    r->uri_start = ngx_pstrdup(r->pool, path);
+    if (r->uri_start == NULL) {
+        goto failed;
+    }
+
+    r->uri_end = r->uri_start + path->len;
+
+    if (ngx_http_parse_uri(r) != NGX_OK) {
+        goto failed;
+    }
+
+    if (ngx_http_process_request_uri(r) != NGX_OK) {
+        goto failed;
+    }
+
+    if (ngx_http_v3_set_push_header(r, "host", &pr->headers_in.server)
+        != NGX_OK)
+    {
+        goto failed;
+    }
+
+    if (pr->headers_in.accept_encoding) {
+        if (ngx_http_v3_set_push_header(r, "accept-encoding",
+                                        &pr->headers_in.accept_encoding->value)
+            != NGX_OK)
+        {
+            goto failed;
+        }
+    }
+
+    if (pr->headers_in.accept_language) {
+        if (ngx_http_v3_set_push_header(r, "accept-language",
+                                        &pr->headers_in.accept_language->value)
+            != NGX_OK)
+        {
+            goto failed;
+        }
+    }
+
+    if (pr->headers_in.user_agent) {
+        if (ngx_http_v3_set_push_header(r, "user-agent",
+                                        &pr->headers_in.user_agent->value)
+            != NGX_OK)
+        {
+            goto failed;
+        }
+    }
+
+    c->read->handler = ngx_http_v3_push_request_handler;
+    c->read->handler = ngx_http_v3_push_request_handler;
+
+    ngx_post_event(c->read, &ngx_posted_events);
+
+    return NGX_OK;
+
+failed:
+
+    if (r) {
+        ngx_http_free_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
+    }
+
+    c->destroyed = 1;
+
+    pool = c->pool;
+
+    ngx_close_connection(c);
+
+    ngx_destroy_pool(pool);
+
+    return NGX_ERROR;
+}
+
+
+static ngx_int_t
+ngx_http_v3_set_push_header(ngx_http_request_t *r, const char *name,
+    ngx_str_t *value)
+{
+    u_char                     *p;
+    ngx_table_elt_t            *h;
+    ngx_http_header_t          *hh;
+    ngx_http_core_main_conf_t  *cmcf;
+
+    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
+                   "http3 push header \"%s\": \"%V\"", name, value);
+
+    cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module);
+
+    p = ngx_pnalloc(r->pool, value->len + 1);
+    if (p == NULL) {
+        return NGX_ERROR;
+    }
+
+    ngx_memcpy(p, value->data, value->len);
+    p[value->len] = '\0';
+
+    h = ngx_list_push(&r->headers_in.headers);
+    if (h == NULL) {
+        return NGX_ERROR;
+    }
+
+    h->key.data = (u_char *) name;
+    h->key.len = ngx_strlen(name);
+    h->hash = ngx_hash_key(h->key.data, h->key.len);
+    h->lowcase_key = (u_char *) name;
+    h->value.data = p;
+    h->value.len = value->len;
+
+    hh = ngx_hash_find(&cmcf->headers_in_hash, h->hash,
+                       h->lowcase_key, h->key.len);
+
+    if (hh && hh->handler(r, h, hh->offset) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}
+
+
+static void
+ngx_http_v3_push_request_handler(ngx_event_t *ev)
+{
+    ngx_connection_t    *c;
+    ngx_http_request_t  *r;
+
+    c = ev->data;
+    r = c->data;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 push request handler");
+
+    ngx_http_process_request(r);
+}
+
+
+static ngx_chain_t *
+ngx_http_v3_create_push_promise(ngx_http_request_t *r, ngx_str_t *path,
+    uint64_t push_id)
+{
+    size_t        n, len;
+    ngx_buf_t    *b;
+    ngx_chain_t  *hl, *cl;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
+                   "http3 create push promise id:%uL", push_id);
+
+    len = ngx_http_v3_encode_varlen_int(NULL, push_id);
+
+    len += ngx_http_v3_encode_header_block_prefix(NULL, 0, 0, 0);
+
+    len += ngx_http_v3_encode_header_ri(NULL, 0,
+                                        NGX_HTTP_V3_HEADER_METHOD_GET);
+
+    len += ngx_http_v3_encode_header_lri(NULL, 0,
+                                         NGX_HTTP_V3_HEADER_AUTHORITY,
+                                         NULL, r->headers_in.server.len);
+
+    if (path->len == 1 && path->data[0] == '/') {
+        len += ngx_http_v3_encode_header_ri(NULL, 0,
+                                            NGX_HTTP_V3_HEADER_PATH_ROOT);
+
+    } else {
+        len += ngx_http_v3_encode_header_lri(NULL, 0,
+                                             NGX_HTTP_V3_HEADER_PATH_ROOT,
+                                             NULL, path->len);
+    }
+
+    if (r->schema.len == 5 && ngx_strncmp(r->schema.data, "https", 5) == 0) {
+        len += ngx_http_v3_encode_header_ri(NULL, 0,
+                                            NGX_HTTP_V3_HEADER_SCHEME_HTTPS);
+
+    } else if (r->schema.len == 4
+               && ngx_strncmp(r->schema.data, "http", 4) == 0)
+    {
+        len += ngx_http_v3_encode_header_ri(NULL, 0,
+                                            NGX_HTTP_V3_HEADER_SCHEME_HTTP);
+
+    } else {
+        len += ngx_http_v3_encode_header_lri(NULL, 0,
+                                             NGX_HTTP_V3_HEADER_SCHEME_HTTP,
+                                             NULL, r->schema.len);
+    }
+
+    if (r->headers_in.accept_encoding) {
+        len += ngx_http_v3_encode_header_lri(NULL, 0,
+                                     NGX_HTTP_V3_HEADER_ACCEPT_ENCODING, NULL,
+                                     r->headers_in.accept_encoding->value.len);
+    }
+
+    if (r->headers_in.accept_language) {
+        len += ngx_http_v3_encode_header_lri(NULL, 0,
+                                     NGX_HTTP_V3_HEADER_ACCEPT_LANGUAGE, NULL,
+                                     r->headers_in.accept_language->value.len);
+    }
+
+    if (r->headers_in.user_agent) {
+        len += ngx_http_v3_encode_header_lri(NULL, 0,
+                                          NGX_HTTP_V3_HEADER_USER_AGENT, NULL,
+                                          r->headers_in.user_agent->value.len);
+    }
+
+    b = ngx_create_temp_buf(r->pool, len);
+    if (b == NULL) {
+        return NULL;
+    }
+
+    b->last = (u_char *) ngx_http_v3_encode_varlen_int(b->last, push_id);
+
+    b->last = (u_char *) ngx_http_v3_encode_header_block_prefix(b->last,
+                                                                0, 0, 0);
+
+    b->last = (u_char *) ngx_http_v3_encode_header_ri(b->last, 0,
+                                                NGX_HTTP_V3_HEADER_METHOD_GET);
+
+    b->last = (u_char *) ngx_http_v3_encode_header_lri(b->last, 0,
+                                                  NGX_HTTP_V3_HEADER_AUTHORITY,
+                                                  r->headers_in.server.data,
+                                                  r->headers_in.server.len);
+
+    if (path->len == 1 && path->data[0] == '/') {
+        b->last = (u_char *) ngx_http_v3_encode_header_ri(b->last, 0,
+                                                 NGX_HTTP_V3_HEADER_PATH_ROOT);
+
+    } else {
+        b->last = (u_char *) ngx_http_v3_encode_header_lri(b->last, 0,
+                                                  NGX_HTTP_V3_HEADER_PATH_ROOT,
+                                                  path->data, path->len);
+    }
+
+    if (r->schema.len == 5 && ngx_strncmp(r->schema.data, "https", 5) == 0) {
+        b->last = (u_char *) ngx_http_v3_encode_header_ri(b->last, 0,
+                                              NGX_HTTP_V3_HEADER_SCHEME_HTTPS);
+
+    } else if (r->schema.len == 4
+               && ngx_strncmp(r->schema.data, "http", 4) == 0)
+    {
+        b->last = (u_char *) ngx_http_v3_encode_header_ri(b->last, 0,
+                                               NGX_HTTP_V3_HEADER_SCHEME_HTTP);
+
+    } else {
+        b->last = (u_char *) ngx_http_v3_encode_header_lri(b->last, 0,
+                                                NGX_HTTP_V3_HEADER_SCHEME_HTTP,
+                                                r->schema.data, r->schema.len);
+    }
+
+    if (r->headers_in.accept_encoding) {
+        b->last = (u_char *) ngx_http_v3_encode_header_lri(b->last, 0,
+                                     NGX_HTTP_V3_HEADER_ACCEPT_ENCODING,
+                                     r->headers_in.accept_encoding->value.data,
+                                     r->headers_in.accept_encoding->value.len);
+    }
+
+    if (r->headers_in.accept_language) {
+        b->last = (u_char *) ngx_http_v3_encode_header_lri(b->last, 0,
+                                     NGX_HTTP_V3_HEADER_ACCEPT_LANGUAGE,
+                                     r->headers_in.accept_language->value.data,
+                                     r->headers_in.accept_language->value.len);
+    }
+
+    if (r->headers_in.user_agent) {
+        b->last = (u_char *) ngx_http_v3_encode_header_lri(b->last, 0,
+                                          NGX_HTTP_V3_HEADER_USER_AGENT,
+                                          r->headers_in.user_agent->value.data,
+                                          r->headers_in.user_agent->value.len);
+    }
+
+    cl = ngx_alloc_chain_link(r->pool);
+    if (cl == NULL) {
+        return NULL;
+    }
+
+    cl->buf = b;
+    cl->next = NULL;
+
+    n = b->last - b->pos;
+
+    len = ngx_http_v3_encode_varlen_int(NULL, NGX_HTTP_V3_FRAME_PUSH_PROMISE)
+          + ngx_http_v3_encode_varlen_int(NULL, n);
+
+    b = ngx_create_temp_buf(r->pool, len);
+    if (b == NULL) {
+        return NULL;
+    }
+
+    b->last = (u_char *) ngx_http_v3_encode_varlen_int(b->last,
+                                               NGX_HTTP_V3_FRAME_PUSH_PROMISE);
+    b->last = (u_char *) ngx_http_v3_encode_varlen_int(b->last, n);
+
+    hl = ngx_alloc_chain_link(r->pool);
+    if (hl == NULL) {
+        return NULL;
+    }
+
+    hl->buf = b;
+    hl->next = cl;
+
+    return hl;
+}
new file mode 100644
--- /dev/null
+++ b/src/http/v3/ngx_http_v3_streams.c
@@ -0,0 +1,819 @@
+
+/*
+ * 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;
+
+
+typedef struct {
+    ngx_queue_t                     queue;
+    uint64_t                        id;
+    ngx_connection_t               *connection;
+    ngx_uint_t                     *npushing;
+} ngx_http_v3_push_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 void ngx_http_v3_push_cleanup(void *data);
+static ngx_connection_t *ngx_http_v3_get_uni_stream(ngx_connection_t *c,
+    ngx_uint_t type);
+static ngx_int_t ngx_http_v3_send_settings(ngx_connection_t *c);
+
+
+ngx_int_t
+ngx_http_v3_init_connection(ngx_connection_t *c)
+{
+    ngx_http_connection_t     *hc;
+    ngx_http_v3_uni_stream_t  *us;
+    ngx_http_v3_connection_t  *h3c;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 init connection");
+
+    hc = c->data;
+
+    if (c->quic == NULL) {
+        h3c = ngx_pcalloc(c->pool, sizeof(ngx_http_v3_connection_t));
+        if (h3c == NULL) {
+            return NGX_ERROR;
+        }
+
+        h3c->hc = *hc;
+
+        ngx_queue_init(&h3c->blocked);
+        ngx_queue_init(&h3c->pushing);
+
+        c->data = h3c;
+        return NGX_OK;
+    }
+
+    if (ngx_http_v3_send_settings(c) == NGX_ERROR) {
+        return NGX_ERROR;
+    }
+
+    if ((c->quic->id & NGX_QUIC_STREAM_UNIDIRECTIONAL) == 0) {
+        return NGX_OK;
+    }
+
+    us = ngx_pcalloc(c->pool, sizeof(ngx_http_v3_uni_stream_t));
+    if (us == NULL) {
+        ngx_http_v3_finalize_connection(c, NGX_HTTP_V3_ERR_INTERNAL_ERROR,
+                                        NULL);
+        return NGX_ERROR;
+    }
+
+    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);
+
+    return NGX_DONE;
+}
+
+
+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->quic->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, rc;
+    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->quic->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_AGAIN) {
+            break;
+        }
+
+        if (n == 0) {
+            rc = NGX_HTTP_V3_ERR_GENERAL_PROTOCOL_ERROR;
+            goto failed;
+        }
+
+        if (n != 1) {
+            rc = NGX_HTTP_V3_ERR_INTERNAL_ERROR;
+            goto failed;
+        }
+
+        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");
+                rc = NGX_HTTP_V3_ERR_STREAM_CREATION_ERROR;
+                goto failed;
+            }
+
+            us->index = index;
+            h3c->known_streams[index] = c;
+        }
+
+        if (n) {
+            us->data = ngx_pcalloc(c->pool, n);
+            if (us->data == NULL) {
+                rc = NGX_HTTP_V3_ERR_INTERNAL_ERROR;
+                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) {
+        rc = NGX_HTTP_V3_ERR_INTERNAL_ERROR;
+        goto failed;
+    }
+
+    return;
+
+failed:
+
+    ngx_http_v3_finalize_connection(c, rc, "could not read stream type");
+    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) {
+            rc = NGX_HTTP_V3_ERR_INTERNAL_ERROR;
+            goto failed;
+        }
+
+        if (n == 0) {
+            if (us->index >= 0) {
+                rc = NGX_HTTP_V3_ERR_CLOSED_CRITICAL_STREAM;
+                goto failed;
+            }
+
+            ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 read eof");
+            ngx_http_v3_close_uni_stream(c);
+            return;
+        }
+
+        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_DONE) {
+                ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                               "http3 read done");
+                ngx_http_v3_close_uni_stream(c);
+                return;
+            }
+
+            if (rc > 0) {
+                goto failed;
+            }
+
+            if (rc != NGX_AGAIN) {
+                rc = NGX_HTTP_V3_ERR_GENERAL_PROTOCOL_ERROR;
+                goto failed;
+            }
+        }
+    }
+
+    if (ngx_handle_read_event(rev, 0) != NGX_OK) {
+        rc = NGX_HTTP_V3_ERR_INTERNAL_ERROR;
+        goto failed;
+    }
+
+    return;
+
+failed:
+
+    ngx_http_v3_finalize_connection(c, rc, "stream error");
+    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_finalize_connection(c, NGX_HTTP_V3_ERR_INTERNAL_ERROR,
+                                        NULL);
+        ngx_http_v3_close_uni_stream(c);
+    }
+}
+
+
+/* XXX async & buffered stream writes */
+
+ngx_connection_t *
+ngx_http_v3_create_push_stream(ngx_connection_t *c, uint64_t push_id)
+{
+    u_char                    *p, buf[NGX_HTTP_V3_VARLEN_INT_LEN * 2];
+    size_t                     n;
+    ngx_connection_t          *sc;
+    ngx_pool_cleanup_t        *cln;
+    ngx_http_v3_push_t        *push;
+    ngx_http_v3_connection_t  *h3c;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 create push stream id:%uL", push_id);
+
+    sc = ngx_quic_open_stream(c, 0);
+    if (sc == NULL) {
+        return NULL;
+    }
+
+    p = buf;
+    p = (u_char *) ngx_http_v3_encode_varlen_int(p, NGX_HTTP_V3_STREAM_PUSH);
+    p = (u_char *) ngx_http_v3_encode_varlen_int(p, push_id);
+    n = p - buf;
+
+    if (sc->send(sc, buf, n) != (ssize_t) n) {
+        goto failed;
+    }
+
+    cln = ngx_pool_cleanup_add(sc->pool, sizeof(ngx_http_v3_push_t));
+    if (cln == NULL) {
+        goto failed;
+    }
+
+    h3c = c->quic->parent->data;
+    h3c->npushing++;
+
+    cln->handler = ngx_http_v3_push_cleanup;
+
+    push = cln->data;
+    push->id = push_id;
+    push->connection = sc;
+    push->npushing = &h3c->npushing;
+
+    ngx_queue_insert_tail(&h3c->pushing, &push->queue);
+
+    return sc;
+
+failed:
+
+    ngx_http_v3_close_uni_stream(sc);
+
+    return NULL;
+}
+
+
+static void
+ngx_http_v3_push_cleanup(void *data)
+{
+    ngx_http_v3_push_t  *push = data;
+
+    ngx_queue_remove(&push->queue);
+    (*push->npushing)--;
+}
+
+
+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->quic->parent->data;
+
+    if (index >= 0) {
+        if (h3c->known_streams[index]) {
+            return h3c->known_streams[index];
+        }
+    }
+
+    sc = ngx_quic_open_stream(c, 0);
+    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;
+
+    if (index >= 0) {
+        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;
+}
+
+
+static ngx_int_t
+ngx_http_v3_send_settings(ngx_connection_t *c)
+{
+    u_char                    *p, buf[NGX_HTTP_V3_VARLEN_INT_LEN * 6];
+    size_t                     n;
+    ngx_connection_t          *cc;
+    ngx_http_v3_srv_conf_t    *h3scf;
+    ngx_http_v3_connection_t  *h3c;
+
+    h3c = c->quic->parent->data;
+
+    if (h3c->settings_sent) {
+        return NGX_OK;
+    }
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 send settings");
+
+    cc = ngx_http_v3_get_uni_stream(c, NGX_HTTP_V3_STREAM_CONTROL);
+    if (cc == NULL) {
+        return NGX_DECLINED;
+    }
+
+    h3scf = ngx_http_get_module_srv_conf(h3c->hc.conf_ctx, ngx_http_v3_module);
+
+    n = ngx_http_v3_encode_varlen_int(NULL,
+                                      NGX_HTTP_V3_PARAM_MAX_TABLE_CAPACITY);
+    n += ngx_http_v3_encode_varlen_int(NULL, h3scf->max_table_capacity);
+    n += ngx_http_v3_encode_varlen_int(NULL, NGX_HTTP_V3_PARAM_BLOCKED_STREAMS);
+    n += ngx_http_v3_encode_varlen_int(NULL, h3scf->max_blocked_streams);
+
+    p = (u_char *) ngx_http_v3_encode_varlen_int(buf,
+                                                 NGX_HTTP_V3_FRAME_SETTINGS);
+    p = (u_char *) ngx_http_v3_encode_varlen_int(p, n);
+    p = (u_char *) ngx_http_v3_encode_varlen_int(p,
+                                         NGX_HTTP_V3_PARAM_MAX_TABLE_CAPACITY);
+    p = (u_char *) ngx_http_v3_encode_varlen_int(p, h3scf->max_table_capacity);
+    p = (u_char *) ngx_http_v3_encode_varlen_int(p,
+                                            NGX_HTTP_V3_PARAM_BLOCKED_STREAMS);
+    p = (u_char *) ngx_http_v3_encode_varlen_int(p, h3scf->max_blocked_streams);
+    n = p - buf;
+
+    if (cc->send(cc, buf, n) != (ssize_t) n) {
+        goto failed;
+    }
+
+    h3c->settings_sent = 1;
+
+    return NGX_OK;
+
+failed:
+
+    ngx_http_v3_close_uni_stream(cc);
+
+    ngx_http_v3_finalize_connection(c, NGX_HTTP_V3_ERR_INTERNAL_ERROR,
+                                    "could not send settings");
+
+    return NGX_ERROR;
+}
+
+
+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;
+}
+
+
+ngx_int_t
+ngx_http_v3_set_max_push_id(ngx_connection_t *c, uint64_t max_push_id)
+{
+    ngx_http_v3_connection_t  *h3c;
+
+    h3c = c->quic->parent->data;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 MAX_PUSH_ID:%uL", max_push_id);
+
+    if (max_push_id < h3c->max_push_id) {
+        return NGX_HTTP_V3_ERR_ID_ERROR;
+    }
+
+    h3c->max_push_id = max_push_id;
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_http_v3_cancel_push(ngx_connection_t *c, uint64_t push_id)
+{
+    ngx_queue_t               *q;
+    ngx_http_request_t        *r;
+    ngx_http_v3_push_t        *push;
+    ngx_http_v3_connection_t  *h3c;
+
+    h3c = c->quic->parent->data;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 CANCEL_PUSH:%uL", push_id);
+
+    if (push_id >= h3c->next_push_id) {
+        return NGX_HTTP_V3_ERR_ID_ERROR;
+    }
+
+    for (q = ngx_queue_head(&h3c->pushing);
+         q != ngx_queue_sentinel(&h3c->pushing);
+         q = ngx_queue_next(&h3c->pushing))
+    {
+        push = (ngx_http_v3_push_t *) q;
+
+        if (push->id != push_id) {
+            continue;
+        }
+
+        r = push->connection->data;
+
+        ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
+                       "http3 cancel push");
+
+        ngx_http_finalize_request(r, NGX_HTTP_CLOSE);
+
+        break;
+    }
+
+    return NGX_OK;
+}
new file mode 100644
--- /dev/null
+++ b/src/http/v3/ngx_http_v3_tables.c
@@ -0,0 +1,686 @@
+
+/*
+ * Copyright (C) Roman Arutyunyan
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_http.h>
+
+
+#define ngx_http_v3_table_entry_size(n, v) ((n)->len + (v)->len + 32)
+
+
+static ngx_int_t ngx_http_v3_evict(ngx_connection_t *c, size_t need);
+static void ngx_http_v3_cleanup_table(void *data);
+static void ngx_http_v3_unblock(void *data);
+static ngx_int_t ngx_http_v3_new_header(ngx_connection_t *c);
+
+
+typedef struct {
+    ngx_queue_t        queue;
+    ngx_connection_t  *connection;
+    ngx_uint_t        *nblocked;
+} ngx_http_v3_block_t;
+
+
+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_str_t  name;
+
+    if (dynamic) {
+        ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 ref insert dynamic[%ui] \"%V\"", index, value);
+
+        if (ngx_http_v3_lookup(c, index, &name, NULL) != NGX_OK) {
+            return NGX_HTTP_V3_ERR_ENCODER_STREAM_ERROR;
+        }
+
+    } else {
+        ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 ref insert static[%ui] \"%V\"", index, value);
+
+        if (ngx_http_v3_lookup_static(c, index, &name, NULL) != NGX_OK) {
+            return NGX_HTTP_V3_ERR_ENCODER_STREAM_ERROR;
+        }
+    }
+
+    return ngx_http_v3_insert(c, &name, value);
+}
+
+
+ngx_int_t
+ngx_http_v3_insert(ngx_connection_t *c, ngx_str_t *name, ngx_str_t *value)
+{
+    u_char                       *p;
+    size_t                        size;
+    ngx_http_v3_header_t         *h;
+    ngx_http_v3_connection_t     *h3c;
+    ngx_http_v3_dynamic_table_t  *dt;
+
+    size = ngx_http_v3_table_entry_size(name, value);
+
+    if (ngx_http_v3_evict(c, size) != NGX_OK) {
+        return NGX_HTTP_V3_ERR_ENCODER_STREAM_ERROR;
+    }
+
+    h3c = c->quic->parent->data;
+    dt = &h3c->table;
+
+    ngx_log_debug4(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 insert [%ui] \"%V\":\"%V\", size:%uz",
+                   dt->base + dt->nelts, name, value, size);
+
+    p = ngx_alloc(sizeof(ngx_http_v3_header_t) + name->len + value->len,
+                  c->log);
+    if (p == NULL) {
+        return NGX_ERROR;
+    }
+
+    h = (ngx_http_v3_header_t *) p;
+
+    h->name.data = p + sizeof(ngx_http_v3_header_t);
+    h->name.len = name->len;
+    h->value.data = ngx_cpymem(h->name.data, name->data, name->len);
+    h->value.len = value->len;
+    ngx_memcpy(h->value.data, value->data, value->len);
+
+    dt->elts[dt->nelts++] = h;
+    dt->size += size;
+
+    /* TODO increment can be sent less often */
+
+    if (ngx_http_v3_client_inc_insert_count(c, 1) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    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_uint_t                     max, prev_max;
+    ngx_connection_t              *pc;
+    ngx_pool_cleanup_t            *cln;
+    ngx_http_v3_header_t         **elts;
+    ngx_http_v3_srv_conf_t        *h3scf;
+    ngx_http_v3_connection_t      *h3c;
+    ngx_http_v3_dynamic_table_t   *dt;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 set capacity %ui", capacity);
+
+    pc = c->quic->parent;
+    h3c = pc->data;
+    h3scf = ngx_http_get_module_srv_conf(h3c->hc.conf_ctx, ngx_http_v3_module);
+
+    if (capacity > h3scf->max_table_capacity) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "client exceeded http3_max_table_capacity limit");
+        return NGX_HTTP_V3_ERR_ENCODER_STREAM_ERROR;
+    }
+
+    dt = &h3c->table;
+
+    if (dt->size > capacity) {
+        if (ngx_http_v3_evict(c, dt->size - capacity) != NGX_OK) {
+            return NGX_HTTP_V3_ERR_ENCODER_STREAM_ERROR;
+        }
+    }
+
+    max = capacity / 32;
+    prev_max = dt->capacity / 32;
+
+    if (max > prev_max) {
+        elts = ngx_alloc(max * sizeof(void *), c->log);
+        if (elts == NULL) {
+            return NGX_ERROR;
+        }
+
+        if (dt->elts == NULL) {
+            cln = ngx_pool_cleanup_add(pc->pool, 0);
+            if (cln == NULL) {
+                return NGX_ERROR;
+            }
+
+            cln->handler = ngx_http_v3_cleanup_table;
+            cln->data = dt;
+
+        } else {
+            ngx_memcpy(elts, dt->elts, dt->nelts * sizeof(void *));
+            ngx_free(dt->elts);
+        }
+
+        dt->elts = elts;
+    }
+
+    dt->capacity = capacity;
+
+    return NGX_OK;
+}
+
+
+static void
+ngx_http_v3_cleanup_table(void *data)
+{
+    ngx_http_v3_dynamic_table_t  *dt = data;
+
+    ngx_uint_t  n;
+
+    for (n = 0; n < dt->nelts; n++) {
+        ngx_free(dt->elts[n]);
+    }
+
+    ngx_free(dt->elts);
+}
+
+
+static ngx_int_t
+ngx_http_v3_evict(ngx_connection_t *c, size_t need)
+{
+    size_t                        size, target;
+    ngx_uint_t                    n;
+    ngx_http_v3_header_t         *h;
+    ngx_http_v3_connection_t     *h3c;
+    ngx_http_v3_dynamic_table_t  *dt;
+
+    h3c = c->quic->parent->data;
+    dt = &h3c->table;
+
+    if (need > dt->capacity) {
+        ngx_log_error(NGX_LOG_ERR, c->log, 0,
+                      "not enough dynamic table capacity");
+        return NGX_ERROR;
+    }
+
+    target = dt->capacity - need;
+    n = 0;
+
+    while (dt->size > target) {
+        h = dt->elts[n++];
+        size = ngx_http_v3_table_entry_size(&h->name, &h->value);
+
+        ngx_log_debug4(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 evict [%ui] \"%V\":\"%V\" size:%uz",
+                       dt->base, &h->name, &h->value, size);
+
+        ngx_free(h);
+        dt->size -= size;
+    }
+
+    if (n) {
+        dt->nelts -= n;
+        dt->base += n;
+        ngx_memmove(dt->elts, &dt->elts[n], dt->nelts * sizeof(void *));
+    }
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_http_v3_duplicate(ngx_connection_t *c, ngx_uint_t index)
+{
+    ngx_str_t                     name, value;
+    ngx_http_v3_connection_t     *h3c;
+    ngx_http_v3_dynamic_table_t  *dt;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 duplicate %ui", index);
+
+    h3c = c->quic->parent->data;
+    dt = &h3c->table;
+
+    if (dt->base + dt->nelts <= index) {
+        return NGX_HTTP_V3_ERR_ENCODER_STREAM_ERROR;
+    }
+
+    index = dt->base + dt->nelts - 1 - index;
+
+    if (ngx_http_v3_lookup(c, index, &name, &value) != NGX_OK) {
+        return NGX_HTTP_V3_ERR_ENCODER_STREAM_ERROR;
+    }
+
+    return ngx_http_v3_insert(c, &name, &value);
+}
+
+
+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;
+}
+
+
+ngx_int_t
+ngx_http_v3_lookup_static(ngx_connection_t *c, ngx_uint_t index,
+    ngx_str_t *name, ngx_str_t *value)
+{
+    ngx_uint_t             nelts;
+    ngx_http_v3_header_t  *h;
+
+    nelts = sizeof(ngx_http_v3_static_table)
+            / sizeof(ngx_http_v3_static_table[0]);
+
+    if (index >= nelts) {
+        ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 static[%ui] lookup out of bounds: %ui",
+                       index, nelts);
+        return NGX_ERROR;
+    }
+
+    h = &ngx_http_v3_static_table[index];
+
+    ngx_log_debug3(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 static[%ui] lookup \"%V\":\"%V\"",
+                   index, &h->name, &h->value);
+
+    if (name) {
+        *name = h->name;
+    }
+
+    if (value) {
+        *value = h->value;
+    }
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_http_v3_lookup(ngx_connection_t *c, ngx_uint_t index, ngx_str_t *name,
+    ngx_str_t *value)
+{
+    ngx_http_v3_header_t         *h;
+    ngx_http_v3_connection_t     *h3c;
+    ngx_http_v3_dynamic_table_t  *dt;
+
+    h3c = c->quic->parent->data;
+    dt = &h3c->table;
+
+    if (index < dt->base || index - dt->base >= dt->nelts) {
+        ngx_log_debug3(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 dynamic[%ui] lookup out of bounds: [%ui,%ui]",
+                       index, dt->base, dt->base + dt->nelts);
+        return NGX_ERROR;
+    }
+
+    h = dt->elts[index - dt->base];
+
+    ngx_log_debug3(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 dynamic[%ui] lookup \"%V\":\"%V\"",
+                   index, &h->name, &h->value);
+
+    if (name) {
+        *name = h->name;
+    }
+
+    if (value) {
+        *value = h->value;
+    }
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_http_v3_decode_insert_count(ngx_connection_t *c, ngx_uint_t *insert_count)
+{
+    ngx_uint_t                    max_entries, full_range, max_value,
+                                  max_wrapped, req_insert_count;
+    ngx_http_v3_srv_conf_t       *h3scf;
+    ngx_http_v3_connection_t     *h3c;
+    ngx_http_v3_dynamic_table_t  *dt;
+
+    /* QPACK 4.5.1.1. Required Insert Count */
+
+    if (*insert_count == 0) {
+        return NGX_OK;
+    }
+
+    h3c = c->quic->parent->data;
+    dt = &h3c->table;
+
+    h3scf = ngx_http_get_module_srv_conf(h3c->hc.conf_ctx, ngx_http_v3_module);
+
+    max_entries = h3scf->max_table_capacity / 32;
+    full_range = 2 * max_entries;
+
+    if (*insert_count > full_range) {
+        return NGX_HTTP_V3_ERR_DECOMPRESSION_FAILED;
+    }
+
+    max_value = dt->base + dt->nelts + max_entries;
+    max_wrapped = (max_value / full_range) * full_range;
+    req_insert_count = max_wrapped + *insert_count - 1;
+
+    if (req_insert_count > max_value) {
+        if (req_insert_count <= full_range) {
+            return NGX_HTTP_V3_ERR_DECOMPRESSION_FAILED;
+        }
+
+        req_insert_count -= full_range;
+    }
+
+    if (req_insert_count == 0) {
+        return NGX_HTTP_V3_ERR_DECOMPRESSION_FAILED;
+    }
+
+    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 decode insert_count %ui -> %ui",
+                   *insert_count, req_insert_count);
+
+    *insert_count = req_insert_count;
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_http_v3_check_insert_count(ngx_connection_t *c, ngx_uint_t insert_count)
+{
+    size_t                        n;
+    ngx_connection_t             *pc;
+    ngx_pool_cleanup_t           *cln;
+    ngx_http_v3_block_t          *block;
+    ngx_http_v3_srv_conf_t       *h3scf;
+    ngx_http_v3_connection_t     *h3c;
+    ngx_http_v3_dynamic_table_t  *dt;
+
+    pc = c->quic->parent;
+    h3c = pc->data;
+    dt = &h3c->table;
+
+    n = dt->base + dt->nelts;
+
+    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 check insert count req:%ui, have:%ui",
+                   insert_count, n);
+
+    if (n >= insert_count) {
+        return NGX_OK;
+    }
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 block stream");
+
+    block = NULL;
+
+    for (cln = c->pool->cleanup; cln; cln = cln->next) {
+        if (cln->handler == ngx_http_v3_unblock) {
+            block = cln->data;
+            break;
+        }
+    }
+
+    if (block == NULL) {
+        cln = ngx_pool_cleanup_add(c->pool, sizeof(ngx_http_v3_block_t));
+        if (cln == NULL) {
+            return NGX_ERROR;
+        }
+
+        cln->handler = ngx_http_v3_unblock;
+
+        block = cln->data;
+        block->queue.prev = NULL;
+        block->connection = c;
+        block->nblocked = &h3c->nblocked;
+    }
+
+    if (block->queue.prev == NULL) {
+        h3scf = ngx_http_get_module_srv_conf(h3c->hc.conf_ctx,
+                                             ngx_http_v3_module);
+
+        if (h3c->nblocked == h3scf->max_blocked_streams) {
+            ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                          "client exceeded http3_max_blocked_streams limit");
+            return NGX_HTTP_V3_ERR_DECOMPRESSION_FAILED;
+        }
+
+        h3c->nblocked++;
+        ngx_queue_insert_tail(&h3c->blocked, &block->queue);
+    }
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 blocked:%ui", h3c->nblocked);
+
+    return NGX_BUSY;
+}
+
+
+static void
+ngx_http_v3_unblock(void *data)
+{
+    ngx_http_v3_block_t  *block = data;
+
+    if (block->queue.prev) {
+        ngx_queue_remove(&block->queue);
+        block->queue.prev = NULL;
+        (*block->nblocked)--;
+    }
+}
+
+
+static ngx_int_t
+ngx_http_v3_new_header(ngx_connection_t *c)
+{
+    ngx_queue_t               *q;
+    ngx_connection_t          *bc;
+    ngx_http_v3_block_t       *block;
+    ngx_http_v3_connection_t  *h3c;
+
+    h3c = c->quic->parent->data;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 new dynamic header, blocked:%ui", h3c->nblocked);
+
+    while (!ngx_queue_empty(&h3c->blocked)) {
+        q = ngx_queue_head(&h3c->blocked);
+        block = (ngx_http_v3_block_t *) q;
+        bc = block->connection;
+
+        ngx_log_debug0(NGX_LOG_DEBUG_HTTP, bc->log, 0, "http3 unblock stream");
+
+        ngx_http_v3_unblock(block);
+        ngx_post_event(bc->read, &ngx_posted_events);
+    }
+
+    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;
+}
--- a/src/stream/ngx_stream.c
+++ b/src/stream/ngx_stream.c
@@ -572,6 +572,9 @@ ngx_stream_add_addrs(ngx_conf_t *cf, ngx
 #if (NGX_STREAM_SSL)
         addrs[i].conf.ssl = addr[i].opt.ssl;
 #endif
+#if (NGX_STREAM_QUIC)
+        addrs[i].conf.quic = addr[i].opt.quic;
+#endif
         addrs[i].conf.proxy_protocol = addr[i].opt.proxy_protocol;
         addrs[i].conf.addr_text = addr[i].opt.addr_text;
     }
@@ -607,6 +610,9 @@ ngx_stream_add_addrs6(ngx_conf_t *cf, ng
 #if (NGX_STREAM_SSL)
         addrs6[i].conf.ssl = addr[i].opt.ssl;
 #endif
+#if (NGX_STREAM_QUIC)
+        addrs6[i].conf.quic = addr[i].opt.quic;
+#endif
         addrs6[i].conf.proxy_protocol = addr[i].opt.proxy_protocol;
         addrs6[i].conf.addr_text = addr[i].opt.addr_text;
     }
--- a/src/stream/ngx_stream.h
+++ b/src/stream/ngx_stream.h
@@ -16,6 +16,10 @@
 #include <ngx_stream_ssl_module.h>
 #endif
 
+#if (NGX_STREAM_QUIC)
+#include <ngx_stream_quic_module.h>
+#endif
+
 
 typedef struct ngx_stream_session_s  ngx_stream_session_t;
 
@@ -51,6 +55,7 @@ typedef struct {
     unsigned                       bind:1;
     unsigned                       wildcard:1;
     unsigned                       ssl:1;
+    unsigned                       quic:1;
 #if (NGX_HAVE_INET6)
     unsigned                       ipv6only:1;
 #endif
@@ -73,6 +78,7 @@ typedef struct {
     ngx_stream_conf_ctx_t         *ctx;
     ngx_str_t                      addr_text;
     unsigned                       ssl:1;
+    unsigned                       quic:1;
     unsigned                       proxy_protocol:1;
 } ngx_stream_addr_conf_t;
 
--- a/src/stream/ngx_stream_core_module.c
+++ b/src/stream/ngx_stream_core_module.c
@@ -325,6 +325,9 @@ ngx_stream_core_content_phase(ngx_stream
     cscf = ngx_stream_get_module_srv_conf(s, ngx_stream_core_module);
 
     if (c->type == SOCK_STREAM
+#if (NGX_STREAM_QUIC)
+        && c->quic == NULL
+#endif
         && cscf->tcp_nodelay
         && ngx_tcp_nodelay(c) != NGX_OK)
     {
@@ -741,6 +744,29 @@ ngx_stream_core_listen(ngx_conf_t *cf, n
 #endif
         }
 
+        if (ngx_strcmp(value[i].data, "quic") == 0) {
+#if (NGX_STREAM_QUIC)
+            ngx_stream_ssl_conf_t  *sslcf;
+
+            sslcf = ngx_stream_conf_get_module_srv_conf(cf,
+                                                        ngx_stream_ssl_module);
+
+            sslcf->listen = 1;
+            sslcf->file = cf->conf_file->file.name.data;
+            sslcf->line = cf->conf_file->line;
+
+            ls->quic = 1;
+            ls->type = SOCK_DGRAM;
+
+            continue;
+#else
+            ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
+                               "the \"quic\" parameter requires "
+                               "ngx_stream_quic_module");
+            return NGX_CONF_ERROR;
+#endif
+        }
+
         if (ngx_strncmp(value[i].data, "so_keepalive=", 13) == 0) {
 
             if (ngx_strcmp(&value[i].data[13], "on") == 0) {
@@ -852,6 +878,12 @@ ngx_stream_core_listen(ngx_conf_t *cf, n
         }
 #endif
 
+#if (NGX_STREAM_SSL && NGX_STREAM_QUIC)
+        if (ls->ssl && ls->quic) {
+            return "\"ssl\" parameter is incompatible with \"quic\"";
+        }
+#endif
+
         if (ls->so_keepalive) {
             return "\"so_keepalive\" parameter is incompatible with \"udp\"";
         }
--- a/src/stream/ngx_stream_handler.c
+++ b/src/stream/ngx_stream_handler.c
@@ -115,6 +115,23 @@ ngx_stream_init_connection(ngx_connectio
         }
     }
 
+#if (NGX_STREAM_QUIC)
+
+    if (addr_conf->quic) {
+        ngx_quic_conf_t  *qcf;
+
+        if (c->quic == NULL) {
+            c->log->connection = c->number;
+
+            qcf = ngx_stream_get_module_srv_conf(addr_conf->ctx,
+                                                 ngx_stream_quic_module);
+            ngx_quic_run(c, qcf);
+            return;
+        }
+    }
+
+#endif
+
     s = ngx_pcalloc(c->pool, sizeof(ngx_stream_session_t));
     if (s == NULL) {
         ngx_stream_close_connection(c);
new file mode 100644
--- /dev/null
+++ b/src/stream/ngx_stream_quic_module.c
@@ -0,0 +1,370 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ * Copyright (C) Roman Arutyunyan
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_stream.h>
+
+
+static ngx_int_t ngx_stream_variable_quic(ngx_stream_session_t *s,
+    ngx_stream_variable_value_t *v, uintptr_t data);
+static ngx_int_t ngx_stream_quic_add_variables(ngx_conf_t *cf);
+static void *ngx_stream_quic_create_srv_conf(ngx_conf_t *cf);
+static char *ngx_stream_quic_merge_srv_conf(ngx_conf_t *cf, void *parent,
+    void *child);
+static char *ngx_stream_quic_max_ack_delay(ngx_conf_t *cf, void *post,
+    void *data);
+static char *ngx_stream_quic_max_udp_payload_size(ngx_conf_t *cf, void *post,
+    void *data);
+
+
+static ngx_conf_post_t  ngx_stream_quic_max_ack_delay_post =
+    { ngx_stream_quic_max_ack_delay };
+static ngx_conf_post_t  ngx_stream_quic_max_udp_payload_size_post =
+    { ngx_stream_quic_max_udp_payload_size };
+static ngx_conf_num_bounds_t  ngx_stream_quic_ack_delay_exponent_bounds =
+    { ngx_conf_check_num_bounds, 0, 20 };
+static ngx_conf_num_bounds_t
+                            ngx_stream_quic_active_connection_id_limit_bounds =
+    { ngx_conf_check_num_bounds, 2, -1 };
+
+
+static ngx_command_t  ngx_stream_quic_commands[] = {
+
+    { ngx_string("quic_max_idle_timeout"),
+      NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1,
+      ngx_conf_set_msec_slot,
+      NGX_STREAM_SRV_CONF_OFFSET,
+      offsetof(ngx_quic_conf_t, tp.max_idle_timeout),
+      NULL },
+
+    { ngx_string("quic_max_ack_delay"),
+      NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1,
+      ngx_conf_set_msec_slot,
+      NGX_STREAM_SRV_CONF_OFFSET,
+      offsetof(ngx_quic_conf_t, tp.max_ack_delay),
+      &ngx_stream_quic_max_ack_delay_post },
+
+    { ngx_string("quic_max_udp_payload_size"),
+      NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1,
+      ngx_conf_set_size_slot,
+      NGX_STREAM_SRV_CONF_OFFSET,
+      offsetof(ngx_quic_conf_t, tp.max_udp_payload_size),
+      &ngx_stream_quic_max_udp_payload_size_post },
+
+    { ngx_string("quic_initial_max_data"),
+      NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1,
+      ngx_conf_set_size_slot,
+      NGX_STREAM_SRV_CONF_OFFSET,
+      offsetof(ngx_quic_conf_t, tp.initial_max_data),
+      NULL },
+
+    { ngx_string("quic_initial_max_stream_data_bidi_local"),
+      NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1,
+      ngx_conf_set_size_slot,
+      NGX_STREAM_SRV_CONF_OFFSET,
+      offsetof(ngx_quic_conf_t, tp.initial_max_stream_data_bidi_local),
+      NULL },
+
+    { ngx_string("quic_initial_max_stream_data_bidi_remote"),
+      NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1,
+      ngx_conf_set_size_slot,
+      NGX_STREAM_SRV_CONF_OFFSET,
+      offsetof(ngx_quic_conf_t, tp.initial_max_stream_data_bidi_remote),
+      NULL },
+
+    { ngx_string("quic_initial_max_stream_data_uni"),
+      NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1,
+      ngx_conf_set_size_slot,
+      NGX_STREAM_SRV_CONF_OFFSET,
+      offsetof(ngx_quic_conf_t, tp.initial_max_stream_data_uni),
+      NULL },
+
+    { ngx_string("quic_initial_max_streams_bidi"),
+      NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1,
+      ngx_conf_set_num_slot,
+      NGX_STREAM_SRV_CONF_OFFSET,
+      offsetof(ngx_quic_conf_t, tp.initial_max_streams_bidi),
+      NULL },
+
+    { ngx_string("quic_initial_max_streams_uni"),
+      NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1,
+      ngx_conf_set_num_slot,
+      NGX_STREAM_SRV_CONF_OFFSET,
+      offsetof(ngx_quic_conf_t, tp.initial_max_streams_uni),
+      NULL },
+
+    { ngx_string("quic_ack_delay_exponent"),
+      NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1,
+      ngx_conf_set_num_slot,
+      NGX_STREAM_SRV_CONF_OFFSET,
+      offsetof(ngx_quic_conf_t, tp.ack_delay_exponent),
+      &ngx_stream_quic_ack_delay_exponent_bounds },
+
+    { ngx_string("quic_disable_active_migration"),
+      NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1,
+      ngx_conf_set_flag_slot,
+      NGX_STREAM_SRV_CONF_OFFSET,
+      offsetof(ngx_quic_conf_t, tp.disable_active_migration),
+      NULL },
+
+    { ngx_string("quic_active_connection_id_limit"),
+      NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1,
+      ngx_conf_set_num_slot,
+      NGX_STREAM_SRV_CONF_OFFSET,
+      offsetof(ngx_quic_conf_t, tp.active_connection_id_limit),
+      &ngx_stream_quic_active_connection_id_limit_bounds },
+
+    { ngx_string("quic_retry"),
+      NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_FLAG,
+      ngx_conf_set_flag_slot,
+      NGX_STREAM_SRV_CONF_OFFSET,
+      offsetof(ngx_quic_conf_t, retry),
+      NULL },
+
+    { ngx_string("quic_stateless_reset_token_key"),
+      NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_FLAG,
+      ngx_conf_set_str_slot,
+      NGX_STREAM_SRV_CONF_OFFSET,
+      offsetof(ngx_quic_conf_t, sr_token_key),
+      NULL },
+
+      ngx_null_command
+};
+
+
+static ngx_stream_module_t  ngx_stream_quic_module_ctx = {
+    ngx_stream_quic_add_variables,         /* preconfiguration */
+    NULL,                                  /* postconfiguration */
+
+    NULL,                                  /* create main configuration */
+    NULL,                                  /* init main configuration */
+
+    ngx_stream_quic_create_srv_conf,       /* create server configuration */
+    ngx_stream_quic_merge_srv_conf,        /* merge server configuration */
+};
+
+
+ngx_module_t  ngx_stream_quic_module = {
+    NGX_MODULE_V1,
+    &ngx_stream_quic_module_ctx,           /* module context */
+    ngx_stream_quic_commands,              /* module directives */
+    NGX_STREAM_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_stream_variable_t  ngx_stream_quic_vars[] = {
+
+    { ngx_string("quic"), NULL, ngx_stream_variable_quic, 0, 0, 0 },
+
+      ngx_stream_null_variable
+};
+
+
+static ngx_int_t
+ngx_stream_variable_quic(ngx_stream_session_t *s,
+    ngx_stream_variable_value_t *v, uintptr_t data)
+{
+    if (s->connection->quic) {
+
+        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_stream_quic_add_variables(ngx_conf_t *cf)
+{
+    ngx_stream_variable_t  *var, *v;
+
+    for (v = ngx_stream_quic_vars; v->name.len; v++) {
+        var = ngx_stream_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_stream_quic_create_srv_conf(ngx_conf_t *cf)
+{
+    ngx_quic_conf_t  *conf;
+
+    conf = ngx_pcalloc(cf->pool, sizeof(ngx_quic_conf_t));
+    if (conf == NULL) {
+        return NULL;
+    }
+
+    /*
+     * set by ngx_pcalloc():
+     *
+     *     conf->tp.original_dcid = { 0, NULL };
+     *     conf->tp.initial_scid = { 0, NULL };
+     *     conf->tp.retry_scid = { 0, NULL };
+     *     conf->tp.preferred_address = NULL
+     *     conf->sr_token_key = { 0, NULL }
+     *     conf->require_alpn = 0;
+     */
+
+    conf->tp.max_idle_timeout = NGX_CONF_UNSET_MSEC;
+    conf->tp.max_ack_delay = NGX_CONF_UNSET_MSEC;
+    conf->tp.max_udp_payload_size = NGX_CONF_UNSET_SIZE;
+    conf->tp.initial_max_data = NGX_CONF_UNSET_SIZE;
+    conf->tp.initial_max_stream_data_bidi_local = NGX_CONF_UNSET_SIZE;
+    conf->tp.initial_max_stream_data_bidi_remote = NGX_CONF_UNSET_SIZE;
+    conf->tp.initial_max_stream_data_uni = NGX_CONF_UNSET_SIZE;
+    conf->tp.initial_max_streams_bidi = NGX_CONF_UNSET_UINT;
+    conf->tp.initial_max_streams_uni = NGX_CONF_UNSET_UINT;
+    conf->tp.ack_delay_exponent = NGX_CONF_UNSET_UINT;
+    conf->tp.disable_active_migration = NGX_CONF_UNSET;
+    conf->tp.active_connection_id_limit = NGX_CONF_UNSET_UINT;
+
+    conf->retry = NGX_CONF_UNSET;
+
+    return conf;
+}
+
+
+static char *
+ngx_stream_quic_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child)
+{
+    ngx_quic_conf_t *prev = parent;
+    ngx_quic_conf_t *conf = child;
+
+    ngx_stream_ssl_conf_t  *scf;
+
+    ngx_conf_merge_msec_value(conf->tp.max_idle_timeout,
+                              prev->tp.max_idle_timeout, 60000);
+
+    ngx_conf_merge_msec_value(conf->tp.max_ack_delay,
+                              prev->tp.max_ack_delay,
+                              NGX_QUIC_DEFAULT_MAX_ACK_DELAY);
+
+    ngx_conf_merge_size_value(conf->tp.max_udp_payload_size,
+                              prev->tp.max_udp_payload_size,
+                              NGX_QUIC_MAX_UDP_PAYLOAD_SIZE);
+
+    ngx_conf_merge_size_value(conf->tp.initial_max_data,
+                              prev->tp.initial_max_data,
+                              16 * NGX_QUIC_STREAM_BUFSIZE);
+
+    ngx_conf_merge_size_value(conf->tp.initial_max_stream_data_bidi_local,
+                              prev->tp.initial_max_stream_data_bidi_local,
+                              NGX_QUIC_STREAM_BUFSIZE);
+
+    ngx_conf_merge_size_value(conf->tp.initial_max_stream_data_bidi_remote,
+                              prev->tp.initial_max_stream_data_bidi_remote,
+                              NGX_QUIC_STREAM_BUFSIZE);
+
+    ngx_conf_merge_size_value(conf->tp.initial_max_stream_data_uni,
+                              prev->tp.initial_max_stream_data_uni,
+                              NGX_QUIC_STREAM_BUFSIZE);
+
+    ngx_conf_merge_uint_value(conf->tp.initial_max_streams_bidi,
+                              prev->tp.initial_max_streams_bidi, 16);
+
+    ngx_conf_merge_uint_value(conf->tp.initial_max_streams_uni,
+                              prev->tp.initial_max_streams_uni, 16);
+
+    ngx_conf_merge_uint_value(conf->tp.ack_delay_exponent,
+                              prev->tp.ack_delay_exponent,
+                              NGX_QUIC_DEFAULT_ACK_DELAY_EXPONENT);
+
+    ngx_conf_merge_value(conf->tp.disable_active_migration,
+                              prev->tp.disable_active_migration, 0);
+
+    ngx_conf_merge_uint_value(conf->tp.active_connection_id_limit,
+                              prev->tp.active_connection_id_limit, 2);
+
+    ngx_conf_merge_value(conf->retry, prev->retry, 0);
+
+    if (conf->retry) {
+        if (RAND_bytes(conf->token_key, sizeof(conf->token_key)) <= 0) {
+            return NGX_CONF_ERROR;
+        }
+    }
+
+    ngx_conf_merge_str_value(conf->sr_token_key, prev->sr_token_key, "");
+
+    if (conf->sr_token_key.len == 0) {
+        conf->sr_token_key.len = NGX_QUIC_DEFAULT_SRT_KEY_LEN;
+
+        conf->sr_token_key.data = ngx_pnalloc(cf->pool, conf->sr_token_key.len);
+        if (conf->sr_token_key.data == NULL) {
+            return NGX_CONF_ERROR;
+        }
+
+        if (RAND_bytes(conf->sr_token_key.data, conf->sr_token_key.len) <= 0) {
+            return NGX_CONF_ERROR;
+        }
+    }
+
+    scf = ngx_stream_conf_get_module_srv_conf(cf, ngx_stream_ssl_module);
+    conf->ssl = &scf->ssl;
+
+    return NGX_CONF_OK;
+}
+
+
+static char *
+ngx_stream_quic_max_ack_delay(ngx_conf_t *cf, void *post, void *data)
+{
+    ngx_msec_t *sp = data;
+
+    if (*sp > 16384) {
+        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
+                           "\"quic_max_ack_delay\" must be less than 16384");
+
+        return NGX_CONF_ERROR;
+    }
+
+    return NGX_CONF_OK;
+}
+
+
+static char *
+ngx_stream_quic_max_udp_payload_size(ngx_conf_t *cf, void *post, void *data)
+{
+    size_t *sp = data;
+
+    if (*sp < NGX_QUIC_MIN_INITIAL_SIZE
+        || *sp > NGX_QUIC_MAX_UDP_PAYLOAD_SIZE)
+    {
+        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
+                           "\"quic_max_udp_payload_size\" must be between "
+                           "%d and %d",
+                           NGX_QUIC_MIN_INITIAL_SIZE,
+                           NGX_QUIC_MAX_UDP_PAYLOAD_SIZE);
+
+        return NGX_CONF_ERROR;
+    }
+
+    return NGX_CONF_OK;
+}
new file mode 100644
--- /dev/null
+++ b/src/stream/ngx_stream_quic_module.h
@@ -0,0 +1,20 @@
+
+/*
+ * Copyright (C) Roman Arutyunyan
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_STREAM_QUIC_H_INCLUDED_
+#define _NGX_STREAM_QUIC_H_INCLUDED_
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_stream.h>
+
+
+extern ngx_module_t  ngx_stream_quic_module;
+
+
+#endif /* _NGX_STREAM_QUIC_H_INCLUDED_ */