From cf514b5782e438ac90f0857bdac9915c2b0a58a1 Mon Sep 17 00:00:00 2001
From: Apertis CI <devel@lists.apertis.org>
Date: Wed, 26 Feb 2025 16:01:27 +0000
Subject: [PATCH] Import Upstream version 1.26.3

---
 .hgtags                                       |  483 ++++
 CHANGES                                       |  301 ++-
 CHANGES.ru                                    |  310 ++-
 LICENSE                                       |    2 +-
 auto/cc/conf                                  |    2 +-
 auto/cc/msvc                                  |   19 +-
 auto/install                                  |    2 +-
 auto/lib/geoip/conf                           |   17 +
 auto/lib/google-perftools/conf                |   16 +
 auto/lib/libatomic/conf                       |    6 +-
 auto/lib/libatomic/make                       |   11 +-
 auto/lib/libgd/conf                           |   17 +
 auto/lib/openssl/conf                         |   63 +-
 auto/lib/openssl/make                         |   15 +-
 auto/lib/openssl/makefile.msvc                |    2 +-
 auto/lib/pcre/conf                            |   16 +
 auto/lib/pcre/make                            |    3 +-
 auto/make                                     |    5 +-
 auto/modules                                  |  109 +-
 auto/options                                  |   15 +
 auto/os/conf                                  |   17 +-
 auto/os/linux                                 |   63 +
 auto/os/win32                                 |    2 +-
 auto/sources                                  |    5 +-
 auto/unix                                     |   48 +
 configure                                     |    1 +
 contrib/vim/syntax/nginx.vim                  | 1173 +++------
 src/core/nginx.c                              |   56 +-
 src/core/nginx.h                              |    4 +-
 src/core/ngx_bpf.c                            |  143 ++
 src/core/ngx_bpf.h                            |   43 +
 src/core/ngx_conf_file.c                      |    4 +-
 src/core/ngx_connection.c                     |   85 +-
 src/core/ngx_connection.h                     |   10 +-
 src/core/ngx_core.h                           |    7 +
 src/core/ngx_hash.h                           |    7 +-
 src/core/ngx_inet.c                           |    2 +-
 src/core/ngx_module.h                         |    5 +
 src/core/ngx_output_chain.c                   |   10 +-
 src/core/ngx_proxy_protocol.c                 |  212 +-
 src/core/ngx_proxy_protocol.h                 |    6 +-
 src/core/ngx_queue.c                          |   52 +-
 src/core/ngx_queue.h                          |    3 +
 src/core/ngx_regex.c                          |   13 +-
 src/core/ngx_resolver.c                       |   61 +-
 src/core/ngx_resolver.h                       |    4 +-
 src/core/ngx_string.c                         |    7 +-
 src/core/ngx_string.h                         |    6 +-
 src/core/ngx_syslog.c                         |   52 +-
 src/core/ngx_syslog.h                         |   22 +-
 src/event/modules/ngx_iocp_module.c           |  379 +++
 src/event/modules/ngx_iocp_module.h           |   22 +
 src/event/ngx_event.c                         |   43 +-
 src/event/ngx_event.h                         |    7 +-
 src/event/ngx_event_acceptex.c                |  229 ++
 src/event/ngx_event_connect.c                 |    2 +
 src/event/ngx_event_connectex.c               |  206 ++
 src/event/ngx_event_openssl.c                 |  614 ++++-
 src/event/ngx_event_openssl.h                 |   50 +-
 src/event/ngx_event_openssl_stapling.c        |    2 +-
 src/event/ngx_event_pipe.c                    |    8 +-
 src/event/ngx_event_udp.c                     |  105 +-
 src/event/ngx_event_udp.h                     |   66 +
 src/event/quic/bpf/bpfgen.sh                  |  113 +
 src/event/quic/bpf/makefile                   |   30 +
 .../quic/bpf/ngx_quic_reuseport_helper.c      |  140 ++
 src/event/quic/ngx_event_quic.c               | 1452 +++++++++++
 src/event/quic/ngx_event_quic.h               |  129 +
 src/event/quic/ngx_event_quic_ack.c           | 1188 +++++++++
 src/event/quic/ngx_event_quic_ack.h           |   30 +
 src/event/quic/ngx_event_quic_bpf.c           |  657 +++++
 src/event/quic/ngx_event_quic_bpf_code.c      |   88 +
 src/event/quic/ngx_event_quic_connection.h    |  305 +++
 src/event/quic/ngx_event_quic_connid.c        |  502 ++++
 src/event/quic/ngx_event_quic_connid.h        |   29 +
 src/event/quic/ngx_event_quic_frames.c        |  895 +++++++
 src/event/quic/ngx_event_quic_frames.h        |   45 +
 src/event/quic/ngx_event_quic_migration.c     | 1003 ++++++++
 src/event/quic/ngx_event_quic_migration.h     |   45 +
 .../quic/ngx_event_quic_openssl_compat.c      |  652 +++++
 .../quic/ngx_event_quic_openssl_compat.h      |   59 +
 src/event/quic/ngx_event_quic_output.c        | 1319 ++++++++++
 src/event/quic/ngx_event_quic_output.h        |   40 +
 src/event/quic/ngx_event_quic_protection.c    | 1243 +++++++++
 src/event/quic/ngx_event_quic_protection.h    |  120 +
 src/event/quic/ngx_event_quic_socket.c        |  237 ++
 src/event/quic/ngx_event_quic_socket.h        |   28 +
 src/event/quic/ngx_event_quic_ssl.c           |  595 +++++
 src/event/quic/ngx_event_quic_ssl.h           |   19 +
 src/event/quic/ngx_event_quic_streams.c       | 1828 ++++++++++++++
 src/event/quic/ngx_event_quic_streams.h       |   44 +
 src/event/quic/ngx_event_quic_tokens.c        |  309 +++
 src/event/quic/ngx_event_quic_tokens.h        |   34 +
 src/event/quic/ngx_event_quic_transport.c     | 2215 +++++++++++++++++
 src/event/quic/ngx_event_quic_transport.h     |  397 +++
 src/event/quic/ngx_event_quic_udp.c           |  420 ++++
 src/http/modules/ngx_http_access_module.c     |    2 +-
 src/http/modules/ngx_http_auth_basic_module.c |    1 +
 .../modules/ngx_http_auth_request_module.c    |   12 +-
 src/http/modules/ngx_http_dav_module.c        |    1 +
 src/http/modules/ngx_http_fastcgi_module.c    |   79 +-
 src/http/modules/ngx_http_flv_module.c        |    3 +-
 src/http/modules/ngx_http_geo_module.c        |   14 +-
 src/http/modules/ngx_http_geoip_module.c      |   14 +-
 src/http/modules/ngx_http_grpc_module.c       |   89 +-
 .../modules/ngx_http_gunzip_filter_module.c   |   18 +-
 .../modules/ngx_http_gzip_filter_module.c     |   31 +-
 .../modules/ngx_http_gzip_static_module.c     |    4 +
 .../modules/ngx_http_headers_filter_module.c  |   55 +-
 src/http/modules/ngx_http_memcached_module.c  |    1 +
 src/http/modules/ngx_http_mp4_module.c        |   35 +-
 src/http/modules/ngx_http_proxy_module.c      |  104 +-
 .../modules/ngx_http_range_filter_module.c    |   16 +
 src/http/modules/ngx_http_realip_module.c     |    7 +-
 src/http/modules/ngx_http_referer_module.c    |    2 +-
 src/http/modules/ngx_http_rewrite_module.c    |    1 +
 src/http/modules/ngx_http_scgi_module.c       |   63 +-
 src/http/modules/ngx_http_ssi_filter_module.c |   39 +-
 src/http/modules/ngx_http_ssi_filter_module.h |    1 +
 src/http/modules/ngx_http_ssl_module.c        |  218 +-
 src/http/modules/ngx_http_ssl_module.h        |    5 -
 src/http/modules/ngx_http_static_module.c     |   10 +-
 src/http/modules/ngx_http_sub_filter_module.c |    8 +-
 .../modules/ngx_http_userid_filter_module.c   |   19 +-
 src/http/modules/ngx_http_uwsgi_module.c      |  146 +-
 src/http/modules/perl/nginx.xs                |  116 +-
 src/http/ngx_http.c                           |   93 +-
 src/http/ngx_http.h                           |   20 +-
 src/http/ngx_http_copy_filter_module.c        |   45 +-
 src/http/ngx_http_core_module.c               |  222 +-
 src/http/ngx_http_core_module.h               |   14 +-
 src/http/ngx_http_file_cache.c                |  103 +-
 src/http/ngx_http_huff_decode.c               |    6 +-
 src/http/ngx_http_parse.c                     |   63 +-
 src/http/ngx_http_request.c                   |  250 +-
 src/http/ngx_http_request.h                   |   20 +-
 src/http/ngx_http_request_body.c              |   28 +
 src/http/ngx_http_script.c                    |    1 +
 src/http/ngx_http_special_response.c          |    1 +
 src/http/ngx_http_upstream.c                  |  449 +++-
 src/http/ngx_http_upstream.h                  |   12 +-
 src/http/ngx_http_variables.c                 |  195 +-
 src/http/ngx_http_variables.h                 |    5 +-
 src/http/ngx_http_write_filter_module.c       |   11 +-
 src/http/v2/ngx_http_v2.c                     |  410 +--
 src/http/v2/ngx_http_v2.h                     |   34 +-
 src/http/v2/ngx_http_v2_filter_module.c       |  631 +----
 src/http/v2/ngx_http_v2_module.c              |  124 +-
 src/http/v2/ngx_http_v2_module.h              |   17 -
 src/http/v3/ngx_http_v3.c                     |  111 +
 src/http/v3/ngx_http_v3.h                     |  160 ++
 src/http/v3/ngx_http_v3_encode.c              |  304 +++
 src/http/v3/ngx_http_v3_encode.h              |   34 +
 src/http/v3/ngx_http_v3_filter_module.c       |  852 +++++++
 src/http/v3/ngx_http_v3_module.c              |  393 +++
 src/http/v3/ngx_http_v3_parse.c               | 1936 ++++++++++++++
 src/http/v3/ngx_http_v3_parse.h               |  146 ++
 src/http/v3/ngx_http_v3_request.c             | 1718 +++++++++++++
 src/http/v3/ngx_http_v3_table.c               |  715 ++++++
 src/http/v3/ngx_http_v3_table.h               |   58 +
 src/http/v3/ngx_http_v3_uni.c                 |  622 +++++
 src/http/v3/ngx_http_v3_uni.h                 |   34 +
 src/mail/ngx_mail_core_module.c               |   51 +-
 src/mail/ngx_mail_handler.c                   |    6 +-
 src/mail/ngx_mail_proxy_module.c              |   16 +-
 src/mail/ngx_mail_ssl_module.c                |   60 +-
 src/mail/ngx_mail_ssl_module.h                |    1 -
 src/os/unix/ngx_darwin_init.c                 |   16 +-
 src/os/unix/ngx_errno.h                       |    1 +
 src/os/unix/ngx_files.c                       |    4 +
 src/os/unix/ngx_linux_config.h                |    4 +
 src/os/unix/ngx_linux_sendfile_chain.c        |    1 +
 src/os/unix/ngx_posix_init.c                  |    5 +-
 src/os/unix/ngx_process_cycle.c               |   21 +-
 src/os/unix/ngx_readv_chain.c                 |    6 +-
 src/os/unix/ngx_recv.c                        |    4 +-
 src/os/unix/ngx_socket.h                      |    2 +
 src/os/unix/ngx_udp_sendmsg_chain.c           |  226 +-
 src/os/win32/nginx.ico                        |  Bin 0 -> 1350 bytes
 src/os/win32/nginx.rc                         |    6 +
 src/os/win32/nginx_icon16.xpm                 |   24 +
 src/os/win32/nginx_icon32.xpm                 |   39 +
 src/os/win32/nginx_icon48.xpm                 |   55 +
 src/os/win32/ngx_alloc.c                      |   44 +
 src/os/win32/ngx_alloc.h                      |   27 +
 src/os/win32/ngx_atomic.h                     |   69 +
 src/os/win32/ngx_dlopen.c                     |   22 +
 src/os/win32/ngx_dlopen.h                     |   32 +
 src/os/win32/ngx_errno.c                      |   60 +
 src/os/win32/ngx_errno.h                      |   72 +
 src/os/win32/ngx_event_log.c                  |   99 +
 src/os/win32/ngx_files.c                      | 1459 +++++++++++
 src/os/win32/ngx_files.h                      |  272 ++
 src/os/win32/ngx_os.h                         |   68 +
 src/os/win32/ngx_process.c                    |  238 ++
 src/os/win32/ngx_process.h                    |   80 +
 src/os/win32/ngx_process_cycle.c              | 1044 ++++++++
 src/os/win32/ngx_process_cycle.h              |   44 +
 src/os/win32/ngx_service.c                    |  134 +
 src/os/win32/ngx_shmem.c                      |  161 ++
 src/os/win32/ngx_shmem.h                      |   33 +
 src/os/win32/ngx_socket.c                     |   49 +
 src/os/win32/ngx_socket.h                     |  255 ++
 src/os/win32/ngx_stat.c                       |   34 +
 src/os/win32/ngx_thread.c                     |   30 +
 src/os/win32/ngx_thread.h                     |   27 +
 src/os/win32/ngx_time.c                       |   83 +
 src/os/win32/ngx_time.h                       |   51 +
 src/os/win32/ngx_udp_wsarecv.c                |  149 ++
 src/os/win32/ngx_user.c                       |   23 +
 src/os/win32/ngx_user.h                       |   25 +
 src/os/win32/ngx_win32_config.h               |  291 +++
 src/os/win32/ngx_win32_init.c                 |  329 +++
 src/os/win32/ngx_wsarecv.c                    |  215 ++
 src/os/win32/ngx_wsarecv_chain.c              |  147 ++
 src/os/win32/ngx_wsasend.c                    |  185 ++
 src/os/win32/ngx_wsasend_chain.c              |  296 +++
 src/stream/ngx_stream.c                       |  844 +++++--
 src/stream/ngx_stream.h                       |  177 +-
 src/stream/ngx_stream_access_module.c         |    2 +-
 src/stream/ngx_stream_core_module.c           |  746 ++++--
 src/stream/ngx_stream_geo_module.c            |    8 +-
 src/stream/ngx_stream_geoip_module.c          |    2 +-
 src/stream/ngx_stream_handler.c               |   10 +-
 src/stream/ngx_stream_pass_module.c           |  327 +++
 src/stream/ngx_stream_proxy_module.c          |   97 +-
 src/stream/ngx_stream_ssl_module.c            |  440 +++-
 src/stream/ngx_stream_ssl_module.h            |    7 +-
 src/stream/ngx_stream_ssl_preread_module.c    |   57 +-
 src/stream/ngx_stream_variables.c             |   62 +
 src/stream/ngx_stream_write_filter_module.c   |    2 +-
 231 files changed, 38197 insertions(+), 3710 deletions(-)
 create mode 100644 .hgtags
 create mode 100644 src/core/ngx_bpf.c
 create mode 100644 src/core/ngx_bpf.h
 create mode 100644 src/event/modules/ngx_iocp_module.c
 create mode 100644 src/event/modules/ngx_iocp_module.h
 create mode 100644 src/event/ngx_event_acceptex.c
 create mode 100644 src/event/ngx_event_connectex.c
 create mode 100644 src/event/ngx_event_udp.h
 create mode 100644 src/event/quic/bpf/bpfgen.sh
 create mode 100644 src/event/quic/bpf/makefile
 create mode 100644 src/event/quic/bpf/ngx_quic_reuseport_helper.c
 create mode 100644 src/event/quic/ngx_event_quic.c
 create mode 100644 src/event/quic/ngx_event_quic.h
 create mode 100644 src/event/quic/ngx_event_quic_ack.c
 create mode 100644 src/event/quic/ngx_event_quic_ack.h
 create mode 100644 src/event/quic/ngx_event_quic_bpf.c
 create mode 100644 src/event/quic/ngx_event_quic_bpf_code.c
 create mode 100644 src/event/quic/ngx_event_quic_connection.h
 create mode 100644 src/event/quic/ngx_event_quic_connid.c
 create mode 100644 src/event/quic/ngx_event_quic_connid.h
 create mode 100644 src/event/quic/ngx_event_quic_frames.c
 create mode 100644 src/event/quic/ngx_event_quic_frames.h
 create mode 100644 src/event/quic/ngx_event_quic_migration.c
 create mode 100644 src/event/quic/ngx_event_quic_migration.h
 create mode 100644 src/event/quic/ngx_event_quic_openssl_compat.c
 create mode 100644 src/event/quic/ngx_event_quic_openssl_compat.h
 create mode 100644 src/event/quic/ngx_event_quic_output.c
 create mode 100644 src/event/quic/ngx_event_quic_output.h
 create mode 100644 src/event/quic/ngx_event_quic_protection.c
 create mode 100644 src/event/quic/ngx_event_quic_protection.h
 create mode 100644 src/event/quic/ngx_event_quic_socket.c
 create mode 100644 src/event/quic/ngx_event_quic_socket.h
 create mode 100644 src/event/quic/ngx_event_quic_ssl.c
 create mode 100644 src/event/quic/ngx_event_quic_ssl.h
 create mode 100644 src/event/quic/ngx_event_quic_streams.c
 create mode 100644 src/event/quic/ngx_event_quic_streams.h
 create mode 100644 src/event/quic/ngx_event_quic_tokens.c
 create mode 100644 src/event/quic/ngx_event_quic_tokens.h
 create mode 100644 src/event/quic/ngx_event_quic_transport.c
 create mode 100644 src/event/quic/ngx_event_quic_transport.h
 create mode 100644 src/event/quic/ngx_event_quic_udp.c
 create mode 100644 src/http/v3/ngx_http_v3.c
 create mode 100644 src/http/v3/ngx_http_v3.h
 create mode 100644 src/http/v3/ngx_http_v3_encode.c
 create mode 100644 src/http/v3/ngx_http_v3_encode.h
 create mode 100644 src/http/v3/ngx_http_v3_filter_module.c
 create mode 100644 src/http/v3/ngx_http_v3_module.c
 create mode 100644 src/http/v3/ngx_http_v3_parse.c
 create mode 100644 src/http/v3/ngx_http_v3_parse.h
 create mode 100644 src/http/v3/ngx_http_v3_request.c
 create mode 100644 src/http/v3/ngx_http_v3_table.c
 create mode 100644 src/http/v3/ngx_http_v3_table.h
 create mode 100644 src/http/v3/ngx_http_v3_uni.c
 create mode 100644 src/http/v3/ngx_http_v3_uni.h
 create mode 100644 src/os/win32/nginx.ico
 create mode 100644 src/os/win32/nginx.rc
 create mode 100644 src/os/win32/nginx_icon16.xpm
 create mode 100644 src/os/win32/nginx_icon32.xpm
 create mode 100644 src/os/win32/nginx_icon48.xpm
 create mode 100644 src/os/win32/ngx_alloc.c
 create mode 100644 src/os/win32/ngx_alloc.h
 create mode 100644 src/os/win32/ngx_atomic.h
 create mode 100644 src/os/win32/ngx_dlopen.c
 create mode 100644 src/os/win32/ngx_dlopen.h
 create mode 100644 src/os/win32/ngx_errno.c
 create mode 100644 src/os/win32/ngx_errno.h
 create mode 100644 src/os/win32/ngx_event_log.c
 create mode 100644 src/os/win32/ngx_files.c
 create mode 100644 src/os/win32/ngx_files.h
 create mode 100644 src/os/win32/ngx_os.h
 create mode 100644 src/os/win32/ngx_process.c
 create mode 100644 src/os/win32/ngx_process.h
 create mode 100644 src/os/win32/ngx_process_cycle.c
 create mode 100644 src/os/win32/ngx_process_cycle.h
 create mode 100644 src/os/win32/ngx_service.c
 create mode 100644 src/os/win32/ngx_shmem.c
 create mode 100644 src/os/win32/ngx_shmem.h
 create mode 100644 src/os/win32/ngx_socket.c
 create mode 100644 src/os/win32/ngx_socket.h
 create mode 100644 src/os/win32/ngx_stat.c
 create mode 100644 src/os/win32/ngx_thread.c
 create mode 100644 src/os/win32/ngx_thread.h
 create mode 100644 src/os/win32/ngx_time.c
 create mode 100644 src/os/win32/ngx_time.h
 create mode 100644 src/os/win32/ngx_udp_wsarecv.c
 create mode 100644 src/os/win32/ngx_user.c
 create mode 100644 src/os/win32/ngx_user.h
 create mode 100644 src/os/win32/ngx_win32_config.h
 create mode 100644 src/os/win32/ngx_win32_init.c
 create mode 100644 src/os/win32/ngx_wsarecv.c
 create mode 100644 src/os/win32/ngx_wsarecv_chain.c
 create mode 100644 src/os/win32/ngx_wsasend.c
 create mode 100644 src/os/win32/ngx_wsasend_chain.c
 create mode 100644 src/stream/ngx_stream_pass_module.c

diff --git a/.hgtags b/.hgtags
new file mode 100644
index 0000000..0dc64a5
--- /dev/null
+++ b/.hgtags
@@ -0,0 +1,483 @@
+551102312e19b704cd22bd7254a9444b9ea14e96 release-0.1.0
+23fb87bddda14ce9faec90f774085634106aded4 release-0.1.1
+295d97d70c698585705345f1a8f92b02e63d6d0d release-0.1.2
+ded1284520cc939ad5ae6ddab39925375e64237d release-0.1.3
+0491b909ef7612d8411f1f59054186c1f3471b52 release-0.1.4
+a88a3e4e158fade0aaa6f3eb25597d5ced2c1075 release-0.1.5
+1f31dc6d33a3a4e65240b08066bf186df9e33b79 release-0.1.6
+5aecc125bc33d81d6214c91d73eb44230a903dde release-0.1.7
+bbd6b0b4a2b15ef8c8f1aaf7b027b6da47303524 release-0.1.8
+2ff194b74f1e60cd04670986973e3b1a6aa3bece release-0.1.9
+31ee1b50354fb829564b81a6f34e8d6ceb2d3f48 release-0.1.10
+8e8f3af115b5b903b2b8f3335de971f18891246f release-0.1.11
+c3c2848fc081e19aec5ffa97e468ad20ddb81df0 release-0.1.12
+ad1e9ebf93bb5ae4c748d471fad2de8a0afc4d2a release-0.1.13
+c5240858380136a67bec261c59b1532560b57885 release-0.1.14
+fd661d14a7fad212e326a7dad6234ea0de992fbf release-0.1.15
+621229427cba1b0af417ff2a101fc4f17a7d93c8 release-0.1.16
+4ebe09b07e3021f1a63b459903ec58f162183b26 release-0.1.17
+31ff3e943e1675a2caf745ba7a981244445d4c98 release-0.1.18
+45a460f82aec80b0f61136aa09f412436d42203a release-0.1.19
+0f836f0288eee4980f57736d50a7a60fa082d8e9 release-0.1.20
+975f62e77f0244f1b631f740be77c72c8f2da1de release-0.1.21
+fc9909c369b2b4716304ac8e38da57b8fb781211 release-0.1.22
+d7c90bb5ce83dab08715e98f9c7b81c7df4b37be release-0.1.23
+64d9afb209da0cd4a917202b7b77e51cc23e2229 release-0.1.24
+d4ea69372b946dc4ec37fc3f5ddd93ff7c3da675 release-0.1.25
+b1648294f6935e993e436fd8a68bca75c74c826d release-0.1.26
+ee66921ecd47a7fa459f70f4a9d660f91f6a1b94 release-0.1.27
+cd3117ad9aab9c58c6f7e677e551e1adbdeaba54 release-0.1.28
+9b8c906f6e63ec2c71cecebfff35819a7d32227d release-0.1.29
+c12967aadd8726daf2d85e3f3e622d89c42db176 release-0.1.30
+fbbf16224844e7d560c00043e8ade8a560415bba release-0.1.31
+417a087c9c4d9abb9b0b9b3f787aff515c43c035 release-0.1.32
+dadfa78d227027348d7f9d1e7b7093d06ba545a0 release-0.1.33
+12234c998d83bfbbaa305273b3dd1b855ca325dc release-0.1.34
+6f00349b98e5f706b82115c6e4dc84456fc0d770 release-0.1.35
+2019117e6b38cc3e89fe4f56a23b271479c627a6 release-0.1.36
+09b42134ac0c42625340f16628e29690a04f8db5 release-0.1.37
+7fa11e5c6e9612ecff5eb58274cc846ae742d1d2 release-0.1.38
+e5d7d0334fdb946133c17523c198800142ac9fe9 release-0.1.39
+c3bd8cdabb8f73e5600a91f198eb7df6fac65e92 release-0.1.40
+d6e48c08d718bf5a9e58c20a37e8ae172bff1139 release-0.1.41
+563ad09abf5042eb41e8ecaf5b4e6c9deaa42731 release-0.1.42
+c9ad0d9c7d59b2fa2a5fe669f1e88debd03e6c04 release-0.1.43
+371c1cee100d7a1b0e6cad4d188e05c98a641ee7 release-0.1.44
+b09ee85d0ac823e36861491eedfc4dfafe282997 release-0.1.45
+511a89da35ada16ae806667d699f9610b4f8499a release-0.2.0
+0148586012ab3dde69b394ec5a389d44bb11c869 release-0.2.1
+818fbd4750b99d14d2736212c939855a11b1f1ef release-0.2.2
+e16a8d574da511622b97d6237d005f40f2cddb30 release-0.2.3
+483cca23060331f2078b1c2984870d80f288ad41 release-0.2.4
+45033d85b30e3f12c407b7cfc518d76e0eda0263 release-0.2.5
+7bd37aef1e7e87858c12b124e253e98558889b50 release-0.2.6
+ecd9c160f25b7a7075dd93383d98a0fc8d8c0a41 release-0.3.0
+c1f965ef97188fd7ef81342dcf8719da18c554d2 release-0.3.1
+e48ebafc69393fc94fecfdf9997c4179fd1ce473 release-0.3.2
+9c2f3ed7a24711d3b42b124d5f831155c8beff95 release-0.3.3
+7c1369d37c7eb0017c28ebcaa0778046f5aafdcc release-0.3.4
+1af2fcb3be8a63796b6b23a488049c92a6bc12f4 release-0.3.5
+174f1e853e1e831b01000aeccfd06a9c8d4d95a2 release-0.3.6
+458b6c3fea65a894c99dd429334a77bb164c7e83 release-0.3.7
+58475592100cb792c125101b6d2d898f5adada30 release-0.3.8
+fcd6fc7ff7f9b132c35193d834e6e7d05026c716 release-0.3.9
+4d9ea73a627a914d364e83e20c58eb1283f4031d release-0.3.10
+4c5c2c55975c1152b5ca5d5d55b32d4dd7945f7a release-0.3.11
+326634fb9d47912ad94221dc2f8fa4bec424d40c release-0.3.12
+4e296b7d25bf62390ca2afb599e395426b94f785 release-0.3.13
+401de5a43ba5a8acdb9c52465193c0ea7354afe7 release-0.3.14
+284cc140593bb16ac71094acd509ab415ff4837d release-0.3.15
+d4e858a5751a7fd08e64586795ed7d336011fbc0 release-0.3.16
+8c0cdd81580eb76d774cfc5724de68e7e5cbbdc2 release-0.3.17
+425af804d968f30eeff01e33b808bc2e8c467f2c release-0.3.18
+ebc68d8ca4962fe3531b7e13444f7ac4395d9c6e release-0.3.19
+9262f520ce214d3d5fd7c842891519336ef85ca6 release-0.3.20
+869b6444d2341a587183859d4df736c7f3381169 release-0.3.21
+77f77f53214a0e3a68fef8226c15532b54f2c365 release-0.3.22
+858700ae46b453ea111b966b6d03f2c21ddcb94e release-0.3.23
+5dac8c7fb71b86aafed8ea352305e7f85759f72e release-0.3.24
+77cdfe394a94a625955e7585e09983b3af9b889b release-0.3.25
+608cf78b24ef7baaf9705e4715a361f26bb16ba9 release-0.3.26
+3f8a2132b93d66ac19bec006205a304a68524a0b release-0.3.27
+c73c5c58c619c22dd3a5a26c91bb0567a62c6930 release-0.3.28
+5ef026a2ac7481f04154f29ab49377bf99aaf96f release-0.3.29
+51b27717f140b71a2e9158807d79da17c888ce4c release-0.3.30
+7a16e281c01f1c7ab3b79c64b43ddb754ea7935e release-0.3.31
+93e85a79757c49d502e42a1cb8264a0f133b0b00 release-0.3.32
+0216fd1471f386168545f772836156761eddec08 release-0.3.33
+fbed40ce7cb4fd7203fecc22a617b9ce5b950fb3 release-0.3.34
+387450de0b4d21652f0b6242a5e26a31e3be8d8c release-0.3.35
+65bf042c0b4f39f18a235464c52f980e9fa24f6b release-0.3.36
+5d2b8078c1c2593b95ec50acfeeafbefa65be344 release-0.3.37
+f971949ffb585d400e0f15508a56232a0f897c80 release-0.3.38
+18268abd340cb351e0c01b9c44e9f8cc05492364 release-0.3.39
+e60fe4cf1d4ea3c34be8c49047c712c6d46c1727 release-0.3.40
+715d243270806d38be776fc3ed826d97514a73d6 release-0.3.41
+5e8fb59c18c19347a5607fb5af075fe1e2925b9a release-0.3.42
+947c6fd27699e0199249ad592151f844c8a900b0 release-0.3.43
+4946078f0a79e6cc952d3e410813aac9b8bda650 release-0.3.44
+95d7da23ea5315a6e9255ce036ed2c51f091f180 release-0.3.45
+1e720b0be7ecd92358da8a60944669fa493e78cd release-0.3.46
+39b7d7b33c918d8f4abc86c4075052d8c19da3c7 release-0.3.47
+7cbef16c71a1f43a07f8141f02e0135c775f0f5b release-0.3.48
+4c8cd5ae5cc100add5c08c252d991b82b1838c6b release-0.3.49
+400711951595aef7cd2ef865b84b31df52b15782 release-0.3.50
+649c9063d0fda23620eaeaf0f6393be0a672ebe7 release-0.3.51
+9079ee4735aefa98165bb2cb26dee4f58d58c1d7 release-0.3.52
+6d5c1535bb9dcd891c5963971f767421a334a728 release-0.3.53
+5fd7a5e990477189c40718c8c3e01002a2c20b81 release-0.3.54
+63a820b0bc6ca629c8e45a069b52d622ddc27a2d release-0.3.55
+562806624c4afb1687cba83bc1852f5d0fecbac3 release-0.3.56
+cec32b3753acf610ac1a6227d14032c1a89d6319 release-0.3.57
+b80f94fa2197b99db5e033fec92e0426d1fe5026 release-0.3.58
+e924670896abe2769ea0fcfd2058b405bed8e8ec release-0.3.59
+921a7ce4baf42fd1091b7e40f89c858c6b23053e release-0.3.60
+df95dcff753a6dc5e94257302aea02c18c7a7c87 release-0.3.61
+7e24168b0853ee7e46c9c7b943ef077dc64f17f5 release-0.4.0
+8183d4ba50f8500465efb27e66dd23f98775dd21 release-0.4.1
+610267a772c7bf911b499d37f66c21ce8f2ebaf7 release-0.4.2
+39dd0b045441e21512e0a6061a03d0df63414d8b release-0.4.3
+5e42c1615f4de0079bd4d8913886d588ce6a295d release-0.4.4
+40266f92b829a870808b3d4ee54c8fccdecbd2d6 release-0.4.5
+56e33c6efee7ff63cdc52bd1cf172bde195079df release-0.4.6
+119bad43bfd493400c57a05848eada2c35a46810 release-0.4.7
+0f404f82a1343cb4e4b277a44e3417385798e5e5 release-0.4.8
+d24a717314365c857b9f283d6072c2a427d5e342 release-0.4.9
+d6f0a00015fdef861fd67fb583b9690638650656 release-0.4.10
+e372368dadd7b2ecd0182b2f1b11db86fc27b2c3 release-0.4.11
+fd57967d850d2361072c72562d1ed03598473478 release-0.4.12
+979045fdcbd20cf7188545c1c589ff240251f890 release-0.4.13
+93c94cfa9f78f0a5740595dde4466ec4fba664f8 release-0.4.14
+589ee12e8d7c2ae5e4f4676bcc7a1279a76f9e8e release-0.5.0
+13416db8a807e5acb4021bc3c581203de57e2f50 release-0.5.1
+06c58edc88831fb31c492a8eddcf2c6056567f18 release-0.5.2
+e2ac5fa41bcba14adbbb722d45c083c30c07bb5c release-0.5.3
+393dbc659df15ccd411680b5c1ce87ed86d4c144 release-0.5.4
+38cc7bd8e04f2c519fd4526c12841a876be353cb release-0.5.5
+6d1fcec2ea79101c756316c015f72e75f601a5ab release-0.5.6
+aed8a9de62456c4b360358bc112ccca32ce02e8d release-0.5.7
+7642f45af67d805452df2667486201c36efaff85 release-0.5.8
+779216610662c3a459935d506f66a9b16b9c9576 release-0.5.9
+9eeb585454f3daa30cf768e95c088a092fe229b9 release-0.5.10
+bb491c8197e38ca10ae63b1f1ecb36bf6fdaf950 release-0.5.11
+613369e08810f36bbcc9734ef1059a03ccbf5e16 release-0.5.12
+bd796ef5c9c9dd34bfac20261b98685e0410122a release-0.5.13
+8a730c49f906d783b47e4b44d735efd083936c64 release-0.5.14
+cb447039152d85e9145139ff2575a6199b9af9d4 release-0.5.15
+64854c7c95d04f838585ca08492823000503fa61 release-0.5.16
+d1ffcf84ea1244f659145c36ff28de6fcdf528b2 release-0.5.17
+796a6e30ca9d29504195c10210dbc8deced0ae83 release-0.5.18
+1f81c711d2a039e1f93b9b515065a2235372d455 release-0.5.19
+8e8f6082654aedb4438c8fca408cfc316c7c5a2a release-0.5.20
+e9551132f7dd40da5719dd5bcf924c86f1436f85 release-0.5.21
+533a252896c4d1cff1586ae42129d610f7497811 release-0.5.22
+f461a49b6c747e0b67f721f2be172902afea5528 release-0.5.23
+2d5ef73671f690b65bf6d9e22e7155f68f484d5a release-0.5.24
+77bf42576050862c268e267ef3e508b145845a25 release-0.5.25
+2aefee4d4ed69eb7567680bf27a2efd212232488 release-0.6.0
+7ac0fe9bec9a2b5f8e191f6fdd6922bfd916a6cb release-0.6.1
+4882735ebc71eeec0fbfe645bdfdb31306872d82 release-0.6.2
+b94731c73d0922f472ff938b9d252ba29020f20c release-0.6.3
+13e649b813d6ccba5db33a61e08ebe09d683cd5b release-0.6.4
+80de622646b0059fd4c553eff47c391bf7503b89 release-0.6.5
+3b05edb2619d5935023b979ee7a9611b61b6c9e5 release-0.6.6
+1dcfd375100c4479611f71efb99271d0a3059215 release-0.6.7
+0228185d4c5772947b842e856ad74cf7f7fd52f3 release-0.6.8
+d1879c52326ecac45c713203670f54220879911e release-0.6.9
+5a80c6ccbe2ad24fa3d4ff6f9fe4a2b07408d19d release-0.6.10
+f88a8b0b39601b19cd740e4db614ab0b5b874686 release-0.6.11
+5557460a7247a1602ae96efd1d0ccf781344cb58 release-0.6.12
+451b02cc770a794cd41363461b446948ae1d8bc8 release-0.6.13
+537b6ef014c4a133e0ab0b7dc817508e0647e315 release-0.6.14
+5e68764f0d6e91a983170fa806e7450a9e9b33fe release-0.6.15
+158aa4e8cc46fcf9504a61469d22daf3476b17bf release-0.6.16
+d8fcca555542619228d9fab89e1665b993f8c3ee release-0.6.17
+60707ebc037086cf004736a0d4979e2a608da033 release-0.6.18
+3c2a99d3a71af846855be35e62edb9a12f363f44 release-0.6.19
+3e0a27f9358ffc1b5249e0ea2311ce7da5c8967e release-0.6.20
+143f4d65b1c875d6563ccb7f653d9157afc72194 release-0.6.21
+95e6160d2b7d0af8ffd1b95a23cadadf8f0b3f6d release-0.6.22
+69a03d5e3b6e6660079ef1ef172db7ac08d8370e release-0.6.23
+3e2a58fb48f1e1a99ebf851e0d47a7034c52ae22 release-0.6.24
+3b8607c05a8bebcfa59235c2126a70d737f0ccf5 release-0.6.25
+07ad5b2606614c4be4ee720c46cf4af126059d31 release-0.6.26
+be531addfabe5214f409d457140c1038af10d199 release-0.6.27
+58f05255d3a345d04baef5cff0ca1ae0ac7ecebb release-0.6.28
+eb2bd21dc8d03f6c94016f04ffb9adaf83a2b606 release-0.6.29
+55408deb3cd171efa9b81d23d7a1dd1ccde0b839 release-0.6.30
+d4288915bba73c4c3c9cf5d39d34e86879eb2b45 release-0.6.31
+0a189588830b8629c4dfea68feb49af36b59e4a9 release-0.7.0
+6ab27a06f3346cf9ec8737f5dbcc82dd4031e30f release-0.7.1
+a07e258cef3b0a0b6e76a6ff4ba4651c5facc85a release-0.7.2
+9992c4583513d2804fc2e7fec860fbc7ab043009 release-0.7.3
+4dc24d50230fbadfc037a414a86390db2de69dd2 release-0.7.4
+9527137b4354a648a229c7169850c7c65272c00d release-0.7.5
+c2f0f7cf306f302254beae512bda18713922375c release-0.7.6
+bbcf6d75556fdcee8bd4aba8f6c27014be9920ee release-0.7.7
+43bde71f0bbe5a33b161760d7f9f980d50386597 release-0.7.8
+769f0dd7081e9011394f264aa22aa66fd79730d8 release-0.7.9
+511edfa732da637f5f0c9476335df7dca994706d release-0.7.10
+0e7023bf6b2461309c29885935443449a41be807 release-0.7.11
+9ad1bd2b21d93902863807528e426862aedee737 release-0.7.12
+d90ea21e24ea35379aef50c5d70564158e110a15 release-0.7.13
+c07d2d20d95c83d804079bbdcecbce4a0c8282f0 release-0.7.14
+0cd7bb051f67eac2b179fb9f9cc988b9ba18ed76 release-0.7.15
+eab2e87deba73ae6abd9cc740e8d4365bed96322 release-0.7.16
+91d7a9eb8ade90e9421d7b1e3c2e47a6bc427876 release-0.7.17
+fc10f7b5cb1305fb930f8ac40b46882d0828d61e release-0.7.18
+9dba9779e37e5969a2d408c792084fd7acfec062 release-0.7.19
+61838d1bcbddc7bc4dd9f30d535573a6fddca8f9 release-0.7.20
+5f665d0fa6a5f6e748157f2ccbc445b2db8125d0 release-0.7.21
+24763afa5efe91e54f00b2ae5b87666eb6c08c3b release-0.7.22
+0562fb355a25266150cbe8c8d4e00f55e3654df3 release-0.7.23
+19c452ecd083550816873a8a31eb3ed9879085e6 release-0.7.24
+46b68faf271d6fdcaaf3ad2c69f6167ea9e9fa28 release-0.7.25
+d04bfca0c7e3ae2e4422bc1d383553139d6f0a19 release-0.7.26
+9425d9c7f8ead95b00a3929a9a5e487e0e3c8499 release-0.7.27
+fbc3e7e8b3ee756568a875f87d8a954a2f9d3bf6 release-0.7.28
+5176dfdf153fc785b18604197d58806f919829ad release-0.7.29
+87e07ccdf0a4ec53458d9d7a4ea66e1239910968 release-0.7.30
+9fddd7e1a7a27f8463867f41a461aad57df461b2 release-0.7.31
+780b2ba1ec6daf6e3773774e26b05b9ff0d5483e release-0.7.32
+83027471a25385b1c671968be761e9aa7a8591a7 release-0.7.33
+1e9a362c3dcee221ca6e34308c483ed93867aca2 release-0.7.34
+c7ee9e15717b54ead5f4a554686e74abe66c6b07 release-0.7.35
+b84548abe9b9d4f4e203f848696e52c8c82c308f release-0.7.36
+3286f0bab8e77dbc7ebb370b1dc379592ccff123 release-0.7.37
+11a4e2ed5b166b9c9f119171aa399a9e3aa4684a release-0.7.38
+f822655d4120629977794c32d3b969343b6c30db release-0.7.39
+8a350e49d2b6751296db6d8e27277ccf63ed412a release-0.7.40
+c4a56c197eeafd71fc1caef7a9d890a330e3c23d release-0.7.41
+a9575a57a5443df39611774cf3840e9088132b0e release-0.7.42
+7503d95d6eadad14c28b2db183ba09848265274b release-0.7.43
+9be652e9114435fc6f1fdec84c0458d56702db91 release-0.7.44
+797e070d480a34b31ddac0d364784773f1bbbcf9 release-0.7.45
+9b5037e7ec7db25875c40f9d1cf20a853388b124 release-0.7.46
+d1d0e6d7ff0ca3c0dd1be1ef1cfff2e3fd0b4e1c release-0.7.47
+9816fb28eda599bfd53940e6d3b6617d1ecb6323 release-0.7.48
+452b9d09df8e3f2fb04b2a33d04d2f3a6436eb34 release-0.7.49
+e4350efa7cf7a0e868c2236a1137de8a33bd8ec6 release-0.7.50
+f51f2bec766c8b6d7e1799d904f18f8ea631bd44 release-0.7.51
+18e39e566781c9c187e2eb62bebd9d669d68f08c release-0.7.52
+b073eaa1dcea296a3488b83d455fab6621a73932 release-0.7.53
+01c6fe6c2a55998434cd3b05dd10ca487ac3fb6c release-0.7.54
+3ed9377e686f2521e6ec15873084381033fb490d release-0.7.55
+a1e44954549c35023b409f728c678be8bf898148 release-0.7.56
+fbb1918a85e38a7becdb1a001dbaf5933f23a919 release-0.7.57
+87f4a49a9cc34a5b11c8784cc5ea89e97b4b2bd8 release-0.7.58
+0c22cb4862c8beb4ee1b9e4627125162a29a5304 release-0.7.59
+82d56c2425ef857cd430b8530a3f9e1127145a67 release-0.8.0
+f4acb784b53cd952559567971b97dde1e818a2b6 release-0.8.1
+b3503597c1a0f0f378afdc5e5e5b85e2c095a4be release-0.8.2
+c98da980514a02ba81c421b25bf91803ffffddf3 release-0.8.3
+db34ec0c53c4b9dec12ffdf70caf89a325ab9577 release-0.8.4
+0914802433b8678ba2cdf91280766f00f4b9b76e release-0.8.5
+ff52ee9e6422f3759f43a442b7ba615595b3a3d4 release-0.8.6
+7607237b4829fff1f60999f4663c50ed9d5182f7 release-0.8.7
+1cef1807bc12cb05ac52fb0e7a0f111d3760b569 release-0.8.8
+a40f8475511d74a468ade29c1505e8986600d7a3 release-0.8.9
+2d9faf2260df6c3e5d4aa1781493c31f27a557d0 release-0.8.10
+d0d61c32331a6505381b5218318f7b69db167ca8 release-0.8.11
+ca7a1c6c798a7eb5b294d4ac3179ec87ecf297d3 release-0.8.12
+81c8277cd8ed55febcb2dd9d9213076f6c0ccb09 release-0.8.13
+3089486a8dc5844b5b6e9f78d536b4b26f7ffa16 release-0.8.14
+d364c2c12dd9723a2dfac3f096f5e55d4cfe6838 release-0.8.15
+52163a1027c3efd6b4c461b60a2ca6266c23e193 release-0.8.16
+06564e9a2d9ec5852132c212e85eda0bf1300307 release-0.8.17
+7aaa959da85e09e29bcac3b1cadec35b0a25b64d release-0.8.18
+4bc73c644329a510da4e96b7241b80ead7772f83 release-0.8.19
+ea3d168fb99c32a5c3545717ecc61e85a375e5dd release-0.8.20
+27951ca037e63dae45ff5b6279124c224ae1255a release-0.8.21
+d56c8b5df517c2bf6e7bc2827b8bf3e08cda90e1 release-0.8.22
+3c6ac062b379b126212cbb27e98a3c8275ef381a release-0.8.23
+89b9173476de14688b1418fbf7df10f91d1719ef release-0.8.24
+aa550cb4159ae0d566006e091fb1c7a888771050 release-0.8.25
+06ce92293f6a65651b08c466f90f55bd69984b98 release-0.8.26
+ea50b0d79ef1d7d901cd0e4dcd7373447849d719 release-0.8.27
+e68b1c35cad86105ff1c5b240f53442f4c36356e release-0.8.28
+78d3582a30afe63fc0adb17c3ac8891a64e47146 release-0.8.29
+9852c5965a3292a1b6127dbb4da9fce4912d898a release-0.8.30
+4f84115914490e572bcbee5069157b7334df2744 release-0.8.31
+59dee6f7f3afeb1fad6ed5983756e48c81ad2a5c release-0.8.32
+a4456378d234c07038456cf32bfe3c651f1d5e82 release-0.8.33
+21cb50799a20575a42f9733342d37a426f79db4d release-0.8.34
+7cb3cb8d78ef7ae63561733ed91fd07933896bc8 release-0.8.35
+aed68639d4eb6afe944b7fb50499c16f7f3f503c release-0.8.36
+265b7fd2ae21c75bbffa5115b83a0123d6c4acb4 release-0.8.37
+fa5f1ca353c0c5aa5415f51d72fd7bbcc02d1ed7 release-0.8.38
+af10bf9d4c6532850aa1f70cdf7504bd109b284c release-0.8.39
+4846ec9f83cb5bc4c8519d5641b35fb9b190430c release-0.8.40
+718b4cb3faf7efe4e0648140f064bf7a92c3f7e8 release-0.8.41
+b5a3065749093282ddd19845e0b77ffc2e54333e release-0.8.42
+34df9fb22fed415cdad52def04095dc6d4b48222 release-0.8.43
+00ec8cd76fb89af27363b76c40d9f88bf4679c3b release-0.8.44
+e16dd52a0d226c23dcae9a11252564a04753bbed release-0.8.45
+f034d9173df0a433e0bbcf5974f12ea9eb9076c0 release-0.8.46
+4434dc967087315efcd0658206a67fe6c85528f3 release-0.8.47
+0b65c962e0cd6783a854877b52c903cb058eec8c release-0.8.48
+a2b7e94b9807e981866bf07e37b715847d1b7120 release-0.8.49
+e7bdb8edc1bab2bc352a9fb6ce765c46575c35bf release-0.8.50
+21dacebd12f65cb57ceb8d2688db5b07fad6e06d release-0.8.51
+67dd7533b99c8945b5b8b5b393504d4e003a1c50 release-0.8.52
+010468d890dbac33a4cae6dfb2017db70721b2fe release-0.8.53
+62b599022a2fa625b526c2ad1711dc6db7d66786 release-0.9.0
+71281dd73b17a0ead5535d531afaee098da723cb release-0.9.1
+16cff36b0e49fc9fdeee13b2e92690286bcc1b3d release-0.9.2
+b7b306325972661117694879d3e22faf4cf0df32 release-0.9.3
+fe671505a8ea86a76f0358b3ec4de84a9037ac2b release-0.9.4
+70542931bc5436d1bbd38f152245d93ac063968d release-0.9.5
+27e2f3b7a3db1819c5d0ba28327ceaba84a13c4e release-0.9.6
+657d05d63915ce2f6c4d763091059f5f85bb10e5 release-0.9.7
+e0fd9f36005923b8f98d1ba1ea583cb7625f318f release-1.0.0
+f8f89eb4e0c27e857ec517d893d4f9a454985084 release-1.0.1
+c50df367648e53d55e80b60a447c9c66caa0d326 release-1.0.2
+80d586db316512b5a9d39f00fe185f7f91523f52 release-1.0.3
+c9c2805ac9245cc48ce6efeba2b4a444f859d6aa release-1.0.4
+fa2c37b1122c2c983b6e91d1188e387d72dde4d6 release-1.0.5
+f31aea5b06654c9163be5acd6d9b7aaf0fdf6b33 release-1.1.0
+44bf95f670656fae01ccb266b3863843ea13d324 release-1.1.1
+da1289482a143dfa016769649bdff636c26f53c8 release-1.1.2
+bac8ba08a6570bac2ecd3bf2ad64b0ac3030c903 release-1.1.3
+911060bc8221d4113a693ae97952a1fa88663ca8 release-1.1.4
+e47531dfabbf8e5f8b8aff9ff353642ea4aa7abb release-1.1.5
+f9ddecfe331462f870a95e4c1c3ba1bb8f19f2d3 release-1.1.6
+378c297bb7459fb99aa9c77decac0d35391a3932 release-1.1.7
+71600ce67510af093d4bc0117a78b3b4678c6b3a release-1.1.8
+482d7d907f1ab92b78084d8b8631ed0eb7dd08f7 release-1.1.9
+c7e65deabf0db5109e8d8f6cf64cd3fb7633a3d1 release-1.1.10
+9590f0cf5aab8e6e0b0c8ae59c70187b2b97d886 release-1.1.11
+ade8fc136430cfc04a8d0885c757968b0987d56c release-1.1.12
+6a6836e65827fd3cb10a406e7bbbe36e0dad8736 release-1.1.13
+6845f4ac909233f5a08ed8a51de137713a888328 release-1.1.14
+2397e9c72f1bc5eac67006e12ad3e33e0ea9ba74 release-1.1.15
+7b7c49639a7bceecabf4963c60b26b65a77d6ce0 release-1.1.16
+f7e1113a9a1648cad122543e7080e895cf2d88f4 release-1.1.17
+2b22743c3079b41233ded0fc35af8aa89bcfab91 release-1.1.18
+0f0b425659e0b26f5bc8ea14a42dbf34de2eaba6 release-1.1.19
+f582d662cc408eb7a132c21f4b298b71d0701abb release-1.2.0
+9ee68d629722f583d43d92271f2eb84281afc630 release-1.3.0
+61b6a3438afef630774e568eefd89c53e3b93287 release-1.3.1
+7ccd50a0a455f2f2d3b241f376e1193ad956196d release-1.2.1
+0000000000000000000000000000000000000000 release-1.2.1
+50107e2d96bbfc2c59e46f889b1a5f68dd10cf19 release-1.3.2
+2c5e1e88c8cf710caf551c5c67eba00443601efe release-1.3.3
+a43447fb82aa03eabcd85352758ae14606a84d35 release-1.3.4
+90f3b4ea7992a7bf9385851a3e77173363091eea release-1.3.5
+3aeb14f88daeb973e4708310daa3dc68ac1200f7 release-1.3.6
+dafd375f1c882b15fa4a9b7aa7c801c55082395e release-1.3.7
+ab7ce0eb4cf78a656750ab1d8e55ef61f7e535ec release-1.3.8
+1b1a9337a7399ad3cdc5e3a2f9fbaaec990271d5 release-1.3.9
+2c053b2572694eb9cd4aed26a498b6cb1f51bbcc release-1.3.10
+36409ac209872ce53019f084e4e07467c5d9d25e release-1.3.11
+560dc55e90c13860a79d8f3e0d67a81c7b0257bb release-1.3.12
+dc195ffe0965b2b9072f8e213fe74ecce38f6773 release-1.3.13
+e04428778567dd4de329bbbe97ad653e22801612 release-1.3.14
+cd84e467c72967b9f5fb4d96bfc708c93edeb634 release-1.3.15
+23159600bdea695db8f9d2890aaf73424303e49c release-1.3.16
+7809529022b83157067e7d1e2fb65d57db5f4d99 release-1.4.0
+48a84bc3ff074a65a63e353b9796ff2b14239699 release-1.5.0
+99eed1a88fc33f32d66e2ec913874dfef3e12fcc release-1.5.1
+5bdca4812974011731e5719a6c398b54f14a6d61 release-1.5.2
+644a079526295aca11c52c46cb81e3754e6ad4ad release-1.5.3
+376a5e7694004048a9d073e4feb81bb54ee3ba91 release-1.5.4
+60e0409b9ec7ee194c6d8102f0656598cc4a6cfe release-1.5.5
+70c5cd3a61cb476c2afb3a61826e59c7cda0b7a7 release-1.5.6
+9ba2542d75bf62a3972278c63561fc2ef5ec573a release-1.5.7
+eaa76f24975948b0ce8be01838d949122d44ed67 release-1.5.8
+5a1759f33b7fa6270e1617c08d7e655b7b127f26 release-1.5.9
+b798fc020e3a84ef68e6c9f47865a319c826d33c release-1.5.10
+f995a10d4c7e9a817157a6ce7b753297ad32897e release-1.5.11
+97b47d95e4449cbde976657cf8cbbc118351ffe0 release-1.5.12
+fd722b890eabc600394349730a093f50dac31639 release-1.5.13
+d161d68df8be32e5cbf72b07db1a707714827803 release-1.7.0
+0351a6d89c3dbcc7a76295024ba6b70e27b9a497 release-1.7.1
+0bd223a546192fdf2e862f33938f4ec2a3b5b283 release-1.7.2
+fe7cd01828d5ca7491059f0690bb4453645eb28b release-1.7.3
+cbb146b120296852e781079d5138b04495bab6df release-1.7.4
+fe129aa02db9001d220f1db7c3c056f79482c111 release-1.7.5
+a8d111bb68847f61d682a3c8792fecb2e52efa2c release-1.7.6
+6d2fbc30f8a7f70136cf08f32d5ff3179d524873 release-1.7.7
+d5ea659b8bab2d6402a2266efa691f705e84001e release-1.7.8
+34b201c1abd1e2d4faeae4650a21574771a03c0e release-1.7.9
+860cfbcc4606ee36d898a9cd0c5ae8858db984d6 release-1.7.10
+2b3b737b5456c05cd63d3d834f4fb4d3776953d0 release-1.7.11
+3ef00a71f56420a9c3e9cec311c9a2109a015d67 release-1.7.12
+53d850fe292f157d2fb999c52788ec1dc53c91ed release-1.9.0
+884a967c369f73ab16ea859670d690fb094d3850 release-1.9.1
+3a32d6e7404a79a0973bcd8d0b83181c5bf66074 release-1.9.2
+e27a215601292872f545a733859e06d01af1017d release-1.9.3
+5cb7e2eed2031e32d2e5422caf9402758c38a6ad release-1.9.4
+942475e10cb47654205ede7ccbe7d568698e665b release-1.9.5
+b78018cfaa2f0ec20494fccb16252daa87c48a31 release-1.9.6
+54117529e40b988590ea2d38aae909b0b191663f release-1.9.7
+1bdc497c81607d854e3edf8b9a3be324c3d136b6 release-1.9.8
+ef107f3ddc237a3007e2769ec04adde0dcf627fa release-1.9.9
+be00ca08e41a69e585b6aff70a725ed6c9e1a876 release-1.9.10
+fe66cff450a95beed36a2515210eb2d7ef62c9d3 release-1.9.11
+ead3907d74f90a14d1646f1b2b56ba01d3d11702 release-1.9.12
+5936b7ed929237f1a73b467f662611cdc0309e51 release-1.9.13
+4106db71cbcb9c8274700199ac17e520902c6c0f release-1.9.14
+13070ecfda67397985f0e986eb9c42ecb46d05b5 release-1.9.15
+271ee30c6791847980cd139d31807541f5e569bf release-1.11.0
+cb783d9cc19761e14e1285d91c38f4b84d0b8756 release-1.11.1
+4d3b3a13a8cf5fc3351a7f167d1c13325e00f21c release-1.11.2
+b83a067949a3384a49fd3d943eb8d0997b31f87b release-1.11.3
+953512ca02c6f63b4fcbbc3e10d0d9835896bf99 release-1.11.4
+5253015a339aaca0a3111473d3e931b6d4752393 release-1.11.5
+5e371426b3bcba4312ce08606194b89b758927d1 release-1.11.6
+5c8f60faf33ca8926473d2da27b4c3c417bd4630 release-1.11.7
+4591da489a30f790def29bc5987f43409b503cae release-1.11.8
+20a45c768e5ed26b740679d0e22045c98727c3cc release-1.11.9
+1ad0999a7ded3d4fb01c7acf8ff57c80b643da7e release-1.11.10
+d8b321a876d6254e9e98795e3b194ef053290354 release-1.11.11
+7f394e433f0003222aa6531931ecc0b24740d5e4 release-1.11.12
+3d0e8655f897959e48cc74e87670bb5492a58871 release-1.11.13
+3671096a45bce570a2afa20b9faf42c7fb0f7e66 release-1.13.0
+539f7893ecb96bee60965528c8958d7eb2f1ce6b release-1.13.1
+5be2b25bdc65775a85f18f68a4be4f58c7384415 release-1.13.2
+8457ce87640f9bfe6221c4ac4466ced20e03bebe release-1.13.3
+bbc642c813c829963ce8197c0ca237ab7601f3d4 release-1.13.4
+0d45b4cf7c2e4e626a5a16e1fe604402ace1cea5 release-1.13.5
+f87da7d9ca02b8ced4caa6c5eb9013ccd47b0117 release-1.13.6
+47cca243d0ed39bf5dcb9859184affc958b79b6f release-1.13.7
+20ca4bcff108d3e66977f4d97508637093492287 release-1.13.8
+fb1212c7eca4c5328fe17d6cd95b010c67336aac release-1.13.9
+31c929e16910c38492581ef474e72fa67c28f124 release-1.13.10
+64179f242cb55fc206bca59de9bfdc4cf5ebcec7 release-1.13.11
+051e5fa03b92b8a564f6b12debd483d267391e82 release-1.13.12
+990b3e885636d763b97ed02d0d2cfc161a4e0c09 release-1.15.0
+4189160cb946bb38d0bc0a452b5eb4cdd8979fb5 release-1.15.1
+b234199c7ed8a156a6bb98f7ff58302c857c954f release-1.15.2
+28b3e17ca7eba1e6a0891afde0e4bc5bcc99c861 release-1.15.3
+49d49835653857daa418e68d6cbfed4958c78fca release-1.15.4
+f062e43d74fc2578bb100a9e82a953efa1eb9e4e release-1.15.5
+2351853ce6867b6166823bdf94333c0a76633c0a release-1.15.6
+051a039ce1c7e09144de4a4846669ec7116cecea release-1.15.7
+ee551e3f6dba336c0d875e266d7d55385f379b42 release-1.15.8
+d2fd76709909767fc727a5b4affcf1dc9ca488a7 release-1.15.9
+75f5c7f628411c79c7044102049f7ab4f7a246e7 release-1.15.10
+5155d0296a5ef9841f035920527ffdb771076b44 release-1.15.11
+0130ca3d58437b3c7c707cdddd813d530c68da9a release-1.15.12
+054c1c46395caff79bb4caf16f40b331f71bb6dd release-1.17.0
+7816bd7dabf6ee86c53c073b90a7143161546e06 release-1.17.1
+2fc9f853a6b7cd29dc84e0af2ed3cf78e0da6ca8 release-1.17.2
+ed4303aa1b31a9aad5440640c0840d9d0af45fed release-1.17.3
+ce2ced3856909f36f8130c99eaa4dbdbae636ddc release-1.17.4
+9af0dddbddb2c368bfedd2801bc100ffad01e19b release-1.17.5
+de68d0d94320cbf033599c6f3ca37e5335c67fd7 release-1.17.6
+e56295fe0ea76bf53b06bffa77a2d3a9a335cb8c release-1.17.7
+fdacd273711ddf20f778c1fb91529ab53979a454 release-1.17.8
+5e8d52bca714d4b85284ddb649d1ba4a3ca978a8 release-1.17.9
+c44970de01474f6f3e01b0adea85ec1d03e3a5f2 release-1.17.10
+cbe6ba650211541310618849168631ce0b788f35 release-1.19.0
+062920e2f3bf871ef7a3d8496edec1b3065faf80 release-1.19.1
+a7b46539f507e6c64efa0efda69ad60b6f4ffbce release-1.19.2
+3cbc2602325f0ac08917a4397d76f5155c34b7b1 release-1.19.3
+dc0cc425fa63a80315f6efb68697cadb6626cdf2 release-1.19.4
+8e5b068f761cd512d10c9671fbde0b568c1fd08b release-1.19.5
+f618488eb769e0ed74ef0d93cd118d2ad79ef94d release-1.19.6
+3fa6e2095a7a51acc630517e1c27a7b7ac41f7b3 release-1.19.7
+8c65d21464aaa5923775f80c32474adc7a320068 release-1.19.8
+da571b8eaf8f30f36c43b3c9b25e01e31f47149c release-1.19.9
+ffcbb9980ee2bad27b4d7b1cd680b14ff47b29aa release-1.19.10
+df34dcc9ac072ffd0945e5a1f3eb7987e8275375 release-1.21.0
+a68ac0677f8553b1f84d357bc9da114731ab5f47 release-1.21.1
+bfbc52374adcbf2f9060afd62de940f6fab3bba5 release-1.21.2
+2217a9c1d0b86026f22700b3c089545db1964f55 release-1.21.3
+39be8a682c58308d9399cddd57e37f9fdb7bdf3e release-1.21.4
+d986378168fd4d70e0121cabac274c560cca9bdf release-1.21.5
+714eb4b2c09e712fb2572a2164ce2bf67638ccac release-1.21.6
+5da2c0902e8e2aa4534008a582a60c61c135960e release-1.23.0
+a63d0a70afea96813ba6667997bc7d68b5863f0d release-1.23.1
+aa901551a7ebad1e8b0f8c11cb44e3424ba29707 release-1.23.2
+ff3afd1ce6a6b65057741df442adfaa71a0e2588 release-1.23.3
+ac779115ed6ee4f3039e9aea414a54e560450ee2 release-1.23.4
+12dcf92b0c2c68552398f19644ce3104459807d7 release-1.25.0
+f8134640e8615448205785cf00b0bc810489b495 release-1.25.1
+1d839f05409d1a50d0f15a2bf36547001f99ae40 release-1.25.2
+294a3d07234f8f65d7b0e0b0e2c5b05c12c5da0a release-1.25.3
+173a0a7dbce569adbb70257c6ec4f0f6bc585009 release-1.25.4
+8618e4d900cc71082fbe7dc72af087937d64faf5 release-1.25.5
+a58202a8c41bf0bd97eef1b946e13105a105520d release-1.26.0
+a63c124e34bcf2d1d1feb8d40ff075103b967c4c release-1.26.1
+e4c5da06073ca24e2ffc5c8f8b8d7833a926356f release-1.26.2
diff --git a/CHANGES b/CHANGES
index c356ba7..9c2fb00 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,15 +1,310 @@
 
-Changes with nginx 1.22.1                                        19 Oct 2022
+Changes with nginx 1.26.3                                        05 Feb 2025
+
+    *) Security: insufficient check in virtual servers handling with TLSv1.3
+       SNI allowed to reuse SSL sessions in a different virtual server, to
+       bypass client SSL certificates verification (CVE-2025-23419).
+
+    *) Bugfix: in the ngx_http_mp4_module.
+       Thanks to Nils Bars.
+
+    *) Workaround: "gzip filter failed to use preallocated memory" alerts
+       appeared in logs when using zlib-ng.
+
+    *) Bugfix: nginx could not build libatomic library using the library
+       sources if the --with-libatomic=DIR option was used.
+
+    *) Bugfix: nginx now ignores QUIC version negotiation packets from
+       clients.
+
+    *) Bugfix: nginx could not be built on Solaris 10 and earlier with the
+       ngx_http_v3_module.
+
+    *) Bugfixes in HTTP/3.
+
+
+Changes with nginx 1.26.2                                        14 Aug 2024
+
+    *) Security: processing of a specially crafted mp4 file by the
+       ngx_http_mp4_module might cause a worker process crash
+       (CVE-2024-7347).
+       Thanks to Nils Bars.
+
+
+Changes with nginx 1.26.1                                        29 May 2024
+
+    *) Security: when using HTTP/3, processing of a specially crafted QUIC
+       session might cause a worker process crash, worker process memory
+       disclosure on systems with MTU larger than 4096 bytes, or might have
+       potential other impact (CVE-2024-32760, CVE-2024-31079,
+       CVE-2024-35200, CVE-2024-34161).
+       Thanks to Nils Bars of CISPA.
+
+    *) Bugfix: reduced memory consumption for long-lived requests if "gzip",
+       "gunzip", "ssi", "sub_filter", or "grpc_pass" directives are used.
+
+    *) Bugfix: nginx could not be built by gcc 14 if the --with-libatomic
+       option was used.
+       Thanks to Edgar Bonet.
+
+    *) Bugfix: in HTTP/3.
+
+
+Changes with nginx 1.26.0                                        23 Apr 2024
+
+    *) 1.26.x stable branch.
+
+
+Changes with nginx 1.25.5                                        16 Apr 2024
+
+    *) Feature: virtual servers in the stream module.
+
+    *) Feature: the ngx_stream_pass_module.
+
+    *) Feature: the "deferred", "accept_filter", and "setfib" parameters of
+       the "listen" directive in the stream module.
+
+    *) Feature: cache line size detection for some architectures.
+       Thanks to Piotr Sikora.
+
+    *) Feature: support for Homebrew on Apple Silicon.
+       Thanks to Piotr Sikora.
+
+    *) Bugfix: Windows cross-compilation bugfixes and improvements.
+       Thanks to Piotr Sikora.
+
+    *) Bugfix: unexpected connection closure while using 0-RTT in QUIC.
+       Thanks to Vladimir Khomutov.
+
+
+Changes with nginx 1.25.4                                        14 Feb 2024
+
+    *) Security: when using HTTP/3 a segmentation fault might occur in a
+       worker process while processing a specially crafted QUIC session
+       (CVE-2024-24989, CVE-2024-24990).
+
+    *) Bugfix: connections with pending AIO operations might be closed
+       prematurely during graceful shutdown of old worker processes.
+
+    *) Bugfix: socket leak alerts no longer logged when fast shutdown was
+       requested after graceful shutdown of old worker processes.
+
+    *) Bugfix: a socket descriptor error, a socket leak, or a segmentation
+       fault in a worker process (for SSL proxying) might occur if AIO was
+       used in a subrequest.
+
+    *) Bugfix: a segmentation fault might occur in a worker process if SSL
+       proxying was used along with the "image_filter" directive and errors
+       with code 415 were redirected with the "error_page" directive.
+
+    *) Bugfixes and improvements in HTTP/3.
+
+
+Changes with nginx 1.25.3                                        24 Oct 2023
+
+    *) Change: improved detection of misbehaving clients when using HTTP/2.
+
+    *) Feature: startup speedup when using a large number of locations.
+       Thanks to Yusuke Nojima.
+
+    *) Bugfix: a segmentation fault might occur in a worker process when
+       using HTTP/2 without SSL; the bug had appeared in 1.25.1.
+
+    *) Bugfix: the "Status" backend response header line with an empty
+       reason phrase was handled incorrectly.
+
+    *) Bugfix: memory leak during reconfiguration when using the PCRE2
+       library.
+       Thanks to ZhenZhong Wu.
+
+    *) Bugfixes and improvements in HTTP/3.
+
+
+Changes with nginx 1.25.2                                        15 Aug 2023
+
+    *) Feature: path MTU discovery when using HTTP/3.
+
+    *) Feature: TLS_AES_128_CCM_SHA256 cipher suite support when using
+       HTTP/3.
+
+    *) Change: now nginx uses appname "nginx" when loading OpenSSL
+       configuration.
+
+    *) Change: now nginx does not try to load OpenSSL configuration if the
+       --with-openssl option was used to built OpenSSL and the OPENSSL_CONF
+       environment variable is not set.
+
+    *) Bugfix: in the $body_bytes_sent variable when using HTTP/3.
+
+    *) Bugfix: in HTTP/3.
+
+
+Changes with nginx 1.25.1                                        13 Jun 2023
+
+    *) Feature: the "http2" directive, which enables HTTP/2 on a per-server
+       basis; the "http2" parameter of the "listen" directive is now
+       deprecated.
+
+    *) Change: HTTP/2 server push support has been removed.
+
+    *) Change: the deprecated "ssl" directive is not supported anymore.
+
+    *) Bugfix: in HTTP/3 when using OpenSSL.
+
+
+Changes with nginx 1.25.0                                        23 May 2023
+
+    *) Feature: experimental HTTP/3 support.
+
+
+Changes with nginx 1.23.4                                        28 Mar 2023
+
+    *) Change: now TLSv1.3 protocol is enabled by default.
+
+    *) Change: now nginx issues a warning if protocol parameters of a
+       listening socket are redefined.
+
+    *) Change: now nginx closes connections with lingering if pipelining was
+       used by the client.
+
+    *) Feature: byte ranges support in the ngx_http_gzip_static_module.
+
+    *) Bugfix: port ranges in the "listen" directive did not work; the bug
+       had appeared in 1.23.3.
+       Thanks to Valentin Bartenev.
+
+    *) Bugfix: incorrect location might be chosen to process a request if a
+       prefix location longer than 255 characters was used in the
+       configuration.
+
+    *) Bugfix: non-ASCII characters in file names on Windows were not
+       supported by the ngx_http_autoindex_module, the ngx_http_dav_module,
+       and the "include" directive.
+
+    *) Change: the logging level of the "data length too long", "length too
+       short", "bad legacy version", "no shared signature algorithms", "bad
+       digest length", "missing sigalgs extension", "encrypted length too
+       long", "bad length", "bad key update", "mixed handshake and non
+       handshake data", "ccs received early", "data between ccs and
+       finished", "packet length too long", "too many warn alerts", "record
+       too small", and "got a fin before a ccs" SSL errors has been lowered
+       from "crit" to "info".
+
+    *) Bugfix: a socket leak might occur when using HTTP/2 and the
+       "error_page" directive to redirect errors with code 400.
+
+    *) Bugfix: messages about logging to syslog errors did not contain
+       information that the errors happened while logging to syslog.
+       Thanks to Safar Safarly.
+
+    *) Workaround: "gzip filter failed to use preallocated memory" alerts
+       appeared in logs when using zlib-ng.
+
+    *) Bugfix: in the mail proxy server.
+
+
+Changes with nginx 1.23.3                                        13 Dec 2022
+
+    *) Bugfix: an error might occur when reading PROXY protocol version 2
+       header with large number of TLVs.
+
+    *) Bugfix: a segmentation fault might occur in a worker process if SSI
+       was used to process subrequests created by other modules.
+       Thanks to Ciel Zhao.
+
+    *) Workaround: when a hostname used in the "listen" directive resolves
+       to multiple addresses, nginx now ignores duplicates within these
+       addresses.
+
+    *) Bugfix: nginx might hog CPU during unbuffered proxying if SSL
+       connections to backends were used.
+
+
+Changes with nginx 1.23.2                                        19 Oct 2022
 
     *) Security: processing of a specially crafted mp4 file by the
        ngx_http_mp4_module might cause a worker process crash, worker
        process memory disclosure, or might have potential other impact
        (CVE-2022-41741, CVE-2022-41742).
 
+    *) Feature: the "$proxy_protocol_tlv_..." variables.
+
+    *) Feature: TLS session tickets encryption keys are now automatically
+       rotated when using shared memory in the "ssl_session_cache"
+       directive.
+
+    *) Change: the logging level of the "bad record type" SSL errors has
+       been lowered from "crit" to "info".
+       Thanks to Murilo Andrade.
+
+    *) Change: now when using shared memory in the "ssl_session_cache"
+       directive the "could not allocate new session" errors are logged at
+       the "warn" level instead of "alert" and not more often than once per
+       second.
+
+    *) Bugfix: nginx/Windows could not be built with OpenSSL 3.0.x.
+
+    *) Bugfix: in logging of the PROXY protocol errors.
+       Thanks to Sergey Brester.
+
+    *) Workaround: shared memory from the "ssl_session_cache" directive was
+       spent on sessions using TLS session tickets when using TLSv1.3 with
+       OpenSSL.
+
+    *) Workaround: timeout specified with the "ssl_session_timeout"
+       directive did not work when using TLSv1.3 with OpenSSL or BoringSSL.
+
+
+Changes with nginx 1.23.1                                        19 Jul 2022
+
+    *) Feature: memory usage optimization in configurations with SSL
+       proxying.
+
+    *) Feature: looking up of IPv4 addresses while resolving now can be
+       disabled with the "ipv4=off" parameter of the "resolver" directive.
+
+    *) Change: the logging level of the "bad key share", "bad extension",
+       "bad cipher", and "bad ecpoint" SSL errors has been lowered from
+       "crit" to "info".
+
+    *) Bugfix: while returning byte ranges nginx did not remove the
+       "Content-Range" header line if it was present in the original backend
+       response.
+
+    *) Bugfix: a proxied response might be truncated during reconfiguration
+       on Linux; the bug had appeared in 1.17.5.
+
+
+Changes with nginx 1.23.0                                        21 Jun 2022
+
+    *) Change in internal API: now header lines are represented as linked
+       lists.
+
+    *) Change: now nginx combines arbitrary header lines with identical
+       names when sending to FastCGI, SCGI, and uwsgi backends, in the
+       $r->header_in() method of the ngx_http_perl_module, and during lookup
+       of the "$http_...", "$sent_http_...", "$sent_trailer_...",
+       "$upstream_http_...", and "$upstream_trailer_..." variables.
+
+    *) Bugfix: if there were multiple "Vary" header lines in the backend
+       response, nginx only used the last of them when caching.
+
+    *) Bugfix: if there were multiple "WWW-Authenticate" header lines in the
+       backend response and errors with code 401 were intercepted or the
+       "auth_request" directive was used, nginx only sent the first of the
+       header lines to the client.
+
+    *) Change: the logging level of the "application data after close
+       notify" SSL errors has been lowered from "crit" to "info".
 
-Changes with nginx 1.22.0                                        24 May 2022
+    *) Bugfix: connections might hang if nginx was built on Linux 2.6.17 or
+       newer, but was used on systems without EPOLLRDHUP support, notably
+       with epoll emulation layers; the bug had appeared in 1.17.5.
+       Thanks to Marcus Ball.
 
-    *) 1.22.x stable branch.
+    *) Bugfix: nginx did not cache the response if the "Expires" response
+       header line disabled caching, but following "Cache-Control" header
+       line enabled caching.
 
 
 Changes with nginx 1.21.6                                        25 Jan 2022
diff --git a/CHANGES.ru b/CHANGES.ru
index 9ee9e11..0d1ba20 100644
--- a/CHANGES.ru
+++ b/CHANGES.ru
@@ -1,5 +1,235 @@
 
-Изменения в nginx 1.22.1                                          19.10.2022
+Изменения в nginx 1.26.3                                          05.02.2025
+
+    *) Безопасность: недостаточная проверка в обработке виртуальных серверов
+       при использовании SNI в TLSv1.3 позволяла повторно использовать
+       SSL-сессию в контексте другого виртуального сервера, чтобы обойти
+       проверку клиентских SSL-сертификатов (CVE-2025-23419).
+
+    *) Исправление: в модуле ngx_http_mp4_module.
+       Спасибо Nils Bars.
+
+    *) Изменение: при использовании zlib-ng в логах появлялись сообщения
+       "gzip filter failed to use preallocated memory".
+
+    *) Исправление: nginx не мог собрать библиотеку libatomic из исходных
+       текстов, если использовался параметр --with-libatomic=DIR.
+
+    *) Исправление: теперь nginx игнорирует пакеты согласования версий QUIC
+       от клиентов.
+
+    *) Исправление: nginx не собирался на Solaris 10 и более ранних с
+       модулем ngx_http_v3_module.
+
+    *) Исправления в HTTP/3.
+
+
+Изменения в nginx 1.26.2                                          14.08.2024
+
+    *) Безопасность: обработка специально созданного mp4-файла модулем
+       ngx_http_mp4_module могла приводить к падению рабочего процесса
+       (CVE-2024-7347).
+       Спасибо Nils Bars.
+
+
+Изменения в nginx 1.26.1                                          29.05.2024
+
+    *) Безопасность: при использовании HTTP/3 обработка специально созданной
+       QUIC-сессии могла приводить к падению рабочего процесса, отправке
+       клиенту содержимого памяти рабочего процесса на системах с MTU больше
+       4096 байт, а также потенциально могла иметь другие последствия
+       (CVE-2024-32760, CVE-2024-31079, CVE-2024-35200, CVE-2024-34161).
+       Спасибо Nils Bars из CISPA.
+
+    *) Исправление: уменьшено потребление памяти для долгоживущих запросов,
+       если используются директивы gzip, gunzip, ssi, sub_filter или
+       grpc_pass.
+
+    *) Исправление: nginx не собирался gcc 14, если использовался параметр
+       --with-libatomic.
+       Спасибо Edgar Bonet.
+
+    *) Исправление: в HTTP/3.
+
+
+Изменения в nginx 1.26.0                                          23.04.2024
+
+    *) Стабильная ветка 1.26.x.
+
+
+Изменения в nginx 1.25.5                                          16.04.2024
+
+    *) Добавление: виртуальные сервера в модуле stream.
+
+    *) Добавление: модуль ngx_stream_pass_module.
+
+    *) Добавление: параметры deferred, accept_filter и setfib директивы
+       listen в модуле stream.
+
+    *) Добавление: определение размера строки кеша процессора для некоторых
+       архитектур.
+       Спасибо Piotr Sikora.
+
+    *) Добавление: поддержка Homebrew на Apple Silicon.
+       Спасибо Piotr Sikora.
+
+    *) Исправление: улучшения и исправления кросс-компиляции для Windows.
+       Спасибо Piotr Sikora.
+
+    *) Исправление: неожиданное закрытие соединения при использовании 0-RTT
+       в QUIC.
+       Спасибо Владимиру Хомутову.
+
+
+Изменения в nginx 1.25.4                                          14.02.2024
+
+    *) Безопасность: при использовании HTTP/3 в рабочем процессе мог
+       произойти segmentation fault во время обработки специально созданной
+       QUIC-сессии (CVE-2024-24989, CVE-2024-24990).
+
+    *) Исправление: соединения с незавершенными AIO-операциями могли
+       закрываться преждевременно во время плавного завершения старых
+       рабочих процессов.
+
+    *) Исправление: теперь nginx не пишет в лог сообщения об утечке сокетов,
+       если во время плавного завершения старых рабочих процессов было
+       запрошено быстрое завершение.
+
+    *) Исправление: при использовании AIO в подзапросе могла происходить
+       ошибка на сокете, утечка сокетов, либо segmentation fault в рабочем
+       процессе (при SSL-проксировании).
+
+    *) Исправление: в рабочем процессе мог произойти segmentation fault,
+       если использовалось SSL-проксирование и директива image_filter, а
+       ошибки с кодом 415 перенаправлялись с помощью директивы error_page.
+
+    *) Исправления и улучшения в HTTP/3.
+
+
+Изменения в nginx 1.25.3                                          24.10.2023
+
+    *) Изменение: улучшено детектирование некорректного поведения клиентов
+       при использовании HTTP/2.
+
+    *) Добавление: уменьшение времени запуска при использовании большого
+       количества location'ов.
+       Спасибо Yusuke Nojima.
+
+    *) Исправление: при использовании HTTP/2 без SSL в рабочем процессе мог
+       произойти segmentation fault; ошибка появилась в 1.25.1.
+
+    *) Исправление: строка "Status" в заголовке ответа бэкенда с пустой
+       поясняющей фразой обрабатывалась некорректно.
+
+    *) Исправление: утечки памяти во время переконфигурации при
+       использовании библиотеки PCRE2.
+       Спасибо ZhenZhong Wu.
+
+    *) Исправления и улучшения в HTTP/3.
+
+
+Изменения в nginx 1.25.2                                          15.08.2023
+
+    *) Добавление: path MTU discovery при использовании HTTP/3.
+
+    *) Добавление: поддержка шифра TLS_AES_128_CCM_SHA256 при использовании
+       HTTP/3.
+
+    *) Изменение: теперь при загрузке конфигурации OpenSSL nginx использует
+       appname "nginx".
+
+    *) Изменение: теперь nginx не пытается загружать конфигурацию OpenSSL,
+       если для сборки OpenSSL использовался параметр --with-openssl и
+       переменная окружения OPENSSL_CONF не установлена.
+
+    *) Исправление: в переменной $body_bytes_sent при использовании HTTP/3.
+
+    *) Исправление: в HTTP/3.
+
+
+Изменения в nginx 1.25.1                                          13.06.2023
+
+    *) Добавление: директива http2, позволяющая включать HTTP/2 в отдельных
+       блоках server; параметр http2 директивы listen объявлен устаревшим.
+
+    *) Изменение: поддержка HTTP/2 server push упразднена.
+
+    *) Изменение: устаревшая директива ssl больше не поддерживается.
+
+    *) Исправление: в HTTP/3 при использовании OpenSSL.
+
+
+Изменения в nginx 1.25.0                                          23.05.2023
+
+    *) Добавление: экспериментальная поддержка HTTP/3.
+
+
+Изменения в nginx 1.23.4                                          28.03.2023
+
+    *) Изменение: теперь протокол TLSv1.3 разрешён по умолчанию.
+
+    *) Изменение: теперь nginx выдаёт предупреждение при переопределении
+       параметров listen-сокета, задающих используемые протоколы.
+
+    *) Изменение: теперь, если клиент использует pipelining, nginx закрывает
+       соединения с ожиданием дополнительных данных (lingering close).
+
+    *) Добавление: поддержка byte ranges для ответов модуля
+       ngx_http_gzip_static_module.
+
+    *) Исправление: диапазоны портов в директиве listen не работали; ошибка
+       появилась в 1.23.3.
+       Спасибо Валентину Бартеневу.
+
+    *) Исправление: для обработки запроса мог быть выбран неверный location,
+       если в конфигурации использовался префиксный location длиннее 255
+       символов.
+
+    *) Исправление: не-ASCII символы в именах файлов на Windows не
+       поддерживались модулями ngx_http_autoindex_module и
+       ngx_http_dav_module, а также директивой include.
+
+    *) Изменение: уровень логгирования ошибок SSL "data length too long",
+       "length too short", "bad legacy version", "no shared signature
+       algorithms", "bad digest length", "missing sigalgs extension",
+       "encrypted length too long", "bad length", "bad key update", "mixed
+       handshake and non handshake data", "ccs received early", "data
+       between ccs and finished", "packet length too long", "too many warn
+       alerts", "record too small", и "got a fin before a ccs" понижен с
+       уровня crit до info.
+
+    *) Исправление: при использовании HTTP/2 и директивы error_page для
+       перенаправления ошибок с кодом 400 могла происходить утечка сокетов.
+
+    *) Исправление: сообщения об ошибках записи в syslog не содержали
+       информации о том, что ошибки происходили в процессе записи в syslog.
+       Спасибо Safar Safarly.
+
+    *) Изменение: при использовании zlib-ng в логах появлялись сообщения
+       "gzip filter failed to use preallocated memory".
+
+    *) Исправление: в почтовом прокси-сервере.
+
+
+Изменения в nginx 1.23.3                                          13.12.2022
+
+    *) Исправление: при чтении заголовка протокола PROXY версии 2,
+       содержащего большое количество TLV, могла возникать ошибка.
+
+    *) Исправление: при использовании SSI для обработки подзапросов,
+       созданных другими модулями, в рабочем процессе мог произойти
+       segmentation fault.
+       Спасибо Ciel Zhao.
+
+    *) Изменение: теперь, если при преобразовании в адреса имени хоста,
+       указанного в директиве listen, возвращается несколько адресов, nginx
+       игнорирует дубликаты среди этих адресов.
+
+    *) Исправление: nginx мог нагружать процессор при небуферизированном
+       проксировании, если использовались SSL-соединения с бэкендами.
+
+
+Изменения в nginx 1.23.2                                          19.10.2022
 
     *) Безопасность: обработка специально созданного mp4-файла модулем
        ngx_http_mp4_module могла приводить к падению рабочего процесса,
@@ -7,10 +237,84 @@
        потенциально могла иметь другие последствия (CVE-2022-41741,
        CVE-2022-41742).
 
+    *) Добавление: переменные "$proxy_protocol_tlv_...".
+
+    *) Добавление: ключи шифрования TLS session tickets теперь автоматически
+       меняются при использовании разделяемой памяти в ssl_session_cache.
+
+    *) Изменение: уровень логгирования ошибок SSL "bad record type" понижен
+       с уровня crit до info.
+       Спасибо Murilo Andrade.
+
+    *) Изменение: теперь при использовании разделяемой памяти в
+       ssl_session_cache сообщения "could not allocate new session"
+       логгируются на уровне warn вместо alert и не чаще одного раза в
+       секунду.
+
+    *) Исправление: nginx/Windows не собирался с OpenSSL 3.0.x.
+
+    *) Исправление: в логгировании ошибок протокола PROXY.
+       Спасибо Сергею Брестеру.
+
+    *) Изменение: при использовании TLSv1.3 с OpenSSL разделяемая память из
+       ssl_session_cache расходовалась в том числе на сессии, использующие
+       TLS session tickets.
+
+    *) Изменение: таймаут, заданный с помощью директивы ssl_session_timeout,
+       не работал при использовании TLSv1.3 с OpenSSL или BoringSSL.
+
+
+Изменения в nginx 1.23.1                                          19.07.2022
+
+    *) Добавление: оптимизация использования памяти в конфигурациях с
+       SSL-проксированием.
+
+    *) Добавление: теперь с помощью параметра "ipv4=off" директивы
+       "resolver" можно запретить поиск IPv4-адресов при преобразовании имён
+       в адреса.
+
+    *) Изменение: уровень логгирования ошибок SSL "bad key share", "bad
+       extension", "bad cipher" и "bad ecpoint" понижен с уровня crit до
+       info.
+
+    *) Исправление: при возврате диапазонов nginx не удалял строку заголовка
+       "Content-Range", если она присутствовала в исходном ответе бэкенда.
+
+    *) Исправление: проксированный ответ мог быть отправлен не полностью при
+       переконфигурации на Linux; ошибка появилась в 1.17.5.
+
+
+Изменения в nginx 1.23.0                                          21.06.2022
+
+    *) Изменение во внутреннем API: теперь строки заголовков представлены
+       связными списками.
+
+    *) Изменение: теперь nginx объединяет произвольные строки заголовков с
+       одинаковыми именами при отправке на FastCGI-, SCGI- и uwsgi-бэкенды,
+       в методе $r->header_in() модуля ngx_http_perl_module, и при доступе
+       через переменные "$http_...", "$sent_http_...", "$sent_trailer_...",
+       "$upstream_http_..." и "$upstream_trailer_...".
+
+    *) Исправление: если в заголовке ответа бэкенда было несколько строк
+       "Vary", при кэшировании nginx учитывал только последнюю из них.
+
+    *) Исправление: если в заголовке ответа бэкенда было несколько строк
+       "WWW-Authenticate" и использовался перехват ошибок с кодом 401 от
+       бэкенда или директива auth_request, nginx пересылал клиенту только
+       первую из этих строк.
+
+    *) Изменение: уровень логгирования ошибок SSL "application data after
+       close notify" понижен с уровня crit до info.
 
-Изменения в nginx 1.22.0                                          24.05.2022
+    *) Исправление: соединения могли зависать, если nginx был собран на
+       Linux 2.6.17 и новее, а использовался на системах без поддержки
+       EPOLLRDHUP, в частности, на системах с эмуляцией epoll; ошибка
+       появилась в 1.17.5.
+       Спасибо Marcus Ball.
 
-    *) Стабильная ветка 1.22.x.
+    *) Исправление: nginx не кэшировал ответ, если строка заголовка ответа
+       "Expires" запрещала кэширование, а последующая строка заголовка
+       "Cache-Control" разрешала кэширование.
 
 
 Изменения в nginx 1.21.6                                          25.01.2022
diff --git a/LICENSE b/LICENSE
index fdedcb7..985470e 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 /* 
  * Copyright (C) 2002-2021 Igor Sysoev
- * Copyright (C) 2011-2022 Nginx, Inc.
+ * Copyright (C) 2011-2024 Nginx, Inc.
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
diff --git a/auto/cc/conf b/auto/cc/conf
index afbca62..ba31cb8 100644
--- a/auto/cc/conf
+++ b/auto/cc/conf
@@ -117,7 +117,7 @@ else
             . auto/cc/acc
         ;;
 
-        msvc*)
+        msvc)
             # MSVC++ 6.0 SP2, MSVC++ Toolkit 2003
 
             . auto/cc/msvc
diff --git a/auto/cc/msvc b/auto/cc/msvc
index 68435ff..567bac7 100644
--- a/auto/cc/msvc
+++ b/auto/cc/msvc
@@ -11,8 +11,8 @@
 # MSVC 2015 (14.0)                        cl 19.00
 
 
-NGX_MSVC_VER=`$NGX_WINE $CC 2>&1 | grep 'Compiler Version' 2>&1 \
-                                 | sed -e 's/^.* Version \(.*\)/\1/'`
+NGX_MSVC_VER=`$NGX_WINE $CC 2>&1 | grep 'C/C++.* [0-9][0-9]*\.[0-9]' 2>&1 \
+                                 | sed -e 's/^.* \([0-9][0-9]*\.[0-9].*\)/\1/'`
 
 echo " + cl version: $NGX_MSVC_VER"
 
@@ -22,6 +22,21 @@ have=NGX_COMPILER value="\"cl $NGX_MSVC_VER\"" . auto/define
 ngx_msvc_ver=`echo $NGX_MSVC_VER | sed -e 's/^\([0-9]*\).*/\1/'`
 
 
+# detect x64 builds
+
+case "$NGX_MSVC_VER" in
+
+    *x64)
+        NGX_MACHINE=amd64
+    ;;
+
+    *)
+        NGX_MACHINE=i386
+    ;;
+
+esac
+
+
 # optimizations
 
 # maximize speed, equivalent to -Og -Oi -Ot -Oy -Ob2 -Gs -GF -Gy
diff --git a/auto/install b/auto/install
index c764fdd..7f73e4b 100644
--- a/auto/install
+++ b/auto/install
@@ -112,7 +112,7 @@ install:	build $NGX_INSTALL_PERL_MODULES
 	test ! -f '\$(DESTDIR)$NGX_SBIN_PATH' \\
 		|| mv '\$(DESTDIR)$NGX_SBIN_PATH' \\
 			'\$(DESTDIR)$NGX_SBIN_PATH.old'
-	cp $NGX_OBJS/nginx '\$(DESTDIR)$NGX_SBIN_PATH'
+	cp $NGX_OBJS/nginx$ngx_binext '\$(DESTDIR)$NGX_SBIN_PATH'
 
 	test -d '\$(DESTDIR)$NGX_CONF_PREFIX' \\
 		|| mkdir -p '\$(DESTDIR)$NGX_CONF_PREFIX'
diff --git a/auto/lib/geoip/conf b/auto/lib/geoip/conf
index 8302aae..47165b1 100644
--- a/auto/lib/geoip/conf
+++ b/auto/lib/geoip/conf
@@ -64,6 +64,23 @@ if [ $ngx_found = no ]; then
 fi
 
 
+if [ $ngx_found = no ]; then
+
+    # Homebrew on Apple Silicon
+
+    ngx_feature="GeoIP library in /opt/homebrew/"
+    ngx_feature_path="/opt/homebrew/include"
+
+    if [ $NGX_RPATH = YES ]; then
+        ngx_feature_libs="-R/opt/homebrew/lib -L/opt/homebrew/lib -lGeoIP"
+    else
+        ngx_feature_libs="-L/opt/homebrew/lib -lGeoIP"
+    fi
+
+    . auto/feature
+fi
+
+
 if [ $ngx_found = yes ]; then
 
     CORE_INCS="$CORE_INCS $ngx_feature_path"
diff --git a/auto/lib/google-perftools/conf b/auto/lib/google-perftools/conf
index 7f1a911..94dadac 100644
--- a/auto/lib/google-perftools/conf
+++ b/auto/lib/google-perftools/conf
@@ -46,6 +46,22 @@ if [ $ngx_found = no ]; then
 fi
 
 
+if [ $ngx_found = no ]; then
+
+    # Homebrew on Apple Silicon
+
+    ngx_feature="Google perftools in /opt/homebrew/"
+
+    if [ $NGX_RPATH = YES ]; then
+        ngx_feature_libs="-R/opt/homebrew/lib -L/opt/homebrew/lib -lprofiler"
+    else
+        ngx_feature_libs="-L/opt/homebrew/lib -lprofiler"
+    fi
+
+    . auto/feature
+fi
+
+
 if [ $ngx_found = yes ]; then
     CORE_LIBS="$CORE_LIBS $ngx_feature_libs"
 
diff --git a/auto/lib/libatomic/conf b/auto/lib/libatomic/conf
index d1e484a..dfdc1a6 100644
--- a/auto/lib/libatomic/conf
+++ b/auto/lib/libatomic/conf
@@ -7,8 +7,8 @@ if [ $NGX_LIBATOMIC != YES ]; then
 
     have=NGX_HAVE_LIBATOMIC . auto/have
     CORE_INCS="$CORE_INCS $NGX_LIBATOMIC/src"
-    LINK_DEPS="$LINK_DEPS $NGX_LIBATOMIC/src/libatomic_ops.a"
-    CORE_LIBS="$CORE_LIBS $NGX_LIBATOMIC/src/libatomic_ops.a"
+    LINK_DEPS="$LINK_DEPS $NGX_LIBATOMIC/build/lib/libatomic_ops.a"
+    CORE_LIBS="$CORE_LIBS $NGX_LIBATOMIC/build/lib/libatomic_ops.a"
 
 else
 
@@ -19,7 +19,7 @@ else
                       #include <atomic_ops.h>"
     ngx_feature_path=
     ngx_feature_libs="-latomic_ops"
-    ngx_feature_test="long  n = 0;
+    ngx_feature_test="AO_t  n = 0;
                       if (!AO_compare_and_swap(&n, 0, 1))
                           return 1;
                       if (AO_fetch_and_add(&n, 1) != 1)
diff --git a/auto/lib/libatomic/make b/auto/lib/libatomic/make
index c90318e..530c746 100644
--- a/auto/lib/libatomic/make
+++ b/auto/lib/libatomic/make
@@ -3,14 +3,19 @@
 # Copyright (C) Nginx, Inc.
 
 
+    case $NGX_LIBATOMIC in
+        /*) ngx_prefix="$NGX_LIBATOMIC/build" ;;
+        *)  ngx_prefix="$PWD/$NGX_LIBATOMIC/build" ;;
+    esac
+
     cat << END                                            >> $NGX_MAKEFILE
 
-$NGX_LIBATOMIC/src/libatomic_ops.a:	$NGX_LIBATOMIC/Makefile
-	cd $NGX_LIBATOMIC && \$(MAKE)
+$NGX_LIBATOMIC/build/lib/libatomic_ops.a:	$NGX_LIBATOMIC/Makefile
+	cd $NGX_LIBATOMIC && \$(MAKE) && \$(MAKE) install
 
 $NGX_LIBATOMIC/Makefile:	$NGX_MAKEFILE
 	cd $NGX_LIBATOMIC \\
 	&& if [ -f Makefile ]; then \$(MAKE) distclean; fi \\
-	&& ./configure
+	&& ./configure --prefix=$ngx_prefix
 
 END
diff --git a/auto/lib/libgd/conf b/auto/lib/libgd/conf
index 6786397..07f5656 100644
--- a/auto/lib/libgd/conf
+++ b/auto/lib/libgd/conf
@@ -65,6 +65,23 @@ if [ $ngx_found = no ]; then
 fi
 
 
+if [ $ngx_found = no ]; then
+
+    # Homebrew on Apple Silicon
+
+    ngx_feature="GD library in /opt/homebrew/"
+    ngx_feature_path="/opt/homebrew/include"
+
+    if [ $NGX_RPATH = YES ]; then
+        ngx_feature_libs="-R/opt/homebrew/lib -L/opt/homebrew/lib -lgd"
+    else
+        ngx_feature_libs="-L/opt/homebrew/lib -lgd"
+    fi
+
+    . auto/feature
+fi
+
+
 if [ $ngx_found = yes ]; then
 
     CORE_INCS="$CORE_INCS $ngx_feature_path"
diff --git a/auto/lib/openssl/conf b/auto/lib/openssl/conf
index 4fb52df..fdf430d 100644
--- a/auto/lib/openssl/conf
+++ b/auto/lib/openssl/conf
@@ -5,12 +5,19 @@
 
 if [ $OPENSSL != NONE ]; then
 
+    have=NGX_OPENSSL . auto/have
+    have=NGX_SSL . auto/have
+
+    have=NGX_OPENSSL_NO_CONFIG . auto/have
+
+    if [ $USE_OPENSSL_QUIC = YES ]; then
+        have=NGX_QUIC . auto/have
+        have=NGX_QUIC_OPENSSL_COMPAT . auto/have
+    fi
+
     case "$CC" in
 
         cl | bcc32)
-            have=NGX_OPENSSL . auto/have
-            have=NGX_SSL . auto/have
-
             CFLAGS="$CFLAGS -DNO_SYS_TYPES_H"
 
             CORE_INCS="$CORE_INCS $OPENSSL/openssl/include"
@@ -33,9 +40,6 @@ if [ $OPENSSL != NONE ]; then
         ;;
 
         *)
-            have=NGX_OPENSSL . auto/have
-            have=NGX_SSL . auto/have
-
             CORE_INCS="$CORE_INCS $OPENSSL/.openssl/include"
             CORE_DEPS="$CORE_DEPS $OPENSSL/.openssl/include/openssl/ssl.h"
             CORE_LIBS="$CORE_LIBS $OPENSSL/.openssl/lib/libssl.a"
@@ -118,11 +122,58 @@ else
             . auto/feature
         fi
 
+        if [ $ngx_found = no ]; then
+
+            # Homebrew on Apple Silicon
+
+            ngx_feature="OpenSSL library in /opt/homebrew/"
+            ngx_feature_path="/opt/homebrew/include"
+
+            if [ $NGX_RPATH = YES ]; then
+                ngx_feature_libs="-R/opt/homebrew/lib -L/opt/homebrew/lib -lssl -lcrypto"
+            else
+                ngx_feature_libs="-L/opt/homebrew/lib -lssl -lcrypto"
+            fi
+
+            ngx_feature_libs="$ngx_feature_libs $NGX_LIBDL $NGX_LIBPTHREAD"
+
+            . auto/feature
+        fi
+
         if [ $ngx_found = yes ]; then
             have=NGX_SSL . auto/have
             CORE_INCS="$CORE_INCS $ngx_feature_path"
             CORE_LIBS="$CORE_LIBS $ngx_feature_libs"
             OPENSSL=YES
+
+            if [ $USE_OPENSSL_QUIC = YES ]; then
+
+                ngx_feature="OpenSSL QUIC support"
+                ngx_feature_name="NGX_QUIC"
+                ngx_feature_test="SSL_set_quic_method(NULL, NULL)"
+                . auto/feature
+
+                if [ $ngx_found = no ]; then
+                    have=NGX_QUIC_OPENSSL_COMPAT . auto/have
+
+                    ngx_feature="OpenSSL QUIC compatibility"
+                    ngx_feature_test="SSL_CTX_add_custom_ext(NULL, 0, 0,
+                                                 NULL, NULL, NULL, NULL, NULL)"
+                    . auto/feature
+                fi
+
+                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 with
+QUIC support into the system, or build the OpenSSL library with QUIC support
+statically from the source with nginx by using --with-openssl=<path> option.
+
+END
+                        exit 1
+                fi
+            fi
         fi
     fi
 
diff --git a/auto/lib/openssl/make b/auto/lib/openssl/make
index 126a238..a7e9369 100644
--- a/auto/lib/openssl/make
+++ b/auto/lib/openssl/make
@@ -7,11 +7,24 @@ case "$CC" in
 
     cl)
 
+        case "$NGX_MACHINE" in
+
+            amd64)
+                OPENSSL_TARGET=VC-WIN64A
+            ;;
+
+            *)
+                OPENSSL_TARGET=VC-WIN32
+            ;;
+
+        esac
+
         cat << END                                            >> $NGX_MAKEFILE
 
 $OPENSSL/openssl/include/openssl/ssl.h:	$NGX_MAKEFILE
 	\$(MAKE) -f auto/lib/openssl/makefile.msvc			\
-		OPENSSL="$OPENSSL" OPENSSL_OPT="$OPENSSL_OPT"
+		OPENSSL="$OPENSSL" OPENSSL_OPT="$OPENSSL_OPT"		\
+		OPENSSL_TARGET="$OPENSSL_TARGET"
 
 END
 
diff --git a/auto/lib/openssl/makefile.msvc b/auto/lib/openssl/makefile.msvc
index 5b90dcb..ed17cde 100644
--- a/auto/lib/openssl/makefile.msvc
+++ b/auto/lib/openssl/makefile.msvc
@@ -6,7 +6,7 @@
 all:
 	cd $(OPENSSL)
 
-	perl Configure VC-WIN32 no-shared				\
+	perl Configure $(OPENSSL_TARGET) no-shared no-threads		\
 		--prefix="%cd%/openssl" 				\
 		--openssldir="%cd%/openssl/ssl" 			\
 		$(OPENSSL_OPT)
diff --git a/auto/lib/pcre/conf b/auto/lib/pcre/conf
index 20c1caf..cdf1809 100644
--- a/auto/lib/pcre/conf
+++ b/auto/lib/pcre/conf
@@ -182,6 +182,22 @@ else
             . auto/feature
         fi
 
+        if [ $ngx_found = no ]; then
+
+            # Homebrew on Apple Silicon
+
+            ngx_feature="PCRE library in /opt/homebrew/"
+            ngx_feature_path="/opt/homebrew/include"
+
+            if [ $NGX_RPATH = YES ]; then
+                ngx_feature_libs="-R/opt/homebrew/lib -L/opt/homebrew/lib -lpcre"
+            else
+                ngx_feature_libs="-L/opt/homebrew/lib -lpcre"
+            fi
+
+            . auto/feature
+        fi
+
         if [ $ngx_found = yes ]; then
             CORE_INCS="$CORE_INCS $ngx_feature_path"
             CORE_LIBS="$CORE_LIBS $ngx_feature_libs"
diff --git a/auto/lib/pcre/make b/auto/lib/pcre/make
index 839ef29..182590a 100644
--- a/auto/lib/pcre/make
+++ b/auto/lib/pcre/make
@@ -36,7 +36,8 @@ if [ $PCRE_LIBRARY = PCRE2 ]; then
                        pcre2_valid_utf.c \
                        pcre2_xclass.c"
 
-        ngx_pcre_test="pcre2_convert.c \
+        ngx_pcre_test="pcre2_chkdint.c \
+                       pcre2_convert.c \
                        pcre2_extuni.c \
                        pcre2_find_bracket.c \
                        pcre2_script_run.c \
diff --git a/auto/make b/auto/make
index ef7c9f6..25ee3fb 100644
--- a/auto/make
+++ b/auto/make
@@ -6,9 +6,10 @@
 echo "creating $NGX_MAKEFILE"
 
 mkdir -p $NGX_OBJS/src/core $NGX_OBJS/src/event $NGX_OBJS/src/event/modules \
+         $NGX_OBJS/src/event/quic \
          $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
diff --git a/auto/modules b/auto/modules
index 94867bf..1a5e421 100644
--- a/auto/modules
+++ b/auto/modules
@@ -102,7 +102,7 @@ if [ $HTTP = YES ]; then
     fi
 
 
-    if [ $HTTP_V2 = YES ]; then
+    if [ $HTTP_V2 = YES -o $HTTP_V3 = YES ]; then
         HTTP_SRCS="$HTTP_SRCS $HTTP_HUFF_SRCS"
     fi
 
@@ -124,6 +124,7 @@ if [ $HTTP = YES ]; then
     #     ngx_http_header_filter
     #     ngx_http_chunked_filter
     #     ngx_http_v2_filter
+    #     ngx_http_v3_filter
     #     ngx_http_range_header_filter
     #     ngx_http_gzip_filter
     #     ngx_http_postpone_filter
@@ -156,6 +157,7 @@ if [ $HTTP = YES ]; then
                       ngx_http_header_filter_module \
                       ngx_http_chunked_filter_module \
                       ngx_http_v2_filter_module \
+                      ngx_http_v3_filter_module \
                       ngx_http_range_header_filter_module \
                       ngx_http_gzip_filter_module \
                       ngx_http_postpone_filter_module \
@@ -217,6 +219,17 @@ if [ $HTTP = YES ]; then
         . auto/module
     fi
 
+    if [ $HTTP_V3 = YES ]; then
+        ngx_module_name=ngx_http_v3_filter_module
+        ngx_module_incs=
+        ngx_module_deps=
+        ngx_module_srcs=src/http/v3/ngx_http_v3_filter_module.c
+        ngx_module_libs=
+        ngx_module_link=$HTTP_V3
+
+        . auto/module
+    fi
+
     if :; then
         ngx_module_name=ngx_http_range_header_filter_module
         ngx_module_incs=
@@ -410,7 +423,6 @@ if [ $HTTP = YES ]; then
 
     if [ $HTTP_V2 = YES ]; then
         have=NGX_HTTP_V2 . auto/have
-        have=NGX_HTTP_HEADERS . auto/have
 
         ngx_module_name=ngx_http_v2_module
         ngx_module_incs=src/http/v2
@@ -426,6 +438,32 @@ if [ $HTTP = YES ]; then
         . auto/module
     fi
 
+    if [ $HTTP_V3 = YES ]; then
+        USE_OPENSSL_QUIC=YES
+        HTTP_SSL=YES
+
+        have=NGX_HTTP_V3 . auto/have
+
+        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_encode.h \
+                         src/http/v3/ngx_http_v3_parse.h \
+                         src/http/v3/ngx_http_v3_table.h \
+                         src/http/v3/ngx_http_v3_uni.h"
+        ngx_module_srcs="src/http/v3/ngx_http_v3.c \
+                         src/http/v3/ngx_http_v3_encode.c \
+                         src/http/v3/ngx_http_v3_parse.c \
+                         src/http/v3/ngx_http_v3_table.c \
+                         src/http/v3/ngx_http_v3_uni.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 :; then
         ngx_module_name=ngx_http_static_module
         ngx_module_incs=
@@ -1128,6 +1166,16 @@ if [ $STREAM != NO ]; then
         . auto/module
     fi
 
+    if [ $STREAM_PASS = YES ]; then
+        ngx_module_name=ngx_stream_pass_module
+        ngx_module_deps=
+        ngx_module_srcs=src/stream/ngx_stream_pass_module.c
+        ngx_module_libs=
+        ngx_module_link=$STREAM_PASS
+
+        . auto/module
+    fi
+
     if [ $STREAM_SET = YES ]; then
         ngx_module_name=ngx_stream_set_module
         ngx_module_deps=
@@ -1272,6 +1320,63 @@ if [ $USE_OPENSSL = YES ]; then
 fi
 
 
+if [ $USE_OPENSSL_QUIC = YES ]; then
+    ngx_module_type=CORE
+    ngx_module_name=ngx_quic_module
+    ngx_module_incs=
+    ngx_module_deps="src/event/quic/ngx_event_quic.h \
+                     src/event/quic/ngx_event_quic_transport.h \
+                     src/event/quic/ngx_event_quic_protection.h \
+                     src/event/quic/ngx_event_quic_connection.h \
+                     src/event/quic/ngx_event_quic_frames.h \
+                     src/event/quic/ngx_event_quic_connid.h \
+                     src/event/quic/ngx_event_quic_migration.h \
+                     src/event/quic/ngx_event_quic_streams.h \
+                     src/event/quic/ngx_event_quic_ssl.h \
+                     src/event/quic/ngx_event_quic_tokens.h \
+                     src/event/quic/ngx_event_quic_ack.h \
+                     src/event/quic/ngx_event_quic_output.h \
+                     src/event/quic/ngx_event_quic_socket.h \
+                     src/event/quic/ngx_event_quic_openssl_compat.h"
+    ngx_module_srcs="src/event/quic/ngx_event_quic.c \
+                     src/event/quic/ngx_event_quic_udp.c \
+                     src/event/quic/ngx_event_quic_transport.c \
+                     src/event/quic/ngx_event_quic_protection.c \
+                     src/event/quic/ngx_event_quic_frames.c \
+                     src/event/quic/ngx_event_quic_connid.c \
+                     src/event/quic/ngx_event_quic_migration.c \
+                     src/event/quic/ngx_event_quic_streams.c \
+                     src/event/quic/ngx_event_quic_ssl.c \
+                     src/event/quic/ngx_event_quic_tokens.c \
+                     src/event/quic/ngx_event_quic_ack.c \
+                     src/event/quic/ngx_event_quic_output.c \
+                     src/event/quic/ngx_event_quic_socket.c \
+                     src/event/quic/ngx_event_quic_openssl_compat.c"
+
+    ngx_module_libs=
+    ngx_module_link=YES
+    ngx_module_order=
+
+    . auto/module
+
+    if [ $QUIC_BPF = YES -a $SO_COOKIE_FOUND = YES ]; then
+        ngx_module_type=CORE
+        ngx_module_name=ngx_quic_bpf_module
+        ngx_module_incs=
+        ngx_module_deps=
+        ngx_module_srcs="src/event/quic/ngx_event_quic_bpf.c \
+                         src/event/quic/ngx_event_quic_bpf_code.c"
+        ngx_module_libs=
+        ngx_module_link=YES
+        ngx_module_order=
+
+        . auto/module
+
+        have=NGX_QUIC_BPF . auto/have
+    fi
+fi
+
+
 if [ $USE_PCRE = YES ]; then
     ngx_module_type=CORE
     ngx_module_name=ngx_regex_module
diff --git a/auto/options b/auto/options
index 48f3a1a..6a6e990 100644
--- a/auto/options
+++ b/auto/options
@@ -45,6 +45,8 @@ USE_THREADS=NO
 
 NGX_FILE_AIO=NO
 
+QUIC_BPF=NO
+
 HTTP=YES
 
 NGX_HTTP_LOG_PATH=
@@ -59,6 +61,7 @@ HTTP_CHARSET=YES
 HTTP_GZIP=YES
 HTTP_SSL=NO
 HTTP_V2=NO
+HTTP_V3=NO
 HTTP_SSI=YES
 HTTP_REALIP=NO
 HTTP_XSLT=NO
@@ -124,6 +127,7 @@ STREAM_GEOIP=NO
 STREAM_MAP=YES
 STREAM_SPLIT_CLIENTS=YES
 STREAM_RETURN=YES
+STREAM_PASS=YES
 STREAM_SET=YES
 STREAM_UPSTREAM_HASH=YES
 STREAM_UPSTREAM_LEAST_CONN=YES
@@ -149,6 +153,7 @@ PCRE_JIT=NO
 PCRE2=YES
 
 USE_OPENSSL=NO
+USE_OPENSSL_QUIC=NO
 OPENSSL=NONE
 
 USE_ZLIB=NO
@@ -166,6 +171,8 @@ USE_GEOIP=NO
 NGX_GOOGLE_PERFTOOLS=NO
 NGX_CPP_TEST=NO
 
+SO_COOKIE_FOUND=NO
+
 NGX_LIBATOMIC=NO
 
 NGX_CPU_CACHE_LINE=
@@ -211,6 +218,8 @@ do
 
         --with-file-aio)                 NGX_FILE_AIO=YES           ;;
 
+        --without-quic_bpf_module)       QUIC_BPF=NONE              ;;
+
         --with-ipv6)
             NGX_POST_CONF_MSG="$NGX_POST_CONF_MSG
 $0: warning: the \"--with-ipv6\" option is deprecated"
@@ -228,6 +237,7 @@ $0: warning: the \"--with-ipv6\" option is deprecated"
 
         --with-http_ssl_module)          HTTP_SSL=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              ;;
@@ -328,6 +338,7 @@ use the \"--with-mail_ssl_module\" option instead"
         --without-stream_split_clients_module)
                                          STREAM_SPLIT_CLIENTS=NO    ;;
         --without-stream_return_module)  STREAM_RETURN=NO           ;;
+        --without-stream_pass_module)    STREAM_PASS=NO             ;;
         --without-stream_set_module)     STREAM_SET=NO              ;;
         --without-stream_upstream_hash_module)
                                          STREAM_UPSTREAM_HASH=NO    ;;
@@ -443,8 +454,11 @@ cat << END
 
   --with-file-aio                    enable file AIO support
 
+  --without-quic_bpf_module          disable ngx_quic_bpf_module
+
   --with-http_ssl_module             enable ngx_http_ssl_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
@@ -544,6 +558,7 @@ cat << END
   --without-stream_split_clients_module
                                      disable ngx_stream_split_clients_module
   --without-stream_return_module     disable ngx_stream_return_module
+  --without-stream_pass_module       disable ngx_stream_pass_module
   --without-stream_set_module        disable ngx_stream_set_module
   --without-stream_upstream_hash_module
                                      disable ngx_stream_upstream_hash_module
diff --git a/auto/os/conf b/auto/os/conf
index 7c6cb69..bb0ce4e 100644
--- a/auto/os/conf
+++ b/auto/os/conf
@@ -110,11 +110,26 @@ case "$NGX_MACHINE" in
         NGX_MACH_CACHE_LINE=64
     ;;
 
-    aarch64 )
+    aarch64 | arm64)
         have=NGX_ALIGNMENT value=16 . auto/define
         NGX_MACH_CACHE_LINE=64
     ;;
 
+    ppc64* | powerpc64*)
+        have=NGX_ALIGNMENT value=16 . auto/define
+        NGX_MACH_CACHE_LINE=128
+    ;;
+
+    riscv64)
+        have=NGX_ALIGNMENT value=16 . auto/define
+        NGX_MACH_CACHE_LINE=64
+    ;;
+
+    s390x)
+        have=NGX_ALIGNMENT value=16 . auto/define
+        NGX_MACH_CACHE_LINE=256
+    ;;
+
     *)
         have=NGX_ALIGNMENT value=16 . auto/define
         NGX_MACH_CACHE_LINE=32
diff --git a/auto/os/linux b/auto/os/linux
index 74b5870..bc0556b 100644
--- a/auto/os/linux
+++ b/auto/os/linux
@@ -228,8 +228,71 @@ ngx_feature_test="struct crypt_data  cd;
                   crypt_r(\"key\", \"salt\", &cd);"
 . auto/feature
 
+if [ $ngx_found = yes ]; then
+    CRYPT_LIB="-lcrypt"
+fi
+
 
 ngx_include="sys/vfs.h";     . auto/include
 
 
+# BPF sockhash
+
+ngx_feature="BPF sockhash"
+ngx_feature_name="NGX_HAVE_BPF"
+ngx_feature_run=no
+ngx_feature_incs="#include <linux/bpf.h>
+                  #include <sys/syscall.h>"
+ngx_feature_path=
+ngx_feature_libs=
+ngx_feature_test="union bpf_attr attr = { 0 };
+
+                  attr.map_flags = 0;
+                  attr.map_type = BPF_MAP_TYPE_SOCKHASH;
+
+                  syscall(__NR_bpf, 0, &attr, 0);"
+. auto/feature
+
+if [ $ngx_found = yes ]; then
+    CORE_SRCS="$CORE_SRCS src/core/ngx_bpf.c"
+    CORE_DEPS="$CORE_DEPS src/core/ngx_bpf.h"
+
+    if [ $QUIC_BPF != NONE ]; then
+        QUIC_BPF=YES
+    fi
+fi
+
+
+ngx_feature="SO_COOKIE"
+ngx_feature_name="NGX_HAVE_SO_COOKIE"
+ngx_feature_run=no
+ngx_feature_incs="#include <sys/socket.h>
+                  $NGX_INCLUDE_INTTYPES_H"
+ngx_feature_path=
+ngx_feature_libs=
+ngx_feature_test="socklen_t optlen = sizeof(uint64_t);
+                  uint64_t cookie;
+                  getsockopt(0, SOL_SOCKET, SO_COOKIE, &cookie, &optlen)"
+. auto/feature
+
+if [ $ngx_found = yes ]; then
+    SO_COOKIE_FOUND=YES
+fi
+
+
+# UDP segmentation offloading
+
+ngx_feature="UDP_SEGMENT"
+ngx_feature_name="NGX_HAVE_UDP_SEGMENT"
+ngx_feature_run=no
+ngx_feature_incs="#include <sys/socket.h>
+                  #include <netinet/udp.h>"
+ngx_feature_path=
+ngx_feature_libs=
+ngx_feature_test="socklen_t optlen = sizeof(int);
+                  int val;
+                  getsockopt(0, SOL_UDP, UDP_SEGMENT, &val, &optlen)"
+. auto/feature
+
+
 CC_AUX_FLAGS="$cc_aux_flags -D_GNU_SOURCE -D_FILE_OFFSET_BITS=64"
diff --git a/auto/os/win32 b/auto/os/win32
index b821ae6..bce764b 100644
--- a/auto/os/win32
+++ b/auto/os/win32
@@ -18,7 +18,7 @@ ngx_binext=".exe"
 
 case "$NGX_CC_NAME" in
 
-    gcc)
+    clang | gcc)
         CORE_LIBS="$CORE_LIBS -ladvapi32 -lws2_32"
         MAIN_LINK="$MAIN_LINK -Wl,--export-all-symbols"
         MAIN_LINK="$MAIN_LINK -Wl,--out-implib=$NGX_OBJS/libnginx.a"
diff --git a/auto/sources b/auto/sources
index 156f797..46408ee 100644
--- a/auto/sources
+++ b/auto/sources
@@ -83,13 +83,14 @@ CORE_SRCS="src/core/nginx.c \
 
 EVENT_MODULES="ngx_events_module ngx_event_core_module"
 
-EVENT_INCS="src/event src/event/modules"
+EVENT_INCS="src/event src/event/modules src/event/quic"
 
 EVENT_DEPS="src/event/ngx_event.h \
             src/event/ngx_event_timer.h \
             src/event/ngx_event_posted.h \
             src/event/ngx_event_connect.h \
-            src/event/ngx_event_pipe.h"
+            src/event/ngx_event_pipe.h \
+            src/event/ngx_event_udp.h"
 
 EVENT_SRCS="src/event/ngx_event.c \
             src/event/ngx_event_timer.c \
diff --git a/auto/unix b/auto/unix
index 8671019..f29e69c 100644
--- a/auto/unix
+++ b/auto/unix
@@ -448,6 +448,54 @@ ngx_feature_test="setsockopt(0, IPPROTO_IPV6, IPV6_RECVPKTINFO, NULL, 0)"
 . auto/feature
 
 
+# IP packet fragmentation
+
+ngx_feature="IP_MTU_DISCOVER"
+ngx_feature_name="NGX_HAVE_IP_MTU_DISCOVER"
+ngx_feature_run=no
+ngx_feature_incs="#include <sys/socket.h>
+                  #include <netinet/in.h>"
+ngx_feature_path=
+ngx_feature_libs=
+ngx_feature_test="(void) IP_PMTUDISC_DO;
+                  setsockopt(0, IPPROTO_IP, IP_MTU_DISCOVER, NULL, 0)"
+. auto/feature
+
+
+ngx_feature="IPV6_MTU_DISCOVER"
+ngx_feature_name="NGX_HAVE_IPV6_MTU_DISCOVER"
+ngx_feature_run=no
+ngx_feature_incs="#include <sys/socket.h>
+                  #include <netinet/in.h>"
+ngx_feature_path=
+ngx_feature_libs=
+ngx_feature_test="(void) IPV6_PMTUDISC_DO;
+                  setsockopt(0, IPPROTO_IPV6, IPV6_MTU_DISCOVER, NULL, 0)"
+. auto/feature
+
+
+ngx_feature="IP_DONTFRAG"
+ngx_feature_name="NGX_HAVE_IP_DONTFRAG"
+ngx_feature_run=no
+ngx_feature_incs="#include <sys/socket.h>
+                  #include <netinet/in.h>"
+ngx_feature_path=
+ngx_feature_libs=
+ngx_feature_test="setsockopt(0, IPPROTO_IP, IP_DONTFRAG, NULL, 0)"
+. auto/feature
+
+
+ngx_feature="IPV6_DONTFRAG"
+ngx_feature_name="NGX_HAVE_IPV6_DONTFRAG"
+ngx_feature_run=no
+ngx_feature_incs="#include <sys/socket.h>
+                  #include <netinet/in.h>"
+ngx_feature_path=
+ngx_feature_libs=
+ngx_feature_test="setsockopt(0, IPPROTO_IP, IPV6_DONTFRAG, NULL, 0)"
+. auto/feature
+
+
 ngx_feature="TCP_DEFER_ACCEPT"
 ngx_feature_name="NGX_HAVE_DEFERRED_ACCEPT"
 ngx_feature_run=no
diff --git a/configure b/configure
index 474d69e..5b88ebb 100755
--- a/configure
+++ b/configure
@@ -44,6 +44,7 @@ if test -z "$NGX_PLATFORM"; then
 else
     echo "building for $NGX_PLATFORM"
     NGX_SYSTEM=$NGX_PLATFORM
+    NGX_MACHINE=i386
 fi
 
 . auto/cc/conf
diff --git a/contrib/vim/syntax/nginx.vim b/contrib/vim/syntax/nginx.vim
index 6828cd3..29eef7a 100644
--- a/contrib/vim/syntax/nginx.vim
+++ b/contrib/vim/syntax/nginx.vim
@@ -65,12 +65,12 @@ syn match ngxListenComment '#.*$'
     \ contained
     \ nextgroup=@ngxListenParams skipwhite skipempty
 syn keyword ngxListenOptions contained
-    \ default_server ssl http2 proxy_protocol
+    \ default_server ssl quic proxy_protocol
     \ setfib fastopen backlog rcvbuf sndbuf accept_filter deferred bind
     \ ipv6only reuseport so_keepalive
     \ nextgroup=@ngxListenParams skipwhite skipempty
 syn keyword ngxListenOptionsDeprecated contained
-    \ spdy
+    \ http2
     \ nextgroup=@ngxListenParams skipwhite skipempty
 syn cluster ngxListenParams
     \ contains=ngxListenParam,ngxListenString,ngxListenComment
@@ -90,7 +90,6 @@ syn keyword ngxDirectiveBlock contained if
 syn keyword ngxDirectiveBlock contained geo
 syn keyword ngxDirectiveBlock contained map
 syn keyword ngxDirectiveBlock contained split_clients
-syn keyword ngxDirectiveBlock contained match
 
 syn keyword ngxDirectiveImportant contained include
 syn keyword ngxDirectiveImportant contained root
@@ -111,19 +110,13 @@ syn keyword ngxDirectiveControl contained set
 syn keyword ngxDirectiveError contained error_page
 syn keyword ngxDirectiveError contained post_action
 
-syn keyword ngxDirectiveDeprecated contained limit_zone
 syn keyword ngxDirectiveDeprecated contained proxy_downstream_buffer
 syn keyword ngxDirectiveDeprecated contained proxy_upstream_buffer
-syn keyword ngxDirectiveDeprecated contained spdy_chunk_size
-syn keyword ngxDirectiveDeprecated contained spdy_headers_comp
-syn keyword ngxDirectiveDeprecated contained spdy_keepalive_timeout
-syn keyword ngxDirectiveDeprecated contained spdy_max_concurrent_streams
-syn keyword ngxDirectiveDeprecated contained spdy_pool_size
-syn keyword ngxDirectiveDeprecated contained spdy_recv_buffer_size
-syn keyword ngxDirectiveDeprecated contained spdy_recv_timeout
-syn keyword ngxDirectiveDeprecated contained spdy_streams_index_size
-syn keyword ngxDirectiveDeprecated contained ssl
-syn keyword ngxDirectiveDeprecated contained upstream_conf
+syn keyword ngxDirectiveDeprecated contained http2_idle_timeout
+syn keyword ngxDirectiveDeprecated contained http2_max_field_size
+syn keyword ngxDirectiveDeprecated contained http2_max_header_size
+syn keyword ngxDirectiveDeprecated contained http2_max_requests
+syn keyword ngxDirectiveDeprecated contained http2_recv_timeout
 
 syn keyword ngxDirective contained absolute_redirect
 syn keyword ngxDirective contained accept_mutex
@@ -141,7 +134,6 @@ syn keyword ngxDirective contained alias
 syn keyword ngxDirective contained allow
 syn keyword ngxDirective contained ancient_browser
 syn keyword ngxDirective contained ancient_browser_value
-syn keyword ngxDirective contained api
 syn keyword ngxDirective contained auth_basic
 syn keyword ngxDirective contained auth_basic_user_file
 syn keyword ngxDirective contained auth_delay
@@ -149,14 +141,6 @@ syn keyword ngxDirective contained auth_http
 syn keyword ngxDirective contained auth_http_header
 syn keyword ngxDirective contained auth_http_pass_client_cert
 syn keyword ngxDirective contained auth_http_timeout
-syn keyword ngxDirective contained auth_jwt
-syn keyword ngxDirective contained auth_jwt_claim_set
-syn keyword ngxDirective contained auth_jwt_header_set
-syn keyword ngxDirective contained auth_jwt_key_file
-syn keyword ngxDirective contained auth_jwt_key_request
-syn keyword ngxDirective contained auth_jwt_leeway
-syn keyword ngxDirective contained auth_jwt_require
-syn keyword ngxDirective contained auth_jwt_type
 syn keyword ngxDirective contained auth_request
 syn keyword ngxDirective contained auth_request_set
 syn keyword ngxDirective contained autoindex
@@ -197,8 +181,6 @@ syn keyword ngxDirective contained error_log
 syn keyword ngxDirective contained etag
 syn keyword ngxDirective contained eventport_events
 syn keyword ngxDirective contained expires
-syn keyword ngxDirective contained f4f
-syn keyword ngxDirective contained f4f_buffer_size
 syn keyword ngxDirective contained fastcgi_bind
 syn keyword ngxDirective contained fastcgi_buffer_size
 syn keyword ngxDirective contained fastcgi_buffering
@@ -215,7 +197,6 @@ syn keyword ngxDirective contained fastcgi_cache_max_range_offset
 syn keyword ngxDirective contained fastcgi_cache_methods
 syn keyword ngxDirective contained fastcgi_cache_min_uses
 syn keyword ngxDirective contained fastcgi_cache_path
-syn keyword ngxDirective contained fastcgi_cache_purge
 syn keyword ngxDirective contained fastcgi_cache_revalidate
 syn keyword ngxDirective contained fastcgi_cache_use_stale
 syn keyword ngxDirective contained fastcgi_cache_valid
@@ -299,28 +280,20 @@ syn keyword ngxDirective contained gzip_types
 syn keyword ngxDirective contained gzip_vary
 syn keyword ngxDirective contained gzip_window
 syn keyword ngxDirective contained hash
-syn keyword ngxDirective contained health_check
-syn keyword ngxDirective contained health_check_timeout
-syn keyword ngxDirective contained hls
-syn keyword ngxDirective contained hls_buffers
-syn keyword ngxDirective contained hls_forward_args
-syn keyword ngxDirective contained hls_fragment
-syn keyword ngxDirective contained hls_mp4_buffer_size
-syn keyword ngxDirective contained hls_mp4_max_buffer_size
+syn keyword ngxDirective contained http2
 syn keyword ngxDirective contained http2_body_preread_size
 syn keyword ngxDirective contained http2_chunk_size
-syn keyword ngxDirective contained http2_idle_timeout
 syn keyword ngxDirective contained http2_max_concurrent_pushes
 syn keyword ngxDirective contained http2_max_concurrent_streams
-syn keyword ngxDirective contained http2_max_field_size
-syn keyword ngxDirective contained http2_max_header_size
-syn keyword ngxDirective contained http2_max_requests
 syn keyword ngxDirective contained http2_pool_size
 syn keyword ngxDirective contained http2_push
 syn keyword ngxDirective contained http2_push_preload
 syn keyword ngxDirective contained http2_recv_buffer_size
-syn keyword ngxDirective contained http2_recv_timeout
 syn keyword ngxDirective contained http2_streams_index_size
+syn keyword ngxDirective contained http3
+syn keyword ngxDirective contained http3_hq
+syn keyword ngxDirective contained http3_max_concurrent_streams
+syn keyword ngxDirective contained http3_stream_buffer_size
 syn keyword ngxDirective contained if_modified_since
 syn keyword ngxDirective contained ignore_invalid_headers
 syn keyword ngxDirective contained image_filter
@@ -339,30 +312,32 @@ syn keyword ngxDirective contained ip_hash
 syn keyword ngxDirective contained js_access
 syn keyword ngxDirective contained js_body_filter
 syn keyword ngxDirective contained js_content
+syn keyword ngxDirective contained js_fetch_buffer_size
 syn keyword ngxDirective contained js_fetch_ciphers
+syn keyword ngxDirective contained js_fetch_max_response_buffer_size
 syn keyword ngxDirective contained js_fetch_protocols
+syn keyword ngxDirective contained js_fetch_timeout
 syn keyword ngxDirective contained js_fetch_trusted_certificate
+syn keyword ngxDirective contained js_fetch_verify
 syn keyword ngxDirective contained js_fetch_verify_depth
 syn keyword ngxDirective contained js_filter
 syn keyword ngxDirective contained js_header_filter
 syn keyword ngxDirective contained js_import
-syn keyword ngxDirective contained js_include
 syn keyword ngxDirective contained js_path
+syn keyword ngxDirective contained js_preload_object
 syn keyword ngxDirective contained js_preread
 syn keyword ngxDirective contained js_set
+syn keyword ngxDirective contained js_shared_dict_zone
 syn keyword ngxDirective contained js_var
 syn keyword ngxDirective contained keepalive
 syn keyword ngxDirective contained keepalive_disable
 syn keyword ngxDirective contained keepalive_requests
 syn keyword ngxDirective contained keepalive_time
 syn keyword ngxDirective contained keepalive_timeout
-syn keyword ngxDirective contained keyval
-syn keyword ngxDirective contained keyval_zone
 syn keyword ngxDirective contained kqueue_changes
 syn keyword ngxDirective contained kqueue_events
 syn keyword ngxDirective contained large_client_header_buffers
 syn keyword ngxDirective contained least_conn
-syn keyword ngxDirective contained least_time
 syn keyword ngxDirective contained limit_conn
 syn keyword ngxDirective contained limit_conn_dry_run
 syn keyword ngxDirective contained limit_conn_log_level
@@ -391,7 +366,6 @@ syn keyword ngxDirective contained max_ranges
 syn keyword ngxDirective contained memcached_bind
 syn keyword ngxDirective contained memcached_buffer_size
 syn keyword ngxDirective contained memcached_connect_timeout
-syn keyword ngxDirective contained memcached_force_ranges
 syn keyword ngxDirective contained memcached_gzip_flag
 syn keyword ngxDirective contained memcached_next_upstream
 syn keyword ngxDirective contained memcached_next_upstream_timeout
@@ -407,14 +381,11 @@ syn keyword ngxDirective contained modern_browser
 syn keyword ngxDirective contained modern_browser_value
 syn keyword ngxDirective contained mp4
 syn keyword ngxDirective contained mp4_buffer_size
-syn keyword ngxDirective contained mp4_limit_rate
-syn keyword ngxDirective contained mp4_limit_rate_after
 syn keyword ngxDirective contained mp4_max_buffer_size
 syn keyword ngxDirective contained mp4_start_key_frame
 syn keyword ngxDirective contained msie_padding
 syn keyword ngxDirective contained msie_refresh
 syn keyword ngxDirective contained multi_accept
-syn keyword ngxDirective contained ntlm
 syn keyword ngxDirective contained open_file_cache
 syn keyword ngxDirective contained open_file_cache_errors
 syn keyword ngxDirective contained open_file_cache_events
@@ -457,7 +428,6 @@ syn keyword ngxDirective contained proxy_cache_max_range_offset
 syn keyword ngxDirective contained proxy_cache_methods
 syn keyword ngxDirective contained proxy_cache_min_uses
 syn keyword ngxDirective contained proxy_cache_path
-syn keyword ngxDirective contained proxy_cache_purge
 syn keyword ngxDirective contained proxy_cache_revalidate
 syn keyword ngxDirective contained proxy_cache_use_stale
 syn keyword ngxDirective contained proxy_cache_valid
@@ -495,7 +465,6 @@ syn keyword ngxDirective contained proxy_requests
 syn keyword ngxDirective contained proxy_responses
 syn keyword ngxDirective contained proxy_send_lowat
 syn keyword ngxDirective contained proxy_send_timeout
-syn keyword ngxDirective contained proxy_session_drop
 syn keyword ngxDirective contained proxy_set_body
 syn keyword ngxDirective contained proxy_set_header
 syn keyword ngxDirective contained proxy_smtp_auth
@@ -520,7 +489,11 @@ syn keyword ngxDirective contained proxy_temp_file_write_size
 syn keyword ngxDirective contained proxy_temp_path
 syn keyword ngxDirective contained proxy_timeout
 syn keyword ngxDirective contained proxy_upload_rate
-syn keyword ngxDirective contained queue
+syn keyword ngxDirective contained quic_active_connection_id_limit
+syn keyword ngxDirective contained quic_bpf
+syn keyword ngxDirective contained quic_gso
+syn keyword ngxDirective contained quic_host_key
+syn keyword ngxDirective contained quic_retry
 syn keyword ngxDirective contained random
 syn keyword ngxDirective contained random_index
 syn keyword ngxDirective contained read_ahead
@@ -551,7 +524,6 @@ syn keyword ngxDirective contained scgi_cache_max_range_offset
 syn keyword ngxDirective contained scgi_cache_methods
 syn keyword ngxDirective contained scgi_cache_min_uses
 syn keyword ngxDirective contained scgi_cache_path
-syn keyword ngxDirective contained scgi_cache_purge
 syn keyword ngxDirective contained scgi_cache_revalidate
 syn keyword ngxDirective contained scgi_cache_use_stale
 syn keyword ngxDirective contained scgi_cache_valid
@@ -590,9 +562,6 @@ syn keyword ngxDirective contained server_name_in_redirect
 syn keyword ngxDirective contained server_names_hash_bucket_size
 syn keyword ngxDirective contained server_names_hash_max_size
 syn keyword ngxDirective contained server_tokens
-syn keyword ngxDirective contained session_log
-syn keyword ngxDirective contained session_log_format
-syn keyword ngxDirective contained session_log_zone
 syn keyword ngxDirective contained set_real_ip_from
 syn keyword ngxDirective contained slice
 syn keyword ngxDirective contained smtp_auth
@@ -640,12 +609,6 @@ syn keyword ngxDirective contained ssl_trusted_certificate
 syn keyword ngxDirective contained ssl_verify_client
 syn keyword ngxDirective contained ssl_verify_depth
 syn keyword ngxDirective contained starttls
-syn keyword ngxDirective contained state
-syn keyword ngxDirective contained status
-syn keyword ngxDirective contained status_format
-syn keyword ngxDirective contained status_zone
-syn keyword ngxDirective contained sticky
-syn keyword ngxDirective contained sticky_cookie_insert
 syn keyword ngxDirective contained stub_status
 syn keyword ngxDirective contained sub_filter
 syn keyword ngxDirective contained sub_filter_last_modified
@@ -688,7 +651,6 @@ syn keyword ngxDirective contained uwsgi_cache_max_range_offset
 syn keyword ngxDirective contained uwsgi_cache_methods
 syn keyword ngxDirective contained uwsgi_cache_min_uses
 syn keyword ngxDirective contained uwsgi_cache_path
-syn keyword ngxDirective contained uwsgi_cache_purge
 syn keyword ngxDirective contained uwsgi_cache_revalidate
 syn keyword ngxDirective contained uwsgi_cache_use_stale
 syn keyword ngxDirective contained uwsgi_cache_valid
@@ -752,6 +714,62 @@ syn keyword ngxDirective contained xslt_string_param
 syn keyword ngxDirective contained xslt_stylesheet
 syn keyword ngxDirective contained xslt_types
 syn keyword ngxDirective contained zone
+
+" nginx-plus commercial extensions directives
+
+syn keyword ngxDirectiveBlock contained match
+syn keyword ngxDirectiveBlock contained otel_exporter
+
+syn keyword ngxDirective contained api
+syn keyword ngxDirective contained auth_jwt
+syn keyword ngxDirective contained auth_jwt_claim_set
+syn keyword ngxDirective contained auth_jwt_header_set
+syn keyword ngxDirective contained auth_jwt_key_cache
+syn keyword ngxDirective contained auth_jwt_key_file
+syn keyword ngxDirective contained auth_jwt_key_request
+syn keyword ngxDirective contained auth_jwt_leeway
+syn keyword ngxDirective contained auth_jwt_require
+syn keyword ngxDirective contained auth_jwt_type
+syn keyword ngxDirective contained f4f
+syn keyword ngxDirective contained f4f_buffer_size
+syn keyword ngxDirective contained fastcgi_cache_purge
+syn keyword ngxDirective contained health_check
+syn keyword ngxDirective contained health_check_timeout
+syn keyword ngxDirective contained hls
+syn keyword ngxDirective contained hls_buffers
+syn keyword ngxDirective contained hls_forward_args
+syn keyword ngxDirective contained hls_fragment
+syn keyword ngxDirective contained hls_mp4_buffer_size
+syn keyword ngxDirective contained hls_mp4_max_buffer_size
+syn keyword ngxDirective contained internal_redirect
+syn keyword ngxDirective contained keyval
+syn keyword ngxDirective contained keyval_zone
+syn keyword ngxDirective contained least_time
+syn keyword ngxDirective contained mp4_limit_rate
+syn keyword ngxDirective contained mp4_limit_rate_after
+syn keyword ngxDirective contained mqtt
+syn keyword ngxDirective contained mqtt_preread
+syn keyword ngxDirective contained mqtt_rewrite_buffer_size
+syn keyword ngxDirective contained mqtt_set_connect
+syn keyword ngxDirective contained ntlm
+syn keyword ngxDirective contained otel_service_name
+syn keyword ngxDirective contained otel_span_attr
+syn keyword ngxDirective contained otel_span_name
+syn keyword ngxDirective contained otel_trace
+syn keyword ngxDirective contained otel_trace_context
+syn keyword ngxDirective contained proxy_cache_purge
+syn keyword ngxDirective contained proxy_session_drop
+syn keyword ngxDirective contained queue
+syn keyword ngxDirective contained scgi_cache_purge
+syn keyword ngxDirective contained session_log
+syn keyword ngxDirective contained session_log_format
+syn keyword ngxDirective contained session_log_zone
+syn keyword ngxDirective contained state
+syn keyword ngxDirective contained status
+syn keyword ngxDirective contained status_format
+syn keyword ngxDirective contained status_zone
+syn keyword ngxDirective contained sticky
+syn keyword ngxDirective contained uwsgi_cache_purge
 syn keyword ngxDirective contained zone_sync
 syn keyword ngxDirective contained zone_sync_buffers
 syn keyword ngxDirective contained zone_sync_connect_retry_interval
@@ -775,61 +793,12 @@ syn keyword ngxDirective contained zone_sync_ssl_verify_depth
 syn keyword ngxDirective contained zone_sync_timeout
 
 " 3rd party modules list taken from
-" https://github.com/freebsd/freebsd-ports/blob/master/www/nginx-devel/Makefile
-" -----------------------------------------------------------------------------
-
-" Accept Language
-" https://github.com/giom/nginx_accept_language_module
-syn keyword ngxDirectiveThirdParty contained set_from_accept_language
+" https://github.com/freebsd/freebsd-ports/blob/main/www/nginx-devel/Makefile.extmod
+" ----------------------------------------------------------------------------------
 
-" Digest Authentication
-" https://github.com/atomx/nginx-http-auth-digest
-syn keyword ngxDirectiveThirdParty contained auth_digest
-syn keyword ngxDirectiveThirdParty contained auth_digest_drop_time
-syn keyword ngxDirectiveThirdParty contained auth_digest_evasion_time
-syn keyword ngxDirectiveThirdParty contained auth_digest_expires
-syn keyword ngxDirectiveThirdParty contained auth_digest_maxtries
-syn keyword ngxDirectiveThirdParty contained auth_digest_replays
-syn keyword ngxDirectiveThirdParty contained auth_digest_shm_size
-syn keyword ngxDirectiveThirdParty contained auth_digest_timeout
-syn keyword ngxDirectiveThirdParty contained auth_digest_user_file
-
-" SPNEGO Authentication
-" https://github.com/stnoonan/spnego-http-auth-nginx-module
-syn keyword ngxDirectiveThirdParty contained auth_gss
-syn keyword ngxDirectiveThirdParty contained auth_gss_allow_basic_fallback
-syn keyword ngxDirectiveThirdParty contained auth_gss_authorized_principal
-syn keyword ngxDirectiveThirdParty contained auth_gss_authorized_principal_regex
-syn keyword ngxDirectiveThirdParty contained auth_gss_constrained_delegation
-syn keyword ngxDirectiveThirdParty contained auth_gss_delegate_credentials
-syn keyword ngxDirectiveThirdParty contained auth_gss_force_realm
-syn keyword ngxDirectiveThirdParty contained auth_gss_format_full
-syn keyword ngxDirectiveThirdParty contained auth_gss_keytab
-syn keyword ngxDirectiveThirdParty contained auth_gss_map_to_local
-syn keyword ngxDirectiveThirdParty contained auth_gss_realm
-syn keyword ngxDirectiveThirdParty contained auth_gss_service_ccache
-syn keyword ngxDirectiveThirdParty contained auth_gss_service_name
-
-" LDAP Authentication
-" https://github.com/kvspb/nginx-auth-ldap
-syn keyword ngxDirectiveThirdParty contained auth_ldap
-syn keyword ngxDirectiveThirdParty contained auth_ldap_cache_enabled
-syn keyword ngxDirectiveThirdParty contained auth_ldap_cache_expiration_time
-syn keyword ngxDirectiveThirdParty contained auth_ldap_cache_size
-syn keyword ngxDirectiveThirdParty contained auth_ldap_servers
-syn keyword ngxDirectiveThirdParty contained auth_ldap_servers_size
-syn keyword ngxDirectiveThirdParty contained ldap_server
-
-" PAM Authentication
-" https://github.com/sto/ngx_http_auth_pam_module
-syn keyword ngxDirectiveThirdParty contained auth_pam
-syn keyword ngxDirectiveThirdParty contained auth_pam_service_name
-syn keyword ngxDirectiveThirdParty contained auth_pam_set_pam_env
-
-" AJP protocol proxy
-" https://github.com/yaoweibin/nginx_ajp_module
-syn keyword ngxDirectiveThirdParty contained ajp_buffers
+" https://github.com/msva/nginx_ajp_module
 syn keyword ngxDirectiveThirdParty contained ajp_buffer_size
+syn keyword ngxDirectiveThirdParty contained ajp_buffers
 syn keyword ngxDirectiveThirdParty contained ajp_busy_buffers_size
 syn keyword ngxDirectiveThirdParty contained ajp_cache
 syn keyword ngxDirectiveThirdParty contained ajp_cache_key
@@ -850,11 +819,13 @@ syn keyword ngxDirectiveThirdParty contained ajp_keep_conn
 syn keyword ngxDirectiveThirdParty contained ajp_max_data_packet_size
 syn keyword ngxDirectiveThirdParty contained ajp_max_temp_file_size
 syn keyword ngxDirectiveThirdParty contained ajp_next_upstream
+syn keyword ngxDirectiveThirdParty contained ajp_param
 syn keyword ngxDirectiveThirdParty contained ajp_pass
 syn keyword ngxDirectiveThirdParty contained ajp_pass_header
 syn keyword ngxDirectiveThirdParty contained ajp_pass_request_body
 syn keyword ngxDirectiveThirdParty contained ajp_pass_request_headers
 syn keyword ngxDirectiveThirdParty contained ajp_read_timeout
+syn keyword ngxDirectiveThirdParty contained ajp_script_url
 syn keyword ngxDirectiveThirdParty contained ajp_secret
 syn keyword ngxDirectiveThirdParty contained ajp_send_lowat
 syn keyword ngxDirectiveThirdParty contained ajp_send_timeout
@@ -865,7 +836,12 @@ syn keyword ngxDirectiveThirdParty contained ajp_temp_path
 syn keyword ngxDirectiveThirdParty contained ajp_upstream_fail_timeout
 syn keyword ngxDirectiveThirdParty contained ajp_upstream_max_fails
 
-" AWS proxy
+" https://github.com/openresty/array-var-nginx-module
+syn keyword ngxDirectiveThirdParty contained array_join
+syn keyword ngxDirectiveThirdParty contained array_map
+syn keyword ngxDirectiveThirdParty contained array_map_op
+syn keyword ngxDirectiveThirdParty contained array_split
+
 " https://github.com/anomalizer/ngx_aws_auth
 syn keyword ngxDirectiveThirdParty contained aws_access_key
 syn keyword ngxDirectiveThirdParty contained aws_endpoint
@@ -874,65 +850,32 @@ syn keyword ngxDirectiveThirdParty contained aws_s3_bucket
 syn keyword ngxDirectiveThirdParty contained aws_sign
 syn keyword ngxDirectiveThirdParty contained aws_signing_key
 
-" embedding Clojure or Java or Groovy programs
-" https://github.com/nginx-clojure/nginx-clojure
-syn keyword ngxDirectiveThirdParty contained access_handler_code
-syn keyword ngxDirectiveThirdParty contained access_handler_name
-syn keyword ngxDirectiveThirdParty contained access_handler_property
-syn keyword ngxDirectiveThirdParty contained access_handler_type
-syn keyword ngxDirectiveThirdParty contained always_read_body
-syn keyword ngxDirectiveThirdParty contained auto_upgrade_ws
-syn keyword ngxDirectiveThirdParty contained body_filter_code
-syn keyword ngxDirectiveThirdParty contained body_filter_name
-syn keyword ngxDirectiveThirdParty contained body_filter_property
-syn keyword ngxDirectiveThirdParty contained body_filter_type
-syn keyword ngxDirectiveThirdParty contained content_handler_code
-syn keyword ngxDirectiveThirdParty contained content_handler_name
-syn keyword ngxDirectiveThirdParty contained content_handler_property
-syn keyword ngxDirectiveThirdParty contained content_handler_type
-syn keyword ngxDirectiveThirdParty contained handler_code
-syn keyword ngxDirectiveThirdParty contained handler_name
-syn keyword ngxDirectiveThirdParty contained handlers_lazy_init
-syn keyword ngxDirectiveThirdParty contained handler_type
-syn keyword ngxDirectiveThirdParty contained header_filter_code
-syn keyword ngxDirectiveThirdParty contained header_filter_name
-syn keyword ngxDirectiveThirdParty contained header_filter_property
-syn keyword ngxDirectiveThirdParty contained header_filter_type
-syn keyword ngxDirectiveThirdParty contained jvm_classpath
-syn keyword ngxDirectiveThirdParty contained jvm_classpath_check
-syn keyword ngxDirectiveThirdParty contained jvm_exit_handler_code
-syn keyword ngxDirectiveThirdParty contained jvm_exit_handler_name
-syn keyword ngxDirectiveThirdParty contained jvm_handler_type
-syn keyword ngxDirectiveThirdParty contained jvm_init_handler_code
-syn keyword ngxDirectiveThirdParty contained jvm_init_handler_name
-syn keyword ngxDirectiveThirdParty contained jvm_options
-syn keyword ngxDirectiveThirdParty contained jvm_path
-syn keyword ngxDirectiveThirdParty contained jvm_var
-syn keyword ngxDirectiveThirdParty contained jvm_workers
-syn keyword ngxDirectiveThirdParty contained log_handler_code
-syn keyword ngxDirectiveThirdParty contained log_handler_name
-syn keyword ngxDirectiveThirdParty contained log_handler_property
-syn keyword ngxDirectiveThirdParty contained log_handler_type
-syn keyword ngxDirectiveThirdParty contained max_balanced_tcp_connections
-syn keyword ngxDirectiveThirdParty contained rewrite_handler_code
-syn keyword ngxDirectiveThirdParty contained rewrite_handler_name
-syn keyword ngxDirectiveThirdParty contained rewrite_handler_property
-syn keyword ngxDirectiveThirdParty contained rewrite_handler_type
-syn keyword ngxDirectiveThirdParty contained shared_map
-syn keyword ngxDirectiveThirdParty contained write_page_size
-
-
-" Certificate Transparency
+" https://github.com/google/ngx_brotli
+syn keyword ngxDirectiveThirdParty contained brotli
+syn keyword ngxDirectiveThirdParty contained brotli_buffers
+syn keyword ngxDirectiveThirdParty contained brotli_comp_level
+syn keyword ngxDirectiveThirdParty contained brotli_min_length
+syn keyword ngxDirectiveThirdParty contained brotli_static
+syn keyword ngxDirectiveThirdParty contained brotli_types
+syn keyword ngxDirectiveThirdParty contained brotli_window
+
+" https://github.com/torden/ngx_cache_purge
+syn keyword ngxDirectiveThirdParty contained cache_purge_response_type
+
+" https://github.com/AirisX/nginx_cookie_flag_module
+syn keyword ngxDirectiveThirdParty contained set_cookie_flag
+
 " https://github.com/grahamedgecombe/nginx-ct
 syn keyword ngxDirectiveThirdParty contained ssl_ct
 syn keyword ngxDirectiveThirdParty contained ssl_ct_static_scts
 
-" ngx_echo
 " https://github.com/openresty/echo-nginx-module
+syn keyword ngxDirectiveThirdParty contained echo
 syn keyword ngxDirectiveThirdParty contained echo_abort_parent
 syn keyword ngxDirectiveThirdParty contained echo_after_body
 syn keyword ngxDirectiveThirdParty contained echo_before_body
 syn keyword ngxDirectiveThirdParty contained echo_blocking_sleep
+syn keyword ngxDirectiveThirdParty contained echo_duplicate
 syn keyword ngxDirectiveThirdParty contained echo_end
 syn keyword ngxDirectiveThirdParty contained echo_exec
 syn keyword ngxDirectiveThirdParty contained echo_flush
@@ -942,28 +885,102 @@ syn keyword ngxDirectiveThirdParty contained echo_location_async
 syn keyword ngxDirectiveThirdParty contained echo_read_request_body
 syn keyword ngxDirectiveThirdParty contained echo_request_body
 syn keyword ngxDirectiveThirdParty contained echo_reset_timer
+syn keyword ngxDirectiveThirdParty contained echo_sleep
 syn keyword ngxDirectiveThirdParty contained echo_status
 syn keyword ngxDirectiveThirdParty contained echo_subrequest
 syn keyword ngxDirectiveThirdParty contained echo_subrequest_async
 
-" FastDFS
-" https://github.com/happyfish100/fastdfs-nginx-module
-syn keyword ngxDirectiveThirdParty contained ngx_fastdfs_module
+" https://github.com/openresty/drizzle-nginx-module
+syn keyword ngxDirectiveThirdParty contained drizzle_buffer_size
+syn keyword ngxDirectiveThirdParty contained drizzle_connect_timeout
+syn keyword ngxDirectiveThirdParty contained drizzle_dbname
+syn keyword ngxDirectiveThirdParty contained drizzle_keepalive
+syn keyword ngxDirectiveThirdParty contained drizzle_module_header
+syn keyword ngxDirectiveThirdParty contained drizzle_pass
+syn keyword ngxDirectiveThirdParty contained drizzle_query
+syn keyword ngxDirectiveThirdParty contained drizzle_recv_cols_timeout
+syn keyword ngxDirectiveThirdParty contained drizzle_recv_rows_timeout
+syn keyword ngxDirectiveThirdParty contained drizzle_send_query_timeout
+syn keyword ngxDirectiveThirdParty contained drizzle_server
+syn keyword ngxDirectiveThirdParty contained drizzle_status
+
+" https://github.com/ZigzagAK/ngx_dynamic_upstream
+syn keyword ngxDirectiveThirdParty contained dns_add_down
+syn keyword ngxDirectiveThirdParty contained dns_ipv6
+syn keyword ngxDirectiveThirdParty contained dns_update
+syn keyword ngxDirectiveThirdParty contained dynamic_state_file
+syn keyword ngxDirectiveThirdParty contained dynamic_upstream
+
+" https://github.com/openresty/encrypted-session-nginx-module
+syn keyword ngxDirectiveThirdParty contained encrypted_session_expires
+syn keyword ngxDirectiveThirdParty contained encrypted_session_iv
+syn keyword ngxDirectiveThirdParty contained encrypted_session_key
+syn keyword ngxDirectiveThirdParty contained set_decrypt_session
+syn keyword ngxDirectiveThirdParty contained set_encrypt_session
+
+" https://github.com/calio/form-input-nginx-module
+syn keyword ngxDirectiveThirdParty contained set_form_input
+syn keyword ngxDirectiveThirdParty contained set_form_input_multi
+
+" https://github.com/nieoding/nginx-gridfs
+syn keyword ngxDirectiveThirdParty contained gridfs
+syn keyword ngxDirectiveThirdParty contained mongo
 
-" ngx_headers_more
 " https://github.com/openresty/headers-more-nginx-module
 syn keyword ngxDirectiveThirdParty contained more_clear_headers
 syn keyword ngxDirectiveThirdParty contained more_clear_input_headers
 syn keyword ngxDirectiveThirdParty contained more_set_headers
 syn keyword ngxDirectiveThirdParty contained more_set_input_headers
 
-" NGINX WebDAV missing commands support (PROPFIND & OPTIONS)
+" https://github.com/dvershinin/nginx_accept_language_module
+syn keyword ngxDirectiveThirdParty contained set_from_accept_language
+
+" https://github.com/atomx/nginx-http-auth-digest
+syn keyword ngxDirectiveThirdParty contained auth_digest
+syn keyword ngxDirectiveThirdParty contained auth_digest_drop_time
+syn keyword ngxDirectiveThirdParty contained auth_digest_evasion_time
+syn keyword ngxDirectiveThirdParty contained auth_digest_expires
+syn keyword ngxDirectiveThirdParty contained auth_digest_maxtries
+syn keyword ngxDirectiveThirdParty contained auth_digest_replays
+syn keyword ngxDirectiveThirdParty contained auth_digest_shm_size
+syn keyword ngxDirectiveThirdParty contained auth_digest_timeout
+syn keyword ngxDirectiveThirdParty contained auth_digest_user_file
+
+" https://github.com/stnoonan/spnego-http-auth-nginx-module
+syn keyword ngxDirectiveThirdParty contained auth_gss
+syn keyword ngxDirectiveThirdParty contained auth_gss_allow_basic_fallback
+syn keyword ngxDirectiveThirdParty contained auth_gss_authorized_principal
+syn keyword ngxDirectiveThirdParty contained auth_gss_authorized_principal_regex
+syn keyword ngxDirectiveThirdParty contained auth_gss_constrained_delegation
+syn keyword ngxDirectiveThirdParty contained auth_gss_delegate_credentials
+syn keyword ngxDirectiveThirdParty contained auth_gss_force_realm
+syn keyword ngxDirectiveThirdParty contained auth_gss_format_full
+syn keyword ngxDirectiveThirdParty contained auth_gss_keytab
+syn keyword ngxDirectiveThirdParty contained auth_gss_map_to_local
+syn keyword ngxDirectiveThirdParty contained auth_gss_realm
+syn keyword ngxDirectiveThirdParty contained auth_gss_service_ccache
+syn keyword ngxDirectiveThirdParty contained auth_gss_service_name
+syn keyword ngxDirectiveThirdParty contained auth_gss_zone_name
+
+" https://github.com/kvspb/nginx-auth-ldap
+syn keyword ngxDirectiveThirdParty contained auth_ldap
+syn keyword ngxDirectiveThirdParty contained auth_ldap_cache_enabled
+syn keyword ngxDirectiveThirdParty contained auth_ldap_cache_expiration_time
+syn keyword ngxDirectiveThirdParty contained auth_ldap_cache_size
+syn keyword ngxDirectiveThirdParty contained auth_ldap_servers
+syn keyword ngxDirectiveThirdParty contained auth_ldap_servers_size
+syn keyword ngxDirectiveThirdParty contained ldap_server
+
+" https://github.com/sto/ngx_http_auth_pam_module
+syn keyword ngxDirectiveThirdParty contained auth_pam
+syn keyword ngxDirectiveThirdParty contained auth_pam_service_name
+syn keyword ngxDirectiveThirdParty contained auth_pam_set_pam_env
+
 " https://github.com/arut/nginx-dav-ext-module
 syn keyword ngxDirectiveThirdParty contained dav_ext_lock
 syn keyword ngxDirectiveThirdParty contained dav_ext_lock_zone
 syn keyword ngxDirectiveThirdParty contained dav_ext_methods
 
-" ngx_eval
 " https://github.com/openresty/nginx-eval-module
 syn keyword ngxDirectiveThirdParty contained eval
 syn keyword ngxDirectiveThirdParty contained eval_buffer_size
@@ -971,9 +988,9 @@ syn keyword ngxDirectiveThirdParty contained eval_escalate
 syn keyword ngxDirectiveThirdParty contained eval_override_content_type
 syn keyword ngxDirectiveThirdParty contained eval_subrequest_in_memory
 
-" Fancy Index
 " https://github.com/aperezdc/ngx-fancyindex
 syn keyword ngxDirectiveThirdParty contained fancyindex
+syn keyword ngxDirectiveThirdParty contained fancyindex_case_sensitive
 syn keyword ngxDirectiveThirdParty contained fancyindex_css_href
 syn keyword ngxDirectiveThirdParty contained fancyindex_default_sort
 syn keyword ngxDirectiveThirdParty contained fancyindex_directories_first
@@ -988,40 +1005,29 @@ syn keyword ngxDirectiveThirdParty contained fancyindex_show_dotfiles
 syn keyword ngxDirectiveThirdParty contained fancyindex_show_path
 syn keyword ngxDirectiveThirdParty contained fancyindex_time_format
 
-" Footer filter
 " https://github.com/alibaba/nginx-http-footer-filter
 syn keyword ngxDirectiveThirdParty contained footer
 syn keyword ngxDirectiveThirdParty contained footer_types
 
-" ngx_http_geoip2_module
 " https://github.com/leev/ngx_http_geoip2_module
 syn keyword ngxDirectiveThirdParty contained geoip2
 syn keyword ngxDirectiveThirdParty contained geoip2_proxy
 syn keyword ngxDirectiveThirdParty contained geoip2_proxy_recursive
 
-" A version of the Nginx HTTP stub status module that outputs in JSON format
-" https://github.com/nginx-modules/nginx-json-status-module
-syn keyword ngxDirectiveThirdParty contained json_status
-syn keyword ngxDirectiveThirdParty contained json_status_type
-
-" MogileFS client for nginx
-" https://github.com/vkholodkov/nginx-mogilefs-module
-syn keyword ngxDirectiveThirdParty contained mogilefs_class
-syn keyword ngxDirectiveThirdParty contained mogilefs_connect_timeout
-syn keyword ngxDirectiveThirdParty contained mogilefs_domain
-syn keyword ngxDirectiveThirdParty contained mogilefs_methods
-syn keyword ngxDirectiveThirdParty contained mogilefs_noverify
-syn keyword ngxDirectiveThirdParty contained mogilefs_pass
-syn keyword ngxDirectiveThirdParty contained mogilefs_read_timeout
-syn keyword ngxDirectiveThirdParty contained mogilefs_send_timeout
-syn keyword ngxDirectiveThirdParty contained mogilefs_tracker
-
-" Ancient nginx plugin; probably not useful to anyone
+" https://github.com/ip2location/ip2location-nginx
+syn keyword ngxDirectiveThirdParty contained ip2location_database
+syn keyword ngxDirectiveThirdParty contained ip2location_proxy
+syn keyword ngxDirectiveThirdParty contained ip2location_proxy_recursive
+
+" https://github.com/ip2location/ip2proxy-nginx
+syn keyword ngxDirectiveThirdParty contained ip2proxy_database
+syn keyword ngxDirectiveThirdParty contained ip2proxy_proxy
+syn keyword ngxDirectiveThirdParty contained ip2proxy_proxy_recursive
+
 " https://github.com/kr/nginx-notice
 syn keyword ngxDirectiveThirdParty contained notice
 syn keyword ngxDirectiveThirdParty contained notice_type
 
-" nchan
 " https://github.com/slact/nchan
 syn keyword ngxDirectiveThirdParty contained nchan_access_control_allow_credentials
 syn keyword ngxDirectiveThirdParty contained nchan_access_control_allow_origin
@@ -1034,8 +1040,8 @@ syn keyword ngxDirectiveThirdParty contained nchan_benchmark_publisher_distribut
 syn keyword ngxDirectiveThirdParty contained nchan_benchmark_subscriber_distribution
 syn keyword ngxDirectiveThirdParty contained nchan_benchmark_subscribers_per_channel
 syn keyword ngxDirectiveThirdParty contained nchan_benchmark_time
-syn keyword ngxDirectiveThirdParty contained nchan_channel_events_channel_id
 syn keyword ngxDirectiveThirdParty contained nchan_channel_event_string
+syn keyword ngxDirectiveThirdParty contained nchan_channel_events_channel_id
 syn keyword ngxDirectiveThirdParty contained nchan_channel_group
 syn keyword ngxDirectiveThirdParty contained nchan_channel_group_accounting
 syn keyword ngxDirectiveThirdParty contained nchan_channel_id
@@ -1073,12 +1079,32 @@ syn keyword ngxDirectiveThirdParty contained nchan_publisher_upstream_request
 syn keyword ngxDirectiveThirdParty contained nchan_pubsub
 syn keyword ngxDirectiveThirdParty contained nchan_pubsub_channel_id
 syn keyword ngxDirectiveThirdParty contained nchan_pubsub_location
+syn keyword ngxDirectiveThirdParty contained nchan_redis_accurate_subscriber_count
 syn keyword ngxDirectiveThirdParty contained nchan_redis_cluster_check_interval
+syn keyword ngxDirectiveThirdParty contained nchan_redis_cluster_check_interval_backoff
+syn keyword ngxDirectiveThirdParty contained nchan_redis_cluster_check_interval_jitter
+syn keyword ngxDirectiveThirdParty contained nchan_redis_cluster_check_interval_max
+syn keyword ngxDirectiveThirdParty contained nchan_redis_cluster_check_interval_min
+syn keyword ngxDirectiveThirdParty contained nchan_redis_cluster_connect_timeout
+syn keyword ngxDirectiveThirdParty contained nchan_redis_cluster_max_failing_time
+syn keyword ngxDirectiveThirdParty contained nchan_redis_cluster_recovery_delay
+syn keyword ngxDirectiveThirdParty contained nchan_redis_cluster_recovery_delay_backoff
+syn keyword ngxDirectiveThirdParty contained nchan_redis_cluster_recovery_delay_jitter
+syn keyword ngxDirectiveThirdParty contained nchan_redis_cluster_recovery_delay_max
+syn keyword ngxDirectiveThirdParty contained nchan_redis_cluster_recovery_delay_min
+syn keyword ngxDirectiveThirdParty contained nchan_redis_command_timeout
 syn keyword ngxDirectiveThirdParty contained nchan_redis_connect_timeout
 syn keyword ngxDirectiveThirdParty contained nchan_redis_discovered_ip_range_blacklist
 syn keyword ngxDirectiveThirdParty contained nchan_redis_fakesub_timer_interval
 syn keyword ngxDirectiveThirdParty contained nchan_redis_idle_channel_cache_timeout
+syn keyword ngxDirectiveThirdParty contained nchan_redis_idle_channel_keepalive_backoff
+syn keyword ngxDirectiveThirdParty contained nchan_redis_idle_channel_keepalive_jitter
+syn keyword ngxDirectiveThirdParty contained nchan_redis_idle_channel_keepalive_max
+syn keyword ngxDirectiveThirdParty contained nchan_redis_idle_channel_keepalive_min
+syn keyword ngxDirectiveThirdParty contained nchan_redis_idle_channel_keepalive_safety_margin
+syn keyword ngxDirectiveThirdParty contained nchan_redis_load_scripts_unconditionally
 syn keyword ngxDirectiveThirdParty contained nchan_redis_namespace
+syn keyword ngxDirectiveThirdParty contained nchan_redis_node_connect_timeout
 syn keyword ngxDirectiveThirdParty contained nchan_redis_nostore_fastpublish
 syn keyword ngxDirectiveThirdParty contained nchan_redis_optimize_target
 syn keyword ngxDirectiveThirdParty contained nchan_redis_pass
@@ -1086,6 +1112,13 @@ syn keyword ngxDirectiveThirdParty contained nchan_redis_pass_inheritable
 syn keyword ngxDirectiveThirdParty contained nchan_redis_password
 syn keyword ngxDirectiveThirdParty contained nchan_redis_ping_interval
 syn keyword ngxDirectiveThirdParty contained nchan_redis_publish_msgpacked_max_size
+syn keyword ngxDirectiveThirdParty contained nchan_redis_reconnect_delay
+syn keyword ngxDirectiveThirdParty contained nchan_redis_reconnect_delay_backoff
+syn keyword ngxDirectiveThirdParty contained nchan_redis_reconnect_delay_jitter
+syn keyword ngxDirectiveThirdParty contained nchan_redis_reconnect_delay_max
+syn keyword ngxDirectiveThirdParty contained nchan_redis_reconnect_delay_min
+syn keyword ngxDirectiveThirdParty contained nchan_redis_retry_commands
+syn keyword ngxDirectiveThirdParty contained nchan_redis_retry_commands_max_wait
 syn keyword ngxDirectiveThirdParty contained nchan_redis_server
 syn keyword ngxDirectiveThirdParty contained nchan_redis_ssl
 syn keyword ngxDirectiveThirdParty contained nchan_redis_ssl_ciphers
@@ -1104,6 +1137,9 @@ syn keyword ngxDirectiveThirdParty contained nchan_redis_tls_server_name
 syn keyword ngxDirectiveThirdParty contained nchan_redis_tls_trusted_certificate
 syn keyword ngxDirectiveThirdParty contained nchan_redis_tls_trusted_certificate_path
 syn keyword ngxDirectiveThirdParty contained nchan_redis_tls_verify_certificate
+syn keyword ngxDirectiveThirdParty contained nchan_redis_upstream_stats
+syn keyword ngxDirectiveThirdParty contained nchan_redis_upstream_stats_disconnected_timeout
+syn keyword ngxDirectiveThirdParty contained nchan_redis_upstream_stats_enabled
 syn keyword ngxDirectiveThirdParty contained nchan_redis_url
 syn keyword ngxDirectiveThirdParty contained nchan_redis_username
 syn keyword ngxDirectiveThirdParty contained nchan_redis_wait_after_connecting
@@ -1113,10 +1149,10 @@ syn keyword ngxDirectiveThirdParty contained nchan_store_messages
 syn keyword ngxDirectiveThirdParty contained nchan_stub_status
 syn keyword ngxDirectiveThirdParty contained nchan_sub_channel_id
 syn keyword ngxDirectiveThirdParty contained nchan_subscribe_existing_channels_only
+syn keyword ngxDirectiveThirdParty contained nchan_subscribe_request
 syn keyword ngxDirectiveThirdParty contained nchan_subscriber
 syn keyword ngxDirectiveThirdParty contained nchan_subscriber_channel_id
 syn keyword ngxDirectiveThirdParty contained nchan_subscriber_compound_etag_message_id
-syn keyword ngxDirectiveThirdParty contained nchan_subscribe_request
 syn keyword ngxDirectiveThirdParty contained nchan_subscriber_first_message
 syn keyword ngxDirectiveThirdParty contained nchan_subscriber_http_raw_stream_separator
 syn keyword ngxDirectiveThirdParty contained nchan_subscriber_info
@@ -1145,7 +1181,6 @@ syn keyword ngxDirectiveThirdParty contained push_subscriber
 syn keyword ngxDirectiveThirdParty contained push_subscriber_concurrency
 syn keyword ngxDirectiveThirdParty contained push_subscriber_timeout
 
-" Push Stream
 " https://github.com/wandenberg/nginx-push-stream-module
 syn keyword ngxDirectiveThirdParty contained push_stream_allow_connections_to_events_channel
 syn keyword ngxDirectiveThirdParty contained push_stream_allowed_origins
@@ -1184,23 +1219,6 @@ syn keyword ngxDirectiveThirdParty contained push_stream_websocket_allow_publish
 syn keyword ngxDirectiveThirdParty contained push_stream_wildcard_channel_max_qtd
 syn keyword ngxDirectiveThirdParty contained push_stream_wildcard_channel_prefix
 
-" redis module
-" https://www.nginx.com/resources/wiki/modules/redis/
-syn keyword ngxDirectiveThirdParty contained redis_bind
-syn keyword ngxDirectiveThirdParty contained redis_buffer_size
-syn keyword ngxDirectiveThirdParty contained redis_connect_timeout
-syn keyword ngxDirectiveThirdParty contained redis_gzip_flag
-syn keyword ngxDirectiveThirdParty contained redis_next_upstream
-syn keyword ngxDirectiveThirdParty contained redis_pass
-syn keyword ngxDirectiveThirdParty contained redis_read_timeout
-syn keyword ngxDirectiveThirdParty contained redis_send_timeout
-
-" ngx_http_response
-" http://catap.ru/downloads/nginx/
-syn keyword ngxDirectiveThirdParty contained response
-syn keyword ngxDirectiveThirdParty contained response_type
-
-" nginx_substitutions_filter
 " https://github.com/yaoweibin/ngx_http_substitutions_filter_module
 syn keyword ngxDirectiveThirdParty contained subs_buffers
 syn keyword ngxDirectiveThirdParty contained subs_filter
@@ -1208,7 +1226,6 @@ syn keyword ngxDirectiveThirdParty contained subs_filter_bypass
 syn keyword ngxDirectiveThirdParty contained subs_filter_types
 syn keyword ngxDirectiveThirdParty contained subs_line_buffer_size
 
-" Tarantool nginx upstream module
 " https://github.com/tarantool/nginx_upstream_module
 syn keyword ngxDirectiveThirdParty contained tnt_allowed_indexes
 syn keyword ngxDirectiveThirdParty contained tnt_allowed_spaces
@@ -1238,44 +1255,28 @@ syn keyword ngxDirectiveThirdParty contained tnt_set_header
 syn keyword ngxDirectiveThirdParty contained tnt_update
 syn keyword ngxDirectiveThirdParty contained tnt_upsert
 
-" A module for nginx web server for handling file uploads using multipart/form-data encoding (RFC 1867)
-" https://github.com/Austinb/nginx-upload-module
+" https://github.com/fdintino/nginx-upload-module
+syn keyword ngxDirectiveThirdParty contained upload_add_header
 syn keyword ngxDirectiveThirdParty contained upload_aggregate_form_field
-syn keyword ngxDirectiveThirdParty contained upload_archive_elm
-syn keyword ngxDirectiveThirdParty contained upload_archive_elm_separator
-syn keyword ngxDirectiveThirdParty contained upload_archive_path
-syn keyword ngxDirectiveThirdParty contained upload_archive_path_separator
 syn keyword ngxDirectiveThirdParty contained upload_buffer_size
 syn keyword ngxDirectiveThirdParty contained upload_cleanup
-syn keyword ngxDirectiveThirdParty contained upload_content_type
-syn keyword ngxDirectiveThirdParty contained upload_discard
-syn keyword ngxDirectiveThirdParty contained upload_field_name
-syn keyword ngxDirectiveThirdParty contained upload_file_crc32
-syn keyword ngxDirectiveThirdParty contained upload_file_md5
-syn keyword ngxDirectiveThirdParty contained upload_file_md5_uc
-syn keyword ngxDirectiveThirdParty contained upload_file_name
-syn keyword ngxDirectiveThirdParty contained upload_file_sha1
-syn keyword ngxDirectiveThirdParty contained upload_file_sha1_uc
-syn keyword ngxDirectiveThirdParty contained upload_file_size
-syn keyword ngxDirectiveThirdParty contained upload_filter
+syn keyword ngxDirectiveThirdParty contained upload_empty_fiels_names
+syn keyword ngxDirectiveThirdParty contained upload_limit_rate
 syn keyword ngxDirectiveThirdParty contained upload_max_file_size
 syn keyword ngxDirectiveThirdParty contained upload_max_output_body_len
 syn keyword ngxDirectiveThirdParty contained upload_max_part_header_len
+syn keyword ngxDirectiveThirdParty contained upload_merge_buffer_size
 syn keyword ngxDirectiveThirdParty contained upload_pass
 syn keyword ngxDirectiveThirdParty contained upload_pass_args
 syn keyword ngxDirectiveThirdParty contained upload_pass_form_field
+syn keyword ngxDirectiveThirdParty contained upload_range_header_buffer_size
+syn keyword ngxDirectiveThirdParty contained upload_resumable
 syn keyword ngxDirectiveThirdParty contained upload_set_form_field
+syn keyword ngxDirectiveThirdParty contained upload_state_store
 syn keyword ngxDirectiveThirdParty contained upload_store
 syn keyword ngxDirectiveThirdParty contained upload_store_access
-syn keyword ngxDirectiveThirdParty contained upload_tmp_path
-syn keyword ngxDirectiveThirdParty contained upload_unzip
-syn keyword ngxDirectiveThirdParty contained upload_unzip_buffers
-syn keyword ngxDirectiveThirdParty contained upload_unzip_hash
-syn keyword ngxDirectiveThirdParty contained upload_unzip_max_file_name_len
-syn keyword ngxDirectiveThirdParty contained upload_unzip_window
-syn keyword ngxDirectiveThirdParty contained upload_void_content_type
-
-" nginx-upload-progress-module
+syn keyword ngxDirectiveThirdParty contained upload_tame_arrays
+
 " https://github.com/masterzen/nginx-upload-progress-module
 syn keyword ngxDirectiveThirdParty contained report_uploads
 syn keyword ngxDirectiveThirdParty contained track_uploads
@@ -1288,7 +1289,6 @@ syn keyword ngxDirectiveThirdParty contained upload_progress_jsonp_output
 syn keyword ngxDirectiveThirdParty contained upload_progress_jsonp_parameter
 syn keyword ngxDirectiveThirdParty contained upload_progress_template
 
-" Health checks upstreams for nginx
 " https://github.com/yaoweibin/nginx_upstream_check_module
 syn keyword ngxDirectiveThirdParty contained check
 syn keyword ngxDirectiveThirdParty contained check_fastcgi_param
@@ -1298,13 +1298,15 @@ syn keyword ngxDirectiveThirdParty contained check_keepalive_requests
 syn keyword ngxDirectiveThirdParty contained check_shm_size
 syn keyword ngxDirectiveThirdParty contained check_status
 
-" The fair load balancer module for nginx
-" https://github.com/cryptofuture/nginx-upstream-fair
+" https://github.com/jaygooby/nginx-upstream-fair
 syn keyword ngxDirectiveThirdParty contained fair
 syn keyword ngxDirectiveThirdParty contained upstream_fair_shm_size
 
-" Nginx Video Thumb Extractor Module
-" https://github.com/wandenberg/nginx-video-thumbextractor-module
+" https://github.com/ayty-adrianomartins/nginx-sticky-module-ng
+syn keyword ngxDirectiveThirdParty contained sticky_hide_cookie
+syn keyword ngxDirectiveThirdParty contained sticky_no_fallback
+
+" https://github.com/Novetta/nginx-video-thumbextractor-module
 syn keyword ngxDirectiveThirdParty contained video_thumbextractor
 syn keyword ngxDirectiveThirdParty contained video_thumbextractor_image_height
 syn keyword ngxDirectiveThirdParty contained video_thumbextractor_image_width
@@ -1329,43 +1331,14 @@ syn keyword ngxDirectiveThirdParty contained video_thumbextractor_tile_sample_in
 syn keyword ngxDirectiveThirdParty contained video_thumbextractor_video_filename
 syn keyword ngxDirectiveThirdParty contained video_thumbextractor_video_second
 
-" drizzle-nginx-module - Upstream module for talking to MySQL and Drizzle directly
-" https://github.com/openresty/drizzle-nginx-module
-syn keyword ngxDirectiveThirdParty contained drizzle_buffer_size
-syn keyword ngxDirectiveThirdParty contained drizzle_connect_timeout
-syn keyword ngxDirectiveThirdParty contained drizzle_dbname
-syn keyword ngxDirectiveThirdParty contained drizzle_keepalive
-syn keyword ngxDirectiveThirdParty contained drizzle_module_header
-syn keyword ngxDirectiveThirdParty contained drizzle_pass
-syn keyword ngxDirectiveThirdParty contained drizzle_query
-syn keyword ngxDirectiveThirdParty contained drizzle_recv_cols_timeout
-syn keyword ngxDirectiveThirdParty contained drizzle_recv_rows_timeout
-syn keyword ngxDirectiveThirdParty contained drizzle_send_query_timeout
-syn keyword ngxDirectiveThirdParty contained drizzle_server
-syn keyword ngxDirectiveThirdParty contained drizzle_status
-
-" ngx_dynamic_upstream
-" https://github.com/cubicdaiya/ngx_dynamic_upstream
-syn keyword ngxDirectiveThirdParty contained dynamic_upstream
-
-" encrypt and decrypt nginx variable values
-" https://github.com/openresty/encrypted-session-nginx-module
-syn keyword ngxDirectiveThirdParty contained encrypted_session_expires
-syn keyword ngxDirectiveThirdParty contained encrypted_session_iv
-syn keyword ngxDirectiveThirdParty contained encrypted_session_key
-syn keyword ngxDirectiveThirdParty contained set_decrypt_session
-syn keyword ngxDirectiveThirdParty contained set_encrypt_session
-
-" serve content directly from MongoDB's GridFS
-" https://github.com/mdirolf/nginx-gridfs
-syn keyword ngxDirectiveThirdParty contained gridfs
-syn keyword ngxDirectiveThirdParty contained mongo
+" https://github.com/calio/iconv-nginx-module
+syn keyword ngxDirectiveThirdParty contained iconv_buffer_size
+syn keyword ngxDirectiveThirdParty contained iconv_filter
+syn keyword ngxDirectiveThirdParty contained set_iconv
 
-" Adds support for arithmetic operations to NGINX config
-" https://github.com/arut/nginx-let-module
+" https://github.com/baysao/nginx-let-module
 syn keyword ngxDirectiveThirdParty contained let
 
-" ngx_http_lua_module - Embed the power of Lua into Nginx HTTP Servers
 " https://github.com/openresty/lua-nginx-module
 syn keyword ngxDirectiveThirdParty contained access_by_lua
 syn keyword ngxDirectiveThirdParty contained access_by_lua_block
@@ -1417,6 +1390,8 @@ syn keyword ngxDirectiveThirdParty contained lua_socket_pool_size
 syn keyword ngxDirectiveThirdParty contained lua_socket_read_timeout
 syn keyword ngxDirectiveThirdParty contained lua_socket_send_lowat
 syn keyword ngxDirectiveThirdParty contained lua_socket_send_timeout
+syn keyword ngxDirectiveThirdParty contained lua_ssl_certificate
+syn keyword ngxDirectiveThirdParty contained lua_ssl_certificate_key
 syn keyword ngxDirectiveThirdParty contained lua_ssl_ciphers
 syn keyword ngxDirectiveThirdParty contained lua_ssl_conf_command
 syn keyword ngxDirectiveThirdParty contained lua_ssl_crl
@@ -1431,6 +1406,8 @@ syn keyword ngxDirectiveThirdParty contained rewrite_by_lua
 syn keyword ngxDirectiveThirdParty contained rewrite_by_lua_block
 syn keyword ngxDirectiveThirdParty contained rewrite_by_lua_file
 syn keyword ngxDirectiveThirdParty contained rewrite_by_lua_no_postpone
+syn keyword ngxDirectiveThirdParty contained server_rewrite_by_lua_block
+syn keyword ngxDirectiveThirdParty contained server_rewrite_by_lua_file
 syn keyword ngxDirectiveThirdParty contained set_by_lua
 syn keyword ngxDirectiveThirdParty contained set_by_lua_block
 syn keyword ngxDirectiveThirdParty contained set_by_lua_file
@@ -1443,7 +1420,16 @@ syn keyword ngxDirectiveThirdParty contained ssl_session_fetch_by_lua_file
 syn keyword ngxDirectiveThirdParty contained ssl_session_store_by_lua_block
 syn keyword ngxDirectiveThirdParty contained ssl_session_store_by_lua_file
 
-" ngx_memc - An extended version of the standard memcached module
+" https://github.com/Taymindis/nginx-link-function
+syn keyword ngxDirectiveThirdParty contained ngx_link_func_add_prop
+syn keyword ngxDirectiveThirdParty contained ngx_link_func_add_req_header
+syn keyword ngxDirectiveThirdParty contained ngx_link_func_ca_cert
+syn keyword ngxDirectiveThirdParty contained ngx_link_func_call
+syn keyword ngxDirectiveThirdParty contained ngx_link_func_download_link_lib
+syn keyword ngxDirectiveThirdParty contained ngx_link_func_lib
+syn keyword ngxDirectiveThirdParty contained ngx_link_func_shm_size
+syn keyword ngxDirectiveThirdParty contained ngx_link_func_subrequest
+
 " https://github.com/openresty/memc-nginx-module
 syn keyword ngxDirectiveThirdParty contained memc_buffer_size
 syn keyword ngxDirectiveThirdParty contained memc_cmds_allowed
@@ -1457,21 +1443,24 @@ syn keyword ngxDirectiveThirdParty contained memc_send_timeout
 syn keyword ngxDirectiveThirdParty contained memc_upstream_fail_timeout
 syn keyword ngxDirectiveThirdParty contained memc_upstream_max_fails
 
-" ModSecurity web application firewall
-" https://github.com/SpiderLabs/ModSecurity/tree/master
-syn keyword ngxDirectiveThirdParty contained ModSecurityConfig
-syn keyword ngxDirectiveThirdParty contained ModSecurityEnabled
-syn keyword ngxDirectiveThirdParty contained pool_context_hash_size
+" https://github.com/SpiderLabs/ModSecurity-nginx
+syn keyword ngxDirectiveThirdParty contained modsecurity
+syn keyword ngxDirectiveThirdParty contained modsecurity_rules
+syn keyword ngxDirectiveThirdParty contained modsecurity_rules_file
+syn keyword ngxDirectiveThirdParty contained modsecurity_rules_remote
+syn keyword ngxDirectiveThirdParty contained modsecurity_transaction_id
 
-" NAXSI is an open-source, high performance, low rules maintenance WAF for NGINX
 " https://github.com/nbs-system/naxsi
 syn keyword ngxDirectiveThirdParty contained BasicRule
 syn keyword ngxDirectiveThirdParty contained CheckRule
 syn keyword ngxDirectiveThirdParty contained DeniedUrl
+syn keyword ngxDirectiveThirdParty contained IgnoreCIDR
+syn keyword ngxDirectiveThirdParty contained IgnoreIP
 syn keyword ngxDirectiveThirdParty contained LearningMode
 syn keyword ngxDirectiveThirdParty contained LibInjectionSql
 syn keyword ngxDirectiveThirdParty contained LibInjectionXss
 syn keyword ngxDirectiveThirdParty contained MainRule
+syn keyword ngxDirectiveThirdParty contained NaxsiLogFile
 syn keyword ngxDirectiveThirdParty contained SecRulesDisabled
 syn keyword ngxDirectiveThirdParty contained SecRulesEnabled
 syn keyword ngxDirectiveThirdParty contained basic_rule
@@ -1481,17 +1470,31 @@ syn keyword ngxDirectiveThirdParty contained learning_mode
 syn keyword ngxDirectiveThirdParty contained libinjection_sql
 syn keyword ngxDirectiveThirdParty contained libinjection_xss
 syn keyword ngxDirectiveThirdParty contained main_rule
+syn keyword ngxDirectiveThirdParty contained naxsi_log
 syn keyword ngxDirectiveThirdParty contained rules_disabled
 syn keyword ngxDirectiveThirdParty contained rules_enabled
 
-" Phusion Passenger
-" https://www.phusionpassenger.com/library/config/nginx/reference/
+" https://github.com/opentracing-contrib/nginx-opentracing
+syn keyword ngxDirectiveThirdParty contained opentracing
+syn keyword ngxDirectiveThirdParty contained opentracing_fastcgi_propagate_context
+syn keyword ngxDirectiveThirdParty contained opentracing_grpc_propagate_context
+syn keyword ngxDirectiveThirdParty contained opentracing_load_tracer
+syn keyword ngxDirectiveThirdParty contained opentracing_location_operation_name
+syn keyword ngxDirectiveThirdParty contained opentracing_operation_name
+syn keyword ngxDirectiveThirdParty contained opentracing_propagate_context
+syn keyword ngxDirectiveThirdParty contained opentracing_tag
+syn keyword ngxDirectiveThirdParty contained opentracing_trace_locations
+syn keyword ngxDirectiveThirdParty contained opentracing_trust_incoming_span
+
+" https://github.com/phusion/passenger
 syn keyword ngxDirectiveThirdParty contained passenger_abort_on_startup_error
 syn keyword ngxDirectiveThirdParty contained passenger_abort_websockets_on_process_shutdown
 syn keyword ngxDirectiveThirdParty contained passenger_admin_panel_auth_type
 syn keyword ngxDirectiveThirdParty contained passenger_admin_panel_password
 syn keyword ngxDirectiveThirdParty contained passenger_admin_panel_url
 syn keyword ngxDirectiveThirdParty contained passenger_admin_panel_username
+syn keyword ngxDirectiveThirdParty contained passenger_analytics_log_group
+syn keyword ngxDirectiveThirdParty contained passenger_analytics_log_user
 syn keyword ngxDirectiveThirdParty contained passenger_anonymous_telemetry_proxy
 syn keyword ngxDirectiveThirdParty contained passenger_app_env
 syn keyword ngxDirectiveThirdParty contained passenger_app_file_descriptor_ulimit
@@ -1499,20 +1502,25 @@ syn keyword ngxDirectiveThirdParty contained passenger_app_group_name
 syn keyword ngxDirectiveThirdParty contained passenger_app_log_file
 syn keyword ngxDirectiveThirdParty contained passenger_app_rights
 syn keyword ngxDirectiveThirdParty contained passenger_app_root
+syn keyword ngxDirectiveThirdParty contained passenger_app_start_command
 syn keyword ngxDirectiveThirdParty contained passenger_app_type
 syn keyword ngxDirectiveThirdParty contained passenger_base_uri
 syn keyword ngxDirectiveThirdParty contained passenger_buffer_response
 syn keyword ngxDirectiveThirdParty contained passenger_buffer_size
+syn keyword ngxDirectiveThirdParty contained passenger_buffer_upload
 syn keyword ngxDirectiveThirdParty contained passenger_buffers
 syn keyword ngxDirectiveThirdParty contained passenger_busy_buffers_size
 syn keyword ngxDirectiveThirdParty contained passenger_concurrency_model
 syn keyword ngxDirectiveThirdParty contained passenger_core_file_descriptor_ulimit
 syn keyword ngxDirectiveThirdParty contained passenger_ctl
 syn keyword ngxDirectiveThirdParty contained passenger_data_buffer_dir
+syn keyword ngxDirectiveThirdParty contained passenger_debug_log_file
 syn keyword ngxDirectiveThirdParty contained passenger_debugger
 syn keyword ngxDirectiveThirdParty contained passenger_default_group
 syn keyword ngxDirectiveThirdParty contained passenger_default_user
+syn keyword ngxDirectiveThirdParty contained passenger_direct_instance_request_address
 syn keyword ngxDirectiveThirdParty contained passenger_disable_anonymous_telemetry
+syn keyword ngxDirectiveThirdParty contained passenger_disable_log_prefix
 syn keyword ngxDirectiveThirdParty contained passenger_disable_security_update_check
 syn keyword ngxDirectiveThirdParty contained passenger_document_root
 syn keyword ngxDirectiveThirdParty contained passenger_dump_config_manifest
@@ -1548,8 +1556,10 @@ syn keyword ngxDirectiveThirdParty contained passenger_nodejs
 syn keyword ngxDirectiveThirdParty contained passenger_pass_header
 syn keyword ngxDirectiveThirdParty contained passenger_pool_idle_time
 syn keyword ngxDirectiveThirdParty contained passenger_pre_start
+syn keyword ngxDirectiveThirdParty contained passenger_preload_bundler
 syn keyword ngxDirectiveThirdParty contained passenger_python
 syn keyword ngxDirectiveThirdParty contained passenger_read_timeout
+syn keyword ngxDirectiveThirdParty contained passenger_request_buffering
 syn keyword ngxDirectiveThirdParty contained passenger_request_queue_overflow_status_code
 syn keyword ngxDirectiveThirdParty contained passenger_resist_deployment_errors
 syn keyword ngxDirectiveThirdParty contained passenger_response_buffer_high_watermark
@@ -1561,36 +1571,36 @@ syn keyword ngxDirectiveThirdParty contained passenger_security_update_check_pro
 syn keyword ngxDirectiveThirdParty contained passenger_set_header
 syn keyword ngxDirectiveThirdParty contained passenger_show_version_in_header
 syn keyword ngxDirectiveThirdParty contained passenger_socket_backlog
+syn keyword ngxDirectiveThirdParty contained passenger_spawn_dir
+syn keyword ngxDirectiveThirdParty contained passenger_spawn_exception_status_code
 syn keyword ngxDirectiveThirdParty contained passenger_spawn_method
 syn keyword ngxDirectiveThirdParty contained passenger_start_timeout
 syn keyword ngxDirectiveThirdParty contained passenger_startup_file
 syn keyword ngxDirectiveThirdParty contained passenger_stat_throttle_rate
 syn keyword ngxDirectiveThirdParty contained passenger_sticky_sessions
+syn keyword ngxDirectiveThirdParty contained passenger_sticky_sessions_cookie_attributes
 syn keyword ngxDirectiveThirdParty contained passenger_sticky_sessions_cookie_name
+syn keyword ngxDirectiveThirdParty contained passenger_temp_path
 syn keyword ngxDirectiveThirdParty contained passenger_thread_count
 syn keyword ngxDirectiveThirdParty contained passenger_turbocaching
+syn keyword ngxDirectiveThirdParty contained passenger_use_global_queue
 syn keyword ngxDirectiveThirdParty contained passenger_user
 syn keyword ngxDirectiveThirdParty contained passenger_user_switching
 syn keyword ngxDirectiveThirdParty contained passenger_vary_turbocache_by_cookie
-syn keyword ngxDirectiveThirdPartyDeprecated contained passenger_analytics_log_group
-syn keyword ngxDirectiveThirdPartyDeprecated contained passenger_analytics_log_user
-syn keyword ngxDirectiveThirdPartyDeprecated contained passenger_debug_log_file
-syn keyword ngxDirectiveThirdPartyDeprecated contained passenger_use_global_queue
-syn keyword ngxDirectiveThirdPartyDeprecated contained rack_env
-syn keyword ngxDirectiveThirdPartyDeprecated contained rails_app_spawner_idle_time
-syn keyword ngxDirectiveThirdPartyDeprecated contained rails_env
-syn keyword ngxDirectiveThirdPartyDeprecated contained rails_framework_spawner_idle_time
-syn keyword ngxDirectiveThirdPartyDeprecated contained rails_spawn_method
-syn keyword ngxDirectiveThirdPartyDeprecated contained union_station_filter
-syn keyword ngxDirectiveThirdPartyDeprecated contained union_station_gateway_address
-syn keyword ngxDirectiveThirdPartyDeprecated contained union_station_gateway_cert
-syn keyword ngxDirectiveThirdPartyDeprecated contained union_station_gateway_port
-syn keyword ngxDirectiveThirdPartyDeprecated contained union_station_key
-syn keyword ngxDirectiveThirdPartyDeprecated contained union_station_proxy_address
-syn keyword ngxDirectiveThirdPartyDeprecated contained union_station_support
-
-" ngx_postgres is an upstream module that allows nginx to communicate directly with PostgreSQL database
-" https://github.com/FRiCKLE/ngx_postgres
+syn keyword ngxDirectiveThirdParty contained rack_env
+syn keyword ngxDirectiveThirdParty contained rails_app_spawner_idle_time
+syn keyword ngxDirectiveThirdParty contained rails_env
+syn keyword ngxDirectiveThirdParty contained rails_framework_spawner_idle_time
+syn keyword ngxDirectiveThirdParty contained rails_spawn_method
+syn keyword ngxDirectiveThirdParty contained union_station_filter
+syn keyword ngxDirectiveThirdParty contained union_station_gateway_address
+syn keyword ngxDirectiveThirdParty contained union_station_gateway_cert
+syn keyword ngxDirectiveThirdParty contained union_station_gateway_port
+syn keyword ngxDirectiveThirdParty contained union_station_key
+syn keyword ngxDirectiveThirdParty contained union_station_proxy_address
+syn keyword ngxDirectiveThirdParty contained union_station_support
+
+" https://github.com/konstruxi/ngx_postgres
 syn keyword ngxDirectiveThirdParty contained postgres_connect_timeout
 syn keyword ngxDirectiveThirdParty contained postgres_escape
 syn keyword ngxDirectiveThirdParty contained postgres_keepalive
@@ -1602,7 +1612,6 @@ syn keyword ngxDirectiveThirdParty contained postgres_rewrite
 syn keyword ngxDirectiveThirdParty contained postgres_server
 syn keyword ngxDirectiveThirdParty contained postgres_set
 
-" ngx_rds_csv - Nginx output filter module to convert Resty-DBD-Streams (RDS) to Comma-Separated Values (CSV)
 " https://github.com/openresty/rds-csv-nginx-module
 syn keyword ngxDirectiveThirdParty contained rds_csv
 syn keyword ngxDirectiveThirdParty contained rds_csv_buffer_size
@@ -1611,7 +1620,6 @@ syn keyword ngxDirectiveThirdParty contained rds_csv_field_name_header
 syn keyword ngxDirectiveThirdParty contained rds_csv_field_separator
 syn keyword ngxDirectiveThirdParty contained rds_csv_row_terminator
 
-" ngx_rds_json - an output filter that formats Resty DBD Streams generated by ngx_drizzle and others to JSON
 " https://github.com/openresty/rds-json-nginx-module
 syn keyword ngxDirectiveThirdParty contained rds_json
 syn keyword ngxDirectiveThirdParty contained rds_json_buffer_size
@@ -1624,7 +1632,6 @@ syn keyword ngxDirectiveThirdParty contained rds_json_root
 syn keyword ngxDirectiveThirdParty contained rds_json_success_property
 syn keyword ngxDirectiveThirdParty contained rds_json_user_property
 
-" ngx_redis2 - Nginx upstream module for the Redis 2.0 protocol
 " https://github.com/openresty/redis2-nginx-module
 syn keyword ngxDirectiveThirdParty contained redis2_bind
 syn keyword ngxDirectiveThirdParty contained redis2_buffer_size
@@ -1638,7 +1645,6 @@ syn keyword ngxDirectiveThirdParty contained redis2_raw_query
 syn keyword ngxDirectiveThirdParty contained redis2_read_timeout
 syn keyword ngxDirectiveThirdParty contained redis2_send_timeout
 
-" NGINX-based Media Streaming Server
 " https://github.com/arut/nginx-rtmp-module
 syn keyword ngxDirectiveThirdParty contained ack_window
 syn keyword ngxDirectiveThirdParty contained application
@@ -1750,7 +1756,6 @@ syn keyword ngxDirectiveThirdParty contained sync
 syn keyword ngxDirectiveThirdParty contained wait_key
 syn keyword ngxDirectiveThirdParty contained wait_video
 
-" ngx_set_misc - Various set_xxx directives added to nginx's rewrite module (md5/sha1, sql/json quoting, and many more)
 " https://github.com/openresty/set-misc-nginx-module
 syn keyword ngxDirectiveThirdParty contained set_base32_alphabet
 syn keyword ngxDirectiveThirdParty contained set_base32_padding
@@ -1770,6 +1775,7 @@ syn keyword ngxDirectiveThirdParty contained set_hmac_sha1
 syn keyword ngxDirectiveThirdParty contained set_hmac_sha256
 syn keyword ngxDirectiveThirdParty contained set_if_empty
 syn keyword ngxDirectiveThirdParty contained set_local_today
+syn keyword ngxDirectiveThirdParty contained set_md5
 syn keyword ngxDirectiveThirdParty contained set_misc_base32_padding
 syn keyword ngxDirectiveThirdParty contained set_quote_json_str
 syn keyword ngxDirectiveThirdParty contained set_quote_pgsql_str
@@ -1778,20 +1784,18 @@ syn keyword ngxDirectiveThirdParty contained set_random
 syn keyword ngxDirectiveThirdParty contained set_rotate
 syn keyword ngxDirectiveThirdParty contained set_secure_random_alphanum
 syn keyword ngxDirectiveThirdParty contained set_secure_random_lcalpha
+syn keyword ngxDirectiveThirdParty contained set_sha1
 syn keyword ngxDirectiveThirdParty contained set_unescape_uri
 
-" nginx-sflow-module
 " https://github.com/sflow/nginx-sflow-module
 syn keyword ngxDirectiveThirdParty contained sflow
 
-" Shibboleth auth request module for Nginx
 " https://github.com/nginx-shib/nginx-http-shibboleth
 syn keyword ngxDirectiveThirdParty contained shib_request
 syn keyword ngxDirectiveThirdParty contained shib_request_set
 syn keyword ngxDirectiveThirdParty contained shib_request_use_headers
 
-" nginx module which adds ability to cache static files
-" https://github.com/FRiCKLE/ngx_slowfs_cache
+" https://github.com/baysao/ngx_slowfs_cache
 syn keyword ngxDirectiveThirdParty contained slowfs_big_file_size
 syn keyword ngxDirectiveThirdParty contained slowfs_cache
 syn keyword ngxDirectiveThirdParty contained slowfs_cache_key
@@ -1801,18 +1805,6 @@ syn keyword ngxDirectiveThirdParty contained slowfs_cache_purge
 syn keyword ngxDirectiveThirdParty contained slowfs_cache_valid
 syn keyword ngxDirectiveThirdParty contained slowfs_temp_path
 
-" Dynamic Image Transformation Module For nginx
-" https://github.com/cubicdaiya/ngx_small_light
-syn keyword ngxDirectiveThirdParty contained small_light
-syn keyword ngxDirectiveThirdParty contained small_light_buffer
-syn keyword ngxDirectiveThirdParty contained small_light_getparam_mode
-syn keyword ngxDirectiveThirdParty contained small_light_imlib2_temp_dir
-syn keyword ngxDirectiveThirdParty contained small_light_material_dir
-syn keyword ngxDirectiveThirdParty contained small_light_pattern_define
-syn keyword ngxDirectiveThirdParty contained small_light_radius_max
-syn keyword ngxDirectiveThirdParty contained small_light_sigma_max
-
-" ngx_srcache - Transparent subrequest-based caching layout for arbitrary nginx locations
 " https://github.com/openresty/srcache-nginx-module
 syn keyword ngxDirectiveThirdParty contained srcache_buffer
 syn keyword ngxDirectiveThirdParty contained srcache_default_expire
@@ -1835,7 +1827,6 @@ syn keyword ngxDirectiveThirdParty contained srcache_store_ranges
 syn keyword ngxDirectiveThirdParty contained srcache_store_skip
 syn keyword ngxDirectiveThirdParty contained srcache_store_statuses
 
-" NGINX-based VOD Packager
 " https://github.com/kaltura/nginx-vod-module
 syn keyword ngxDirectiveThirdParty contained vod
 syn keyword ngxDirectiveThirdParty contained vod_align_segments_to_key_frames
@@ -1875,6 +1866,7 @@ syn keyword ngxDirectiveThirdParty contained vod_live_window_duration
 syn keyword ngxDirectiveThirdParty contained vod_manifest_duration_policy
 syn keyword ngxDirectiveThirdParty contained vod_manifest_segment_durations_mode
 syn keyword ngxDirectiveThirdParty contained vod_mapping_cache
+syn keyword ngxDirectiveThirdParty contained vod_max_frame_count
 syn keyword ngxDirectiveThirdParty contained vod_max_frames_size
 syn keyword ngxDirectiveThirdParty contained vod_max_mapping_response_size
 syn keyword ngxDirectiveThirdParty contained vod_max_metadata_size
@@ -1901,6 +1893,7 @@ syn keyword ngxDirectiveThirdParty contained vod_response_cache
 syn keyword ngxDirectiveThirdParty contained vod_secret_key
 syn keyword ngxDirectiveThirdParty contained vod_segment_count_policy
 syn keyword ngxDirectiveThirdParty contained vod_segment_duration
+syn keyword ngxDirectiveThirdParty contained vod_segment_max_frame_count
 syn keyword ngxDirectiveThirdParty contained vod_segments_base_url
 syn keyword ngxDirectiveThirdParty contained vod_source_clip_map_uri
 syn keyword ngxDirectiveThirdParty contained vod_speed_param_name
@@ -1910,7 +1903,6 @@ syn keyword ngxDirectiveThirdParty contained vod_tracks_param_name
 syn keyword ngxDirectiveThirdParty contained vod_upstream_extra_args
 syn keyword ngxDirectiveThirdParty contained vod_upstream_location
 
-" Nginx virtual host traffic status module
 " https://github.com/vozlt/nginx-module-vts
 syn keyword ngxDirectiveThirdParty contained vhost_traffic_status
 syn keyword ngxDirectiveThirdParty contained vhost_traffic_status_average_method
@@ -1934,7 +1926,6 @@ syn keyword ngxDirectiveThirdParty contained vhost_traffic_status_limit_traffic_
 syn keyword ngxDirectiveThirdParty contained vhost_traffic_status_set_by_filter
 syn keyword ngxDirectiveThirdParty contained vhost_traffic_status_zone
 
-" xss-nginx-module - Native cross-site scripting support in nginx
 " https://github.com/openresty/xss-nginx-module
 syn keyword ngxDirectiveThirdParty contained xss_callback_arg
 syn keyword ngxDirectiveThirdParty contained xss_check_status
@@ -1943,471 +1934,6 @@ syn keyword ngxDirectiveThirdParty contained xss_input_types
 syn keyword ngxDirectiveThirdParty contained xss_output_type
 syn keyword ngxDirectiveThirdParty contained xss_override_status
 
-" Add support for array-typed variables to nginx config files
-" https://github.com/openresty/array-var-nginx-module
-syn keyword ngxDirectiveThirdParty contained array_join
-syn keyword ngxDirectiveThirdParty contained array_map
-syn keyword ngxDirectiveThirdParty contained array_map_op
-syn keyword ngxDirectiveThirdParty contained array_split
-
-" NGINX module for Brotli compression
-" https://github.com/eustas/ngx_brotli
-syn keyword ngxDirectiveThirdParty contained brotli
-syn keyword ngxDirectiveThirdParty contained brotli_buffers
-syn keyword ngxDirectiveThirdParty contained brotli_comp_level
-syn keyword ngxDirectiveThirdParty contained brotli_min_length
-syn keyword ngxDirectiveThirdParty contained brotli_static
-syn keyword ngxDirectiveThirdParty contained brotli_types
-syn keyword ngxDirectiveThirdParty contained brotli_window
-
-" form-input-nginx-module
-" https://github.com/calio/form-input-nginx-module
-syn keyword ngxDirectiveThirdParty contained set_form_input
-syn keyword ngxDirectiveThirdParty contained set_form_input_multi
-
-" character conversion nginx module using libiconv
-" https://github.com/calio/iconv-nginx-module
-syn keyword ngxDirectiveThirdParty contained iconv_buffer_size
-syn keyword ngxDirectiveThirdParty contained iconv_filter
-syn keyword ngxDirectiveThirdParty contained set_iconv
-
-" 3rd party modules list taken from
-" https://www.nginx.com/resources/wiki/modules/
-" ---------------------------------------------
-
-" Nginx Module for Authenticating Akamai G2O requests
-" https://github.com/kaltura/nginx_mod_akamai_g2o
-syn keyword ngxDirectiveThirdParty contained g2o
-syn keyword ngxDirectiveThirdParty contained g2o_data_header
-syn keyword ngxDirectiveThirdParty contained g2o_hash_function
-syn keyword ngxDirectiveThirdParty contained g2o_key
-syn keyword ngxDirectiveThirdParty contained g2o_log_level
-syn keyword ngxDirectiveThirdParty contained g2o_nonce
-syn keyword ngxDirectiveThirdParty contained g2o_sign_header
-syn keyword ngxDirectiveThirdParty contained g2o_time_window
-syn keyword ngxDirectiveThirdParty contained g2o_version
-
-" nginx_lua_module
-" https://github.com/alacner/nginx_lua_module
-syn keyword ngxDirectiveThirdParty contained lua_file
-
-" Nginx Audio Track for HTTP Live Streaming
-" https://github.com/flavioribeiro/nginx-audio-track-for-hls-module
-syn keyword ngxDirectiveThirdParty contained ngx_hls_audio_track
-syn keyword ngxDirectiveThirdParty contained ngx_hls_audio_track_output_format
-syn keyword ngxDirectiveThirdParty contained ngx_hls_audio_track_output_header
-syn keyword ngxDirectiveThirdParty contained ngx_hls_audio_track_rootpath
-
-" A Nginx module to dump backtrace when a worker process exits abnormally
-" https://github.com/alibaba/nginx-backtrace
-syn keyword ngxDirectiveThirdParty contained backtrace_log
-syn keyword ngxDirectiveThirdParty contained backtrace_max_stack_size
-
-" circle_gif module
-" https://github.com/evanmiller/nginx_circle_gif
-syn keyword ngxDirectiveThirdParty contained circle_gif
-syn keyword ngxDirectiveThirdParty contained circle_gif_max_radius
-syn keyword ngxDirectiveThirdParty contained circle_gif_min_radius
-syn keyword ngxDirectiveThirdParty contained circle_gif_step_radius
-
-" Upstream Consistent Hash
-" https://github.com/replay/ngx_http_consistent_hash
-syn keyword ngxDirectiveThirdParty contained consistent_hash
-
-" Nginx module for etags on dynamic content
-" https://github.com/kali/nginx-dynamic-etags
-syn keyword ngxDirectiveThirdParty contained dynamic_etags
-
-" Enhanced Nginx Memcached Module
-" https://github.com/bpaquet/ngx_http_enhanced_memcached_module
-syn keyword ngxDirectiveThirdParty contained enhanced_memcached_allow_delete
-syn keyword ngxDirectiveThirdParty contained enhanced_memcached_allow_put
-syn keyword ngxDirectiveThirdParty contained enhanced_memcached_bind
-syn keyword ngxDirectiveThirdParty contained enhanced_memcached_buffer_size
-syn keyword ngxDirectiveThirdParty contained enhanced_memcached_connect_timeout
-syn keyword ngxDirectiveThirdParty contained enhanced_memcached_flush
-syn keyword ngxDirectiveThirdParty contained enhanced_memcached_flush_namespace
-syn keyword ngxDirectiveThirdParty contained enhanced_memcached_hash_keys_with_md5
-syn keyword ngxDirectiveThirdParty contained enhanced_memcached_pass
-syn keyword ngxDirectiveThirdParty contained enhanced_memcached_read_timeout
-syn keyword ngxDirectiveThirdParty contained enhanced_memcached_send_timeout
-syn keyword ngxDirectiveThirdParty contained enhanced_memcached_stats
-
-" nginx max connections queue
-" https://github.com/ezmobius/nginx-ey-balancer
-syn keyword ngxDirectiveThirdParty contained max_connections_max_queue_length
-syn keyword ngxDirectiveThirdParty contained max_connections_queue_timeout
-
-" Nginx module for POST authentication and authorization
-" https://github.com/veruu/ngx_form_auth
-syn keyword ngxDirectiveThirdParty contained form_auth
-syn keyword ngxDirectiveThirdParty contained form_auth_login
-syn keyword ngxDirectiveThirdParty contained form_auth_pam_service
-syn keyword ngxDirectiveThirdParty contained form_auth_password
-syn keyword ngxDirectiveThirdParty contained form_auth_remote_user
-
-" ngx_http_accounting_module
-" https://github.com/Lax/ngx_http_accounting_module
-syn keyword ngxDirectiveThirdParty contained accounting
-syn keyword ngxDirectiveThirdParty contained accounting_id
-syn keyword ngxDirectiveThirdParty contained accounting_interval
-syn keyword ngxDirectiveThirdParty contained accounting_log
-syn keyword ngxDirectiveThirdParty contained accounting_perturb
-
-" concatenating files in a given context: CSS and JS files usually
-" https://github.com/alibaba/nginx-http-concat
-syn keyword ngxDirectiveThirdParty contained concat
-syn keyword ngxDirectiveThirdParty contained concat_delimiter
-syn keyword ngxDirectiveThirdParty contained concat_ignore_file_error
-syn keyword ngxDirectiveThirdParty contained concat_max_files
-syn keyword ngxDirectiveThirdParty contained concat_types
-syn keyword ngxDirectiveThirdParty contained concat_unique
-
-" update upstreams' config by restful interface
-" https://github.com/yzprofile/ngx_http_dyups_module
-syn keyword ngxDirectiveThirdParty contained dyups_interface
-syn keyword ngxDirectiveThirdParty contained dyups_shm_zone_size
-
-" add given content to the end of the response according to the condition specified
-" https://github.com/flygoast/ngx_http_footer_if_filter
-syn keyword ngxDirectiveThirdParty contained footer_if
-
-" NGINX HTTP Internal Redirect Module
-" https://github.com/flygoast/ngx_http_internal_redirect
-syn keyword ngxDirectiveThirdParty contained internal_redirect_if
-syn keyword ngxDirectiveThirdParty contained internal_redirect_if_no_postpone
-
-" nginx-ip-blocker
-" https://github.com/tmthrgd/nginx-ip-blocker
-syn keyword ngxDirectiveThirdParty contained ip_blocker
-
-" IP2Location Nginx
-" https://github.com/chrislim2888/ip2location-nginx
-syn keyword ngxDirectiveThirdParty contained ip2location_database
-
-" Limit upload rate
-" https://github.com/cfsego/limit_upload_rate
-syn keyword ngxDirectiveThirdParty contained limit_upload_rate
-syn keyword ngxDirectiveThirdParty contained limit_upload_rate_after
-syn keyword ngxDirectiveThirdParty contained limit_upload_rate_log_level
-
-" limit the number of connections to upstream
-" https://github.com/cfsego/nginx-limit-upstream
-syn keyword ngxDirectiveThirdParty contained limit_upstream_conn
-syn keyword ngxDirectiveThirdParty contained limit_upstream_log_level
-syn keyword ngxDirectiveThirdParty contained limit_upstream_zone
-
-" conditional accesslog for nginx
-" https://github.com/cfsego/ngx_log_if
-syn keyword ngxDirectiveThirdParty contained access_log_bypass_if
-
-" log messages over ZeroMQ
-" https://github.com/alticelabs/nginx-log-zmq
-syn keyword ngxDirectiveThirdParty contained log_zmq_endpoint
-syn keyword ngxDirectiveThirdParty contained log_zmq_format
-syn keyword ngxDirectiveThirdParty contained log_zmq_off
-syn keyword ngxDirectiveThirdParty contained log_zmq_server
-
-" simple module to uppercase/lowercase strings in the nginx config
-" https://github.com/replay/ngx_http_lower_upper_case
-syn keyword ngxDirectiveThirdParty contained lower
-syn keyword ngxDirectiveThirdParty contained upper
-
-" content filter for nginx, which returns the md5 hash of the content otherwise returned
-" https://github.com/kainswor/nginx_md5_filter
-syn keyword ngxDirectiveThirdParty contained md5_filter
-
-" Non-blocking upstream module for Nginx to connect to MongoDB
-" https://github.com/simpl/ngx_mongo
-syn keyword ngxDirectiveThirdParty contained mongo_auth
-syn keyword ngxDirectiveThirdParty contained mongo_bind
-syn keyword ngxDirectiveThirdParty contained mongo_buffer_size
-syn keyword ngxDirectiveThirdParty contained mongo_buffering
-syn keyword ngxDirectiveThirdParty contained mongo_buffers
-syn keyword ngxDirectiveThirdParty contained mongo_busy_buffers_size
-syn keyword ngxDirectiveThirdParty contained mongo_connect_timeout
-syn keyword ngxDirectiveThirdParty contained mongo_json
-syn keyword ngxDirectiveThirdParty contained mongo_next_upstream
-syn keyword ngxDirectiveThirdParty contained mongo_pass
-syn keyword ngxDirectiveThirdParty contained mongo_query
-syn keyword ngxDirectiveThirdParty contained mongo_read_timeout
-syn keyword ngxDirectiveThirdParty contained mongo_send_timeout
-
-" Nginx OCSP processing module designed for response caching
-" https://github.com/kyprizel/nginx_ocsp_proxy-module
-syn keyword ngxDirectiveThirdParty contained ocsp_cache_timeout
-syn keyword ngxDirectiveThirdParty contained ocsp_proxy
-
-" Nginx OpenSSL version check at startup
-" https://github.com/apcera/nginx-openssl-version
-syn keyword ngxDirectiveThirdParty contained openssl_builddate_minimum
-syn keyword ngxDirectiveThirdParty contained openssl_version_minimum
-
-" Automatic PageSpeed optimization module for Nginx
-" https://github.com/pagespeed/ngx_pagespeed
-syn keyword ngxDirectiveThirdParty contained pagespeed
-
-" PECL Memcache standard hashing compatible loadbalancer for Nginx
-" https://github.com/replay/ngx_http_php_memcache_standard_balancer
-syn keyword ngxDirectiveThirdParty contained hash_key
-
-" nginx module to parse php sessions
-" https://github.com/replay/ngx_http_php_session
-syn keyword ngxDirectiveThirdParty contained php_session_parse
-syn keyword ngxDirectiveThirdParty contained php_session_strip_formatting
-
-" Nginx HTTP rDNS module
-" https://github.com/flant/nginx-http-rdns
-syn keyword ngxDirectiveThirdParty contained rdns
-syn keyword ngxDirectiveThirdParty contained rdns_allow
-syn keyword ngxDirectiveThirdParty contained rdns_deny
-
-" Streaming regular expression replacement in response bodies
-" https://github.com/openresty/replace-filter-nginx-module
-syn keyword ngxDirectiveThirdParty contained replace_filter
-syn keyword ngxDirectiveThirdParty contained replace_filter_last_modified
-syn keyword ngxDirectiveThirdParty contained replace_filter_max_buffered_size
-syn keyword ngxDirectiveThirdParty contained replace_filter_skip
-syn keyword ngxDirectiveThirdParty contained replace_filter_types
-
-" Link RRDtool's graphing facilities directly into nginx
-" https://github.com/evanmiller/mod_rrd_graph
-syn keyword ngxDirectiveThirdParty contained rrd_graph
-syn keyword ngxDirectiveThirdParty contained rrd_graph_root
-
-" Module for nginx to proxy rtmp using http protocol
-" https://github.com/kwojtek/nginx-rtmpt-proxy-module
-syn keyword ngxDirectiveThirdParty contained rtmpt_proxy
-syn keyword ngxDirectiveThirdParty contained rtmpt_proxy_http_timeout
-syn keyword ngxDirectiveThirdParty contained rtmpt_proxy_rtmp_timeout
-syn keyword ngxDirectiveThirdParty contained rtmpt_proxy_stat
-syn keyword ngxDirectiveThirdParty contained rtmpt_proxy_stylesheet
-syn keyword ngxDirectiveThirdParty contained rtmpt_proxy_target
-
-" Syntactically Awesome NGINX Module
-" https://github.com/mneudert/sass-nginx-module
-syn keyword ngxDirectiveThirdParty contained sass_compile
-syn keyword ngxDirectiveThirdParty contained sass_error_log
-syn keyword ngxDirectiveThirdParty contained sass_include_path
-syn keyword ngxDirectiveThirdParty contained sass_indent
-syn keyword ngxDirectiveThirdParty contained sass_is_indented_syntax
-syn keyword ngxDirectiveThirdParty contained sass_linefeed
-syn keyword ngxDirectiveThirdParty contained sass_output_style
-syn keyword ngxDirectiveThirdParty contained sass_precision
-syn keyword ngxDirectiveThirdParty contained sass_source_comments
-syn keyword ngxDirectiveThirdParty contained sass_source_map_embed
-
-" Nginx Selective Cache Purge Module
-" https://github.com/wandenberg/nginx-selective-cache-purge-module
-syn keyword ngxDirectiveThirdParty contained selective_cache_purge_query
-syn keyword ngxDirectiveThirdParty contained selective_cache_purge_redis_database
-syn keyword ngxDirectiveThirdParty contained selective_cache_purge_redis_host
-syn keyword ngxDirectiveThirdParty contained selective_cache_purge_redis_password
-syn keyword ngxDirectiveThirdParty contained selective_cache_purge_redis_port
-syn keyword ngxDirectiveThirdParty contained selective_cache_purge_redis_unix_socket
-
-" cconv nginx module
-" https://github.com/liseen/set-cconv-nginx-module
-syn keyword ngxDirectiveThirdParty contained set_cconv_to_simp
-syn keyword ngxDirectiveThirdParty contained set_cconv_to_trad
-syn keyword ngxDirectiveThirdParty contained set_pinyin_to_normal
-
-" Nginx module that allows the setting of variables to the value of a variety of hashes
-" https://github.com/simpl/ngx_http_set_hash
-syn keyword ngxDirectiveThirdParty contained set_md5
-syn keyword ngxDirectiveThirdParty contained set_md5_upper
-syn keyword ngxDirectiveThirdParty contained set_murmur2
-syn keyword ngxDirectiveThirdParty contained set_murmur2_upper
-syn keyword ngxDirectiveThirdParty contained set_sha1
-syn keyword ngxDirectiveThirdParty contained set_sha1_upper
-
-" Nginx module to set the language of a request based on a number of options
-" https://github.com/simpl/ngx_http_set_lang
-syn keyword ngxDirectiveThirdParty contained lang_cookie
-syn keyword ngxDirectiveThirdParty contained lang_get_var
-syn keyword ngxDirectiveThirdParty contained lang_host
-syn keyword ngxDirectiveThirdParty contained lang_list
-syn keyword ngxDirectiveThirdParty contained lang_post_var
-syn keyword ngxDirectiveThirdParty contained lang_referer
-syn keyword ngxDirectiveThirdParty contained set_lang
-syn keyword ngxDirectiveThirdParty contained set_lang_method
-
-" Nginx Sorted Querystring Module
-" https://github.com/wandenberg/nginx-sorted-querystring-module
-syn keyword ngxDirectiveThirdParty contained sorted_querysting_filter_parameter
-
-" Nginx upstream module for Sphinx 2.x search daemon
-" https://github.com/reeteshranjan/sphinx2-nginx-module
-syn keyword ngxDirectiveThirdParty contained sphinx2_bind
-syn keyword ngxDirectiveThirdParty contained sphinx2_buffer_size
-syn keyword ngxDirectiveThirdParty contained sphinx2_connect_timeout
-syn keyword ngxDirectiveThirdParty contained sphinx2_next_upstream
-syn keyword ngxDirectiveThirdParty contained sphinx2_pass
-syn keyword ngxDirectiveThirdParty contained sphinx2_read_timeout
-syn keyword ngxDirectiveThirdParty contained sphinx2_send_timeout
-
-" Nginx module for retrieving user attributes and groups from SSSD
-" https://github.com/veruu/ngx_sssd_info
-syn keyword ngxDirectiveThirdParty contained sssd_info
-syn keyword ngxDirectiveThirdParty contained sssd_info_attribute
-syn keyword ngxDirectiveThirdParty contained sssd_info_attribute_separator
-syn keyword ngxDirectiveThirdParty contained sssd_info_attributes
-syn keyword ngxDirectiveThirdParty contained sssd_info_group
-syn keyword ngxDirectiveThirdParty contained sssd_info_group_separator
-syn keyword ngxDirectiveThirdParty contained sssd_info_groups
-syn keyword ngxDirectiveThirdParty contained sssd_info_output_to
-
-" An nginx module for sending statistics to statsd
-" https://github.com/zebrafishlabs/nginx-statsd
-syn keyword ngxDirectiveThirdParty contained statsd_count
-syn keyword ngxDirectiveThirdParty contained statsd_sample_rate
-syn keyword ngxDirectiveThirdParty contained statsd_server
-syn keyword ngxDirectiveThirdParty contained statsd_timing
-
-" ngx_stream_echo - TCP/stream echo module for NGINX (a port of the ngx_http_echo module)
-" https://github.com/openresty/stream-echo-nginx-module
-syn keyword ngxDirectiveThirdParty contained echo
-syn keyword ngxDirectiveThirdParty contained echo_client_error_log_level
-syn keyword ngxDirectiveThirdParty contained echo_discard_request
-syn keyword ngxDirectiveThirdParty contained echo_duplicate
-syn keyword ngxDirectiveThirdParty contained echo_flush_wait
-syn keyword ngxDirectiveThirdParty contained echo_lingering_close
-syn keyword ngxDirectiveThirdParty contained echo_lingering_time
-syn keyword ngxDirectiveThirdParty contained echo_lingering_timeout
-syn keyword ngxDirectiveThirdParty contained echo_read_buffer_size
-syn keyword ngxDirectiveThirdParty contained echo_read_bytes
-syn keyword ngxDirectiveThirdParty contained echo_read_line
-syn keyword ngxDirectiveThirdParty contained echo_read_timeout
-syn keyword ngxDirectiveThirdParty contained echo_request_data
-syn keyword ngxDirectiveThirdParty contained echo_send_timeout
-syn keyword ngxDirectiveThirdParty contained echo_sleep
-
-" Embed the power of Lua into NGINX TCP/UDP servers
-" https://github.com/openresty/stream-lua-nginx-module
-syn keyword ngxDirectiveThirdParty contained lua_add_variable
-syn keyword ngxDirectiveThirdParty contained preread_by_lua_block
-syn keyword ngxDirectiveThirdParty contained preread_by_lua_file
-syn keyword ngxDirectiveThirdParty contained preread_by_lua_no_postpone
-
-" nginx-upsync-module
-" https://github.com/weibocom/nginx-upsync-module
-syn keyword ngxDirectiveThirdParty contained upstream_show
-syn keyword ngxDirectiveThirdParty contained upsync
-syn keyword ngxDirectiveThirdParty contained upsync_dump_path
-syn keyword ngxDirectiveThirdParty contained upsync_lb
-
-" Whitespace stripper for nginx
-" https://github.com/evanmiller/mod_strip
-syn keyword ngxDirectiveThirdParty contained strip
-
-" Split one big HTTP/Range request to multiple subrange requesets
-" https://github.com/Qihoo360/ngx_http_subrange_module
-syn keyword ngxDirectiveThirdParty contained subrange
-
-" summarizer-nginx-module
-" https://github.com/reeteshranjan/summarizer-nginx-module
-syn keyword ngxDirectiveThirdParty contained summarizer_bind
-syn keyword ngxDirectiveThirdParty contained summarizer_buffer_size
-syn keyword ngxDirectiveThirdParty contained summarizer_connect_timeout
-syn keyword ngxDirectiveThirdParty contained summarizer_next_upstream
-syn keyword ngxDirectiveThirdParty contained summarizer_pass
-syn keyword ngxDirectiveThirdParty contained summarizer_read_timeout
-syn keyword ngxDirectiveThirdParty contained summarizer_send_timeout
-
-" nginx module providing API to communicate with supervisord and manage (start/stop) backends on-demand
-" https://github.com/FRiCKLE/ngx_supervisord
-syn keyword ngxDirectiveThirdParty contained supervisord
-syn keyword ngxDirectiveThirdParty contained supervisord_inherit_backend_status
-syn keyword ngxDirectiveThirdParty contained supervisord_name
-syn keyword ngxDirectiveThirdParty contained supervisord_start
-syn keyword ngxDirectiveThirdParty contained supervisord_stop
-
-" simple robot mitigation module using cookie based challenge/response technique. Not supported any more.
-" https://github.com/kyprizel/testcookie-nginx-module
-syn keyword ngxDirectiveThirdParty contained testcookie
-syn keyword ngxDirectiveThirdParty contained testcookie_arg
-syn keyword ngxDirectiveThirdParty contained testcookie_deny_keepalive
-syn keyword ngxDirectiveThirdParty contained testcookie_domain
-syn keyword ngxDirectiveThirdParty contained testcookie_expires
-syn keyword ngxDirectiveThirdParty contained testcookie_fallback
-syn keyword ngxDirectiveThirdParty contained testcookie_get_only
-syn keyword ngxDirectiveThirdParty contained testcookie_httponly_flag
-syn keyword ngxDirectiveThirdParty contained testcookie_https_location
-syn keyword ngxDirectiveThirdParty contained testcookie_internal
-syn keyword ngxDirectiveThirdParty contained testcookie_max_attempts
-syn keyword ngxDirectiveThirdParty contained testcookie_name
-syn keyword ngxDirectiveThirdParty contained testcookie_p3p
-syn keyword ngxDirectiveThirdParty contained testcookie_pass
-syn keyword ngxDirectiveThirdParty contained testcookie_path
-syn keyword ngxDirectiveThirdParty contained testcookie_port_in_redirect
-syn keyword ngxDirectiveThirdParty contained testcookie_redirect_via_refresh
-syn keyword ngxDirectiveThirdParty contained testcookie_refresh_encrypt_cookie
-syn keyword ngxDirectiveThirdParty contained testcookie_refresh_encrypt_cookie_iv
-syn keyword ngxDirectiveThirdParty contained testcookie_refresh_encrypt_cookie_key
-syn keyword ngxDirectiveThirdParty contained testcookie_refresh_status
-syn keyword ngxDirectiveThirdParty contained testcookie_refresh_template
-syn keyword ngxDirectiveThirdParty contained testcookie_samesite
-syn keyword ngxDirectiveThirdParty contained testcookie_secret
-syn keyword ngxDirectiveThirdParty contained testcookie_secure_flag
-syn keyword ngxDirectiveThirdParty contained testcookie_session
-syn keyword ngxDirectiveThirdParty contained testcookie_whitelist
-
-" ngx_http_types_filter_module
-" https://github.com/flygoast/ngx_http_types_filter
-syn keyword ngxDirectiveThirdParty contained types_filter
-syn keyword ngxDirectiveThirdParty contained types_filter_use_default
-
-" A module allowing the nginx to use files embedded in a zip file
-" https://github.com/youzee/nginx-unzip-module
-syn keyword ngxDirectiveThirdParty contained file_in_unzip
-syn keyword ngxDirectiveThirdParty contained file_in_unzip_archivefile
-syn keyword ngxDirectiveThirdParty contained file_in_unzip_extract
-
-" An asynchronous domain name resolve module for nginx upstream
-" https://github.com/wdaike/ngx_upstream_jdomain
-syn keyword ngxDirectiveThirdParty contained jdomain
-
-" Nginx url encoding converting module
-" https://github.com/vozlt/nginx-module-url
-syn keyword ngxDirectiveThirdParty contained url_encoding_convert
-syn keyword ngxDirectiveThirdParty contained url_encoding_convert_alloc_size
-syn keyword ngxDirectiveThirdParty contained url_encoding_convert_alloc_size_x
-syn keyword ngxDirectiveThirdParty contained url_encoding_convert_from
-syn keyword ngxDirectiveThirdParty contained url_encoding_convert_phase
-syn keyword ngxDirectiveThirdParty contained url_encoding_convert_to
-
-" A nginx module to match browsers and crawlers
-" https://github.com/alibaba/nginx-http-user-agent
-syn keyword ngxDirectiveThirdParty contained user_agent
-
-" nginx load-balancer module implementing ketama consistent hashing
-" https://github.com/flygoast/ngx_http_upstream_ketama_chash
-syn keyword ngxDirectiveThirdParty contained ketama_chash
-
-" nginx-sticky-module-ng
-" https://github.com/ayty-adrianomartins/nginx-sticky-module-ng
-syn keyword ngxDirectiveThirdParty contained sticky_no_fallback
-
-" dynamic linking and call the function of your application
-" https://github.com/Taymindis/nginx-link-function
-syn keyword ngxDirectiveThirdParty contained ngx_link_func_add_prop
-syn keyword ngxDirectiveThirdParty contained ngx_link_func_add_req_header
-syn keyword ngxDirectiveThirdParty contained ngx_link_func_ca_cert
-syn keyword ngxDirectiveThirdParty contained ngx_link_func_call
-syn keyword ngxDirectiveThirdParty contained ngx_link_func_download_link_lib
-syn keyword ngxDirectiveThirdParty contained ngx_link_func_lib
-syn keyword ngxDirectiveThirdParty contained ngx_link_func_shm_size
-syn keyword ngxDirectiveThirdParty contained ngx_link_func_subrequest
-
-" purge content from FastCGI, proxy, SCGI and uWSGI caches
-" https://github.com/torden/ngx_cache_purge
-syn keyword ngxDirectiveThirdParty contained cache_purge_response_type
-
-" set the flags "HttpOnly", "secure" and "SameSite" for cookies
-" https://github.com/AirisX/nginx_cookie_flag_module
-syn keyword ngxDirectiveThirdParty contained set_cookie_flag
-
-" Embed websockify into Nginx (convert any tcp connection into websocket)
 " https://github.com/tg123/websockify-nginx-module
 syn keyword ngxDirectiveThirdParty contained websockify_buffer_size
 syn keyword ngxDirectiveThirdParty contained websockify_connect_timeout
@@ -2415,54 +1941,13 @@ syn keyword ngxDirectiveThirdParty contained websockify_pass
 syn keyword ngxDirectiveThirdParty contained websockify_read_timeout
 syn keyword ngxDirectiveThirdParty contained websockify_send_timeout
 
-" IP2Location Nginx
-" https://github.com/ip2location/ip2location-nginx
-syn keyword ngxDirectiveThirdParty contained ip2location_addresstype
-syn keyword ngxDirectiveThirdParty contained ip2location_areacode
-syn keyword ngxDirectiveThirdParty contained ip2location_category
-syn keyword ngxDirectiveThirdParty contained ip2location_city
-syn keyword ngxDirectiveThirdParty contained ip2location_country_long
-syn keyword ngxDirectiveThirdParty contained ip2location_country_short
-syn keyword ngxDirectiveThirdParty contained ip2location_domain
-syn keyword ngxDirectiveThirdParty contained ip2location_elevation
-syn keyword ngxDirectiveThirdParty contained ip2location_iddcode
-syn keyword ngxDirectiveThirdParty contained ip2location_isp
-syn keyword ngxDirectiveThirdParty contained ip2location_latitude
-syn keyword ngxDirectiveThirdParty contained ip2location_longitude
-syn keyword ngxDirectiveThirdParty contained ip2location_mcc
-syn keyword ngxDirectiveThirdParty contained ip2location_mnc
-syn keyword ngxDirectiveThirdParty contained ip2location_mobilebrand
-syn keyword ngxDirectiveThirdParty contained ip2location_netspeed
-syn keyword ngxDirectiveThirdParty contained ip2location_proxy
-syn keyword ngxDirectiveThirdParty contained ip2location_proxy_recursive
-syn keyword ngxDirectiveThirdParty contained ip2location_region
-syn keyword ngxDirectiveThirdParty contained ip2location_timezone
-syn keyword ngxDirectiveThirdParty contained ip2location_usagetype
-syn keyword ngxDirectiveThirdParty contained ip2location_weatherstationcode
-syn keyword ngxDirectiveThirdParty contained ip2location_weatherstationname
-syn keyword ngxDirectiveThirdParty contained ip2location_zipcode
-
-" IP2Proxy module for Nginx
-" https://github.com/ip2location/ip2proxy-nginx
-syn keyword ngxDirectiveThirdParty contained ip2proxy_as
-syn keyword ngxDirectiveThirdParty contained ip2proxy_asn
-syn keyword ngxDirectiveThirdParty contained ip2proxy_city
-syn keyword ngxDirectiveThirdParty contained ip2proxy_country_long
-syn keyword ngxDirectiveThirdParty contained ip2proxy_country_short
-syn keyword ngxDirectiveThirdParty contained ip2proxy_database
-syn keyword ngxDirectiveThirdParty contained ip2proxy_domain
-syn keyword ngxDirectiveThirdParty contained ip2proxy_isp
-syn keyword ngxDirectiveThirdParty contained ip2proxy_is_proxy
-syn keyword ngxDirectiveThirdParty contained ip2proxy_last_seen
-syn keyword ngxDirectiveThirdParty contained ip2proxy_provider
-syn keyword ngxDirectiveThirdParty contained ip2proxy_proxy
-syn keyword ngxDirectiveThirdParty contained ip2proxy_proxy_recursive
-syn keyword ngxDirectiveThirdParty contained ip2proxy_proxy_type
-syn keyword ngxDirectiveThirdParty contained ip2proxy_region
-syn keyword ngxDirectiveThirdParty contained ip2proxy_threat
-syn keyword ngxDirectiveThirdParty contained ip2proxy_usage_type
-
-
+" https://github.com/vozlt/nginx-module-sts
+syn keyword ngxDirectiveThirdParty contained stream_server_traffic_status
+syn keyword ngxDirectiveThirdParty contained stream_server_traffic_status_average_method
+syn keyword ngxDirectiveThirdParty contained stream_server_traffic_status_display
+syn keyword ngxDirectiveThirdParty contained stream_server_traffic_status_display_format
+syn keyword ngxDirectiveThirdParty contained stream_server_traffic_status_display_jsonp
+syn keyword ngxDirectiveThirdParty contained stream_server_traffic_status_zone
 
 " highlight
 
diff --git a/src/core/nginx.c b/src/core/nginx.c
index 48a20e9..0deb27b 100644
--- a/src/core/nginx.c
+++ b/src/core/nginx.c
@@ -13,6 +13,7 @@
 static void ngx_show_version_info(void);
 static ngx_int_t ngx_add_inherited_sockets(ngx_cycle_t *cycle);
 static void ngx_cleanup_environment(void *data);
+static void ngx_cleanup_environment_variable(void *data);
 static ngx_int_t ngx_get_options(int argc, char *const *argv);
 static ngx_int_t ngx_process_options(ngx_cycle_t *cycle);
 static ngx_int_t ngx_save_argv(ngx_cycle_t *cycle, int argc, char *const *argv);
@@ -518,7 +519,8 @@ ngx_add_inherited_sockets(ngx_cycle_t *cycle)
 char **
 ngx_set_environment(ngx_cycle_t *cycle, ngx_uint_t *last)
 {
-    char                **p, **env;
+    char                **p, **env, *str;
+    size_t                len;
     ngx_str_t            *var;
     ngx_uint_t            i, n;
     ngx_core_conf_t      *ccf;
@@ -600,7 +602,31 @@ tz_found:
     for (i = 0; i < ccf->env.nelts; i++) {
 
         if (var[i].data[var[i].len] == '=') {
-            env[n++] = (char *) var[i].data;
+
+            if (last) {
+                env[n++] = (char *) var[i].data;
+                continue;
+            }
+
+            cln = ngx_pool_cleanup_add(cycle->pool, 0);
+            if (cln == NULL) {
+                return NULL;
+            }
+
+            len = ngx_strlen(var[i].data) + 1;
+
+            str = ngx_alloc(len, cycle->log);
+            if (str == NULL) {
+                return NULL;
+            }
+
+            ngx_memcpy(str, var[i].data, len);
+
+            cln->handler = ngx_cleanup_environment_variable;
+            cln->data = str;
+
+            env[n++] = str;
+
             continue;
         }
 
@@ -645,6 +671,29 @@ ngx_cleanup_environment(void *data)
 }
 
 
+static void
+ngx_cleanup_environment_variable(void *data)
+{
+    char  *var = data;
+
+    char  **p;
+
+    for (p = environ; *p; p++) {
+
+        /*
+         * if an environment variable is still used, as it happens on exit,
+         * the only option is to leak it
+         */
+
+        if (*p == var) {
+            return;
+        }
+    }
+
+    ngx_free(var);
+}
+
+
 ngx_pid_t
 ngx_exec_new_binary(ngx_cycle_t *cycle, char *const *argv)
 {
@@ -680,6 +729,9 @@ ngx_exec_new_binary(ngx_cycle_t *cycle, char *const *argv)
 
     ls = cycle->listening.elts;
     for (i = 0; i < cycle->listening.nelts; i++) {
+        if (ls[i].ignore) {
+            continue;
+        }
         p = ngx_sprintf(p, "%ud;", ls[i].fd);
     }
 
diff --git a/src/core/nginx.h b/src/core/nginx.h
index 9f8756b..ef000e0 100644
--- a/src/core/nginx.h
+++ b/src/core/nginx.h
@@ -9,8 +9,8 @@
 #define _NGINX_H_INCLUDED_
 
 
-#define nginx_version      1022001
-#define NGINX_VERSION      "1.22.1"
+#define nginx_version      1026003
+#define NGINX_VERSION      "1.26.3"
 #define NGINX_VER          "nginx/" NGINX_VERSION
 
 #ifdef NGX_BUILD
diff --git a/src/core/ngx_bpf.c b/src/core/ngx_bpf.c
new file mode 100644
index 0000000..363a02c
--- /dev/null
+++ b/src/core/ngx_bpf.c
@@ -0,0 +1,143 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+#define NGX_BPF_LOGBUF_SIZE  (16 * 1024)
+
+
+static ngx_inline int
+ngx_bpf(enum bpf_cmd cmd, union bpf_attr *attr, unsigned int size)
+{
+    return syscall(__NR_bpf, cmd, attr, size);
+}
+
+
+void
+ngx_bpf_program_link(ngx_bpf_program_t *program, const char *symbol, int fd)
+{
+    ngx_uint_t        i;
+    ngx_bpf_reloc_t  *rl;
+
+    rl = program->relocs;
+
+    for (i = 0; i < program->nrelocs; i++) {
+        if (ngx_strcmp(rl[i].name, symbol) == 0) {
+            program->ins[rl[i].offset].src_reg = 1;
+            program->ins[rl[i].offset].imm = fd;
+        }
+    }
+}
+
+
+int
+ngx_bpf_load_program(ngx_log_t *log, ngx_bpf_program_t *program)
+{
+    int             fd;
+    union bpf_attr  attr;
+#if (NGX_DEBUG)
+    char            buf[NGX_BPF_LOGBUF_SIZE];
+#endif
+
+    ngx_memzero(&attr, sizeof(union bpf_attr));
+
+    attr.license = (uintptr_t) program->license;
+    attr.prog_type = program->type;
+    attr.insns = (uintptr_t) program->ins;
+    attr.insn_cnt = program->nins;
+
+#if (NGX_DEBUG)
+    /* for verifier errors */
+    attr.log_buf = (uintptr_t) buf;
+    attr.log_size = NGX_BPF_LOGBUF_SIZE;
+    attr.log_level = 1;
+#endif
+
+    fd = ngx_bpf(BPF_PROG_LOAD, &attr, sizeof(attr));
+    if (fd < 0) {
+        ngx_log_error(NGX_LOG_ALERT, log, ngx_errno,
+                      "failed to load BPF program");
+
+        ngx_log_debug1(NGX_LOG_DEBUG_CORE, log, 0,
+                       "bpf verifier: %s", buf);
+
+        return -1;
+    }
+
+    return fd;
+}
+
+
+int
+ngx_bpf_map_create(ngx_log_t *log, enum bpf_map_type type, int key_size,
+    int value_size, int max_entries, uint32_t map_flags)
+{
+    int             fd;
+    union bpf_attr  attr;
+
+    ngx_memzero(&attr, sizeof(union bpf_attr));
+
+    attr.map_type = type;
+    attr.key_size = key_size;
+    attr.value_size = value_size;
+    attr.max_entries = max_entries;
+    attr.map_flags = map_flags;
+
+    fd = ngx_bpf(BPF_MAP_CREATE, &attr, sizeof(attr));
+    if (fd < 0) {
+        ngx_log_error(NGX_LOG_ALERT, log, ngx_errno,
+                      "failed to create BPF map");
+        return NGX_ERROR;
+    }
+
+    return fd;
+}
+
+
+int
+ngx_bpf_map_update(int fd, const void *key, const void *value, uint64_t flags)
+{
+    union bpf_attr attr;
+
+    ngx_memzero(&attr, sizeof(union bpf_attr));
+
+    attr.map_fd = fd;
+    attr.key = (uintptr_t) key;
+    attr.value = (uintptr_t) value;
+    attr.flags = flags;
+
+    return ngx_bpf(BPF_MAP_UPDATE_ELEM, &attr, sizeof(attr));
+}
+
+
+int
+ngx_bpf_map_delete(int fd, const void *key)
+{
+    union bpf_attr attr;
+
+    ngx_memzero(&attr, sizeof(union bpf_attr));
+
+    attr.map_fd = fd;
+    attr.key = (uintptr_t) key;
+
+    return ngx_bpf(BPF_MAP_DELETE_ELEM, &attr, sizeof(attr));
+}
+
+
+int
+ngx_bpf_map_lookup(int fd, const void *key, void *value)
+{
+    union bpf_attr attr;
+
+    ngx_memzero(&attr, sizeof(union bpf_attr));
+
+    attr.map_fd = fd;
+    attr.key = (uintptr_t) key;
+    attr.value = (uintptr_t) value;
+
+    return ngx_bpf(BPF_MAP_LOOKUP_ELEM, &attr, sizeof(attr));
+}
diff --git a/src/core/ngx_bpf.h b/src/core/ngx_bpf.h
new file mode 100644
index 0000000..f62a36e
--- /dev/null
+++ b/src/core/ngx_bpf.h
@@ -0,0 +1,43 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_BPF_H_INCLUDED_
+#define _NGX_BPF_H_INCLUDED_
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+#include <linux/bpf.h>
+
+
+typedef struct {
+    char                *name;
+    int                  offset;
+} ngx_bpf_reloc_t;
+
+typedef struct {
+    char                *license;
+    enum bpf_prog_type   type;
+    struct bpf_insn     *ins;
+    size_t               nins;
+    ngx_bpf_reloc_t     *relocs;
+    size_t               nrelocs;
+} ngx_bpf_program_t;
+
+
+void ngx_bpf_program_link(ngx_bpf_program_t *program, const char *symbol,
+    int fd);
+int ngx_bpf_load_program(ngx_log_t *log, ngx_bpf_program_t *program);
+
+int ngx_bpf_map_create(ngx_log_t *log, enum bpf_map_type type, int key_size,
+    int value_size, int max_entries, uint32_t map_flags);
+int ngx_bpf_map_update(int fd, const void *key, const void *value,
+    uint64_t flags);
+int ngx_bpf_map_delete(int fd, const void *key);
+int ngx_bpf_map_lookup(int fd, const void *key, void *value);
+
+#endif /* _NGX_BPF_H_INCLUDED_ */
diff --git a/src/core/ngx_conf_file.c b/src/core/ngx_conf_file.c
index fec7bb8..197704b 100644
--- a/src/core/ngx_conf_file.c
+++ b/src/core/ngx_conf_file.c
@@ -544,8 +544,8 @@ ngx_conf_read_token(ngx_conf_t *cf)
                     }
 
                     ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
-                                  "unexpected end of file, "
-                                  "expecting \";\" or \"}\"");
+                                       "unexpected end of file, "
+                                       "expecting \";\" or \"}\"");
                     return NGX_ERROR;
                 }
 
diff --git a/src/core/ngx_connection.c b/src/core/ngx_connection.c
index fe729a7..75809d9 100644
--- a/src/core/ngx_connection.c
+++ b/src/core/ngx_connection.c
@@ -660,7 +660,7 @@ ngx_open_listening_sockets(ngx_cycle_t *cycle)
                 /*
                  * on OpenVZ after suspend/resume EADDRINUSE
                  * may be returned by listen() instead of bind(), see
-                 * https://bugzilla.openvz.org/show_bug.cgi?id=2470
+                 * https://bugs.openvz.org/browse/OVZ-5587
                  */
 
                 if (err != NGX_EADDRINUSE || !ngx_test_config) {
@@ -1013,6 +1013,78 @@ ngx_configure_listening_sockets(ngx_cycle_t *cycle)
             }
         }
 
+#endif
+
+#if (NGX_HAVE_IP_MTU_DISCOVER)
+
+        if (ls[i].quic && ls[i].sockaddr->sa_family == AF_INET) {
+            value = IP_PMTUDISC_DO;
+
+            if (setsockopt(ls[i].fd, IPPROTO_IP, IP_MTU_DISCOVER,
+                           (const void *) &value, sizeof(int))
+                == -1)
+            {
+                ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_socket_errno,
+                              "setsockopt(IP_MTU_DISCOVER) "
+                              "for %V failed, ignored",
+                              &ls[i].addr_text);
+            }
+        }
+
+#elif (NGX_HAVE_IP_DONTFRAG)
+
+        if (ls[i].quic && ls[i].sockaddr->sa_family == AF_INET) {
+            value = 1;
+
+            if (setsockopt(ls[i].fd, IPPROTO_IP, IP_DONTFRAG,
+                           (const void *) &value, sizeof(int))
+                == -1)
+            {
+                ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_socket_errno,
+                              "setsockopt(IP_DONTFRAG) "
+                              "for %V failed, ignored",
+                              &ls[i].addr_text);
+            }
+        }
+
+#endif
+
+#if (NGX_HAVE_INET6)
+
+#if (NGX_HAVE_IPV6_MTU_DISCOVER)
+
+        if (ls[i].quic && ls[i].sockaddr->sa_family == AF_INET6) {
+            value = IPV6_PMTUDISC_DO;
+
+            if (setsockopt(ls[i].fd, IPPROTO_IPV6, IPV6_MTU_DISCOVER,
+                           (const void *) &value, sizeof(int))
+                == -1)
+            {
+                ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_socket_errno,
+                              "setsockopt(IPV6_MTU_DISCOVER) "
+                              "for %V failed, ignored",
+                              &ls[i].addr_text);
+            }
+        }
+
+#elif (NGX_HAVE_IP_DONTFRAG)
+
+        if (ls[i].quic && ls[i].sockaddr->sa_family == AF_INET6) {
+            value = 1;
+
+            if (setsockopt(ls[i].fd, IPPROTO_IPV6, IPV6_DONTFRAG,
+                           (const void *) &value, sizeof(int))
+                == -1)
+            {
+                ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_socket_errno,
+                              "setsockopt(IPV6_DONTFRAG) "
+                              "for %V failed, ignored",
+                              &ls[i].addr_text);
+            }
+        }
+
+#endif
+
 #endif
     }
 
@@ -1037,6 +1109,12 @@ ngx_close_listening_sockets(ngx_cycle_t *cycle)
     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) {
@@ -1505,6 +1583,10 @@ ngx_connection_error(ngx_connection_t *c, ngx_err_t err, char *text)
     }
 #endif
 
+    if (err == NGX_EMSGSIZE && c->log_error == NGX_ERROR_IGNORE_EMSGSIZE) {
+        return 0;
+    }
+
     if (err == 0
         || err == NGX_ECONNRESET
 #if (NGX_WIN32)
@@ -1522,6 +1604,7 @@ ngx_connection_error(ngx_connection_t *c, ngx_err_t err, char *text)
     {
         switch (c->log_error) {
 
+        case NGX_ERROR_IGNORE_EMSGSIZE:
         case NGX_ERROR_IGNORE_EINVAL:
         case NGX_ERROR_IGNORE_ECONNRESET:
         case NGX_ERROR_INFO:
diff --git a/src/core/ngx_connection.h b/src/core/ngx_connection.h
index 8cc1475..84dd804 100644
--- a/src/core/ngx_connection.h
+++ b/src/core/ngx_connection.h
@@ -73,6 +73,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;
@@ -96,7 +97,8 @@ typedef enum {
     NGX_ERROR_ERR,
     NGX_ERROR_INFO,
     NGX_ERROR_IGNORE_ECONNRESET,
-    NGX_ERROR_IGNORE_EINVAL
+    NGX_ERROR_IGNORE_EINVAL,
+    NGX_ERROR_IGNORE_EMSGSIZE
 } ngx_connection_log_error_e;
 
 
@@ -147,6 +149,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
@@ -172,6 +178,7 @@ struct ngx_connection_s {
     unsigned            timedout:1;
     unsigned            error:1;
     unsigned            destroyed:1;
+    unsigned            pipeline:1;
 
     unsigned            idle:1;
     unsigned            reusable:1;
@@ -184,6 +191,7 @@ struct ngx_connection_s {
     unsigned            tcp_nopush:2;    /* ngx_connection_tcp_nopush_e */
 
     unsigned            need_last_buf:1;
+    unsigned            need_flush_buf:1;
 
 #if (NGX_HAVE_SENDFILE_NODISKIO || NGX_COMPAT)
     unsigned            busy_count:2;
diff --git a/src/core/ngx_core.h b/src/core/ngx_core.h
index 7ecdca0..88db7dc 100644
--- a/src/core/ngx_core.h
+++ b/src/core/ngx_core.h
@@ -27,6 +27,7 @@ typedef struct ngx_connection_s      ngx_connection_t;
 typedef struct ngx_thread_task_s     ngx_thread_task_t;
 typedef struct ngx_ssl_s             ngx_ssl_t;
 typedef struct ngx_proxy_protocol_s  ngx_proxy_protocol_t;
+typedef struct ngx_quic_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)(ngx_connection_t *c);
 #include <ngx_resolver.h>
 #if (NGX_OPENSSL)
 #include <ngx_event_openssl.h>
+#if (NGX_QUIC)
+#include <ngx_event_quic.h>
+#endif
 #endif
 #include <ngx_process_cycle.h>
 #include <ngx_conf_file.h>
@@ -91,6 +95,9 @@ typedef void (*ngx_connection_handler_pt)(ngx_connection_t *c);
 #include <ngx_connection.h>
 #include <ngx_syslog.h>
 #include <ngx_proxy_protocol.h>
+#if (NGX_HAVE_BPF)
+#include <ngx_bpf.h>
+#endif
 
 
 #define LF     (u_char) '\n'
diff --git a/src/core/ngx_hash.h b/src/core/ngx_hash.h
index abc3cbe..3b57099 100644
--- a/src/core/ngx_hash.h
+++ b/src/core/ngx_hash.h
@@ -89,12 +89,15 @@ typedef struct {
 } ngx_hash_keys_arrays_t;
 
 
-typedef struct {
+typedef struct ngx_table_elt_s  ngx_table_elt_t;
+
+struct ngx_table_elt_s {
     ngx_uint_t        hash;
     ngx_str_t         key;
     ngx_str_t         value;
     u_char           *lowcase_key;
-} ngx_table_elt_t;
+    ngx_table_elt_t  *next;
+};
 
 
 void *ngx_hash_find(ngx_hash_t *hash, ngx_uint_t key, u_char *name, size_t len);
diff --git a/src/core/ngx_inet.c b/src/core/ngx_inet.c
index 4228504..acb2ef4 100644
--- a/src/core/ngx_inet.c
+++ b/src/core/ngx_inet.c
@@ -507,7 +507,7 @@ ngx_cidr_match(struct sockaddr *sa, ngx_array_t *cidrs)
 
             p = inaddr6->s6_addr;
 
-            inaddr = p[12] << 24;
+            inaddr = (in_addr_t) p[12] << 24;
             inaddr += p[13] << 16;
             inaddr += p[14] << 8;
             inaddr += p[15];
diff --git a/src/core/ngx_module.h b/src/core/ngx_module.h
index 6fb4554..a415cd6 100644
--- a/src/core/ngx_module.h
+++ b/src/core/ngx_module.h
@@ -107,7 +107,12 @@
 #endif
 
 #define NGX_MODULE_SIGNATURE_17  "0"
+
+#if (NGX_QUIC || NGX_COMPAT)
+#define NGX_MODULE_SIGNATURE_18  "1"
+#else
 #define NGX_MODULE_SIGNATURE_18  "0"
+#endif
 
 #if (NGX_HAVE_OPENAT)
 #define NGX_MODULE_SIGNATURE_19  "1"
diff --git a/src/core/ngx_output_chain.c b/src/core/ngx_output_chain.c
index 8570742..a46209c 100644
--- a/src/core/ngx_output_chain.c
+++ b/src/core/ngx_output_chain.c
@@ -117,7 +117,10 @@ ngx_output_chain(ngx_output_chain_ctx_t *ctx, ngx_chain_t *in)
 
                 ngx_debug_point();
 
-                ctx->in = ctx->in->next;
+                cl = ctx->in;
+                ctx->in = cl->next;
+
+                ngx_free_chain(ctx->pool, cl);
 
                 continue;
             }
@@ -203,7 +206,10 @@ ngx_output_chain(ngx_output_chain_ctx_t *ctx, ngx_chain_t *in)
             /* delete the completed buf from the ctx->in chain */
 
             if (ngx_buf_size(ctx->in->buf) == 0) {
-                ctx->in = ctx->in->next;
+                cl = ctx->in;
+                ctx->in = cl->next;
+
+                ngx_free_chain(ctx->pool, cl);
             }
 
             cl = ngx_alloc_chain_link(ctx->pool);
diff --git a/src/core/ngx_proxy_protocol.c b/src/core/ngx_proxy_protocol.c
index 7a9e7f9..49888b9 100644
--- a/src/core/ngx_proxy_protocol.c
+++ b/src/core/ngx_proxy_protocol.c
@@ -13,7 +13,15 @@
 #define NGX_PROXY_PROTOCOL_AF_INET6         2
 
 
-#define ngx_proxy_protocol_parse_uint16(p)  ((p)[0] << 8 | (p)[1])
+#define ngx_proxy_protocol_parse_uint16(p)                                    \
+    ( ((uint16_t) (p)[0] << 8)                                                \
+    + (           (p)[1]) )
+
+#define ngx_proxy_protocol_parse_uint32(p)                                    \
+    ( ((uint32_t) (p)[0] << 24)                                               \
+    + (           (p)[1] << 16)                                               \
+    + (           (p)[2] << 8)                                                \
+    + (           (p)[3]) )
 
 
 typedef struct {
@@ -40,12 +48,52 @@ typedef struct {
 } ngx_proxy_protocol_inet6_addrs_t;
 
 
+typedef struct {
+    u_char                                  type;
+    u_char                                  len[2];
+} ngx_proxy_protocol_tlv_t;
+
+
+typedef struct {
+    u_char                                  client;
+    u_char                                  verify[4];
+} ngx_proxy_protocol_tlv_ssl_t;
+
+
+typedef struct {
+    ngx_str_t                               name;
+    ngx_uint_t                              type;
+} ngx_proxy_protocol_tlv_entry_t;
+
+
 static u_char *ngx_proxy_protocol_read_addr(ngx_connection_t *c, u_char *p,
     u_char *last, ngx_str_t *addr);
 static u_char *ngx_proxy_protocol_read_port(u_char *p, u_char *last,
     in_port_t *port, u_char sep);
 static u_char *ngx_proxy_protocol_v2_read(ngx_connection_t *c, u_char *buf,
     u_char *last);
+static ngx_int_t ngx_proxy_protocol_lookup_tlv(ngx_connection_t *c,
+    ngx_str_t *tlvs, ngx_uint_t type, ngx_str_t *value);
+
+
+static ngx_proxy_protocol_tlv_entry_t  ngx_proxy_protocol_tlv_entries[] = {
+    { ngx_string("alpn"),       0x01 },
+    { ngx_string("authority"),  0x02 },
+    { ngx_string("unique_id"),  0x05 },
+    { ngx_string("ssl"),        0x20 },
+    { ngx_string("netns"),      0x30 },
+    { ngx_null_string,          0x00 }
+};
+
+
+static ngx_proxy_protocol_tlv_entry_t  ngx_proxy_protocol_tlv_ssl_entries[] = {
+    { ngx_string("version"),    0x21 },
+    { ngx_string("cn"),         0x22 },
+    { ngx_string("cipher"),     0x23 },
+    { ngx_string("sig_alg"),    0x24 },
+    { ngx_string("key_alg"),    0x25 },
+    { ngx_null_string,          0x00 }
+};
 
 
 u_char *
@@ -61,7 +109,7 @@ ngx_proxy_protocol_read(ngx_connection_t *c, u_char *buf, u_char *last)
     len = last - buf;
 
     if (len >= sizeof(ngx_proxy_protocol_header_t)
-        && memcmp(p, signature, sizeof(signature) - 1) == 0)
+        && ngx_memcmp(p, signature, sizeof(signature) - 1) == 0)
     {
         return ngx_proxy_protocol_v2_read(c, buf, last);
     }
@@ -139,8 +187,14 @@ skip:
 
 invalid:
 
+    for (p = buf; p < last; p++) {
+        if (*p == CR || *p == LF) {
+            break;
+        }
+    }
+
     ngx_log_error(NGX_LOG_ERR, c->log, 0,
-                  "broken header: \"%*s\"", (size_t) (last - buf), buf);
+                  "broken header: \"%*s\"", (size_t) (p - buf), buf);
 
     return NULL;
 }
@@ -227,7 +281,9 @@ ngx_proxy_protocol_write(ngx_connection_t *c, u_char *buf, u_char *last)
 {
     ngx_uint_t  port, lport;
 
-    if (last - buf < NGX_PROXY_PROTOCOL_MAX_HEADER) {
+    if (last - buf < NGX_PROXY_PROTOCOL_V1_MAX_HEADER) {
+        ngx_log_error(NGX_LOG_ALERT, c->log, 0,
+                      "too small buffer for PROXY protocol");
         return NULL;
     }
 
@@ -340,11 +396,11 @@ ngx_proxy_protocol_v2_read(ngx_connection_t *c, u_char *buf, u_char *last)
 
         src_sockaddr.sockaddr_in.sin_family = AF_INET;
         src_sockaddr.sockaddr_in.sin_port = 0;
-        memcpy(&src_sockaddr.sockaddr_in.sin_addr, in->src_addr, 4);
+        ngx_memcpy(&src_sockaddr.sockaddr_in.sin_addr, in->src_addr, 4);
 
         dst_sockaddr.sockaddr_in.sin_family = AF_INET;
         dst_sockaddr.sockaddr_in.sin_port = 0;
-        memcpy(&dst_sockaddr.sockaddr_in.sin_addr, in->dst_addr, 4);
+        ngx_memcpy(&dst_sockaddr.sockaddr_in.sin_addr, in->dst_addr, 4);
 
         pp->src_port = ngx_proxy_protocol_parse_uint16(in->src_port);
         pp->dst_port = ngx_proxy_protocol_parse_uint16(in->dst_port);
@@ -367,11 +423,11 @@ ngx_proxy_protocol_v2_read(ngx_connection_t *c, u_char *buf, u_char *last)
 
         src_sockaddr.sockaddr_in6.sin6_family = AF_INET6;
         src_sockaddr.sockaddr_in6.sin6_port = 0;
-        memcpy(&src_sockaddr.sockaddr_in6.sin6_addr, in6->src_addr, 16);
+        ngx_memcpy(&src_sockaddr.sockaddr_in6.sin6_addr, in6->src_addr, 16);
 
         dst_sockaddr.sockaddr_in6.sin6_family = AF_INET6;
         dst_sockaddr.sockaddr_in6.sin6_port = 0;
-        memcpy(&dst_sockaddr.sockaddr_in6.sin6_addr, in6->dst_addr, 16);
+        ngx_memcpy(&dst_sockaddr.sockaddr_in6.sin6_addr, in6->dst_addr, 16);
 
         pp->src_port = ngx_proxy_protocol_parse_uint16(in6->src_port);
         pp->dst_port = ngx_proxy_protocol_parse_uint16(in6->dst_port);
@@ -412,11 +468,147 @@ ngx_proxy_protocol_v2_read(ngx_connection_t *c, u_char *buf, u_char *last)
                    &pp->src_addr, pp->src_port, &pp->dst_addr, pp->dst_port);
 
     if (buf < end) {
-        ngx_log_debug1(NGX_LOG_DEBUG_CORE, c->log, 0,
-                       "PROXY protocol v2 %z bytes of tlv ignored", end - buf);
+        pp->tlvs.data = ngx_pnalloc(c->pool, end - buf);
+        if (pp->tlvs.data == NULL) {
+            return NULL;
+        }
+
+        ngx_memcpy(pp->tlvs.data, buf, end - buf);
+        pp->tlvs.len = end - buf;
     }
 
     c->proxy_protocol = pp;
 
     return end;
 }
+
+
+ngx_int_t
+ngx_proxy_protocol_get_tlv(ngx_connection_t *c, ngx_str_t *name,
+    ngx_str_t *value)
+{
+    u_char                          *p;
+    size_t                           n;
+    uint32_t                         verify;
+    ngx_str_t                        ssl, *tlvs;
+    ngx_int_t                        rc, type;
+    ngx_proxy_protocol_tlv_ssl_t    *tlv_ssl;
+    ngx_proxy_protocol_tlv_entry_t  *te;
+
+    if (c->proxy_protocol == NULL) {
+        return NGX_DECLINED;
+    }
+
+    ngx_log_debug1(NGX_LOG_DEBUG_CORE, c->log, 0,
+                   "PROXY protocol v2 get tlv \"%V\"", name);
+
+    te = ngx_proxy_protocol_tlv_entries;
+    tlvs = &c->proxy_protocol->tlvs;
+
+    p = name->data;
+    n = name->len;
+
+    if (n >= 4 && p[0] == 's' && p[1] == 's' && p[2] == 'l' && p[3] == '_') {
+
+        rc = ngx_proxy_protocol_lookup_tlv(c, tlvs, 0x20, &ssl);
+        if (rc != NGX_OK) {
+            return rc;
+        }
+
+        if (ssl.len < sizeof(ngx_proxy_protocol_tlv_ssl_t)) {
+            return NGX_ERROR;
+        }
+
+        p += 4;
+        n -= 4;
+
+        if (n == 6 && ngx_strncmp(p, "verify", 6) == 0) {
+
+            tlv_ssl = (ngx_proxy_protocol_tlv_ssl_t *) ssl.data;
+            verify = ngx_proxy_protocol_parse_uint32(tlv_ssl->verify);
+
+            value->data = ngx_pnalloc(c->pool, NGX_INT32_LEN);
+            if (value->data == NULL) {
+                return NGX_ERROR;
+            }
+
+            value->len = ngx_sprintf(value->data, "%uD", verify)
+                         - value->data;
+            return NGX_OK;
+        }
+
+        ssl.data += sizeof(ngx_proxy_protocol_tlv_ssl_t);
+        ssl.len -= sizeof(ngx_proxy_protocol_tlv_ssl_t);
+
+        te = ngx_proxy_protocol_tlv_ssl_entries;
+        tlvs = &ssl;
+    }
+
+    if (n >= 2 && p[0] == '0' && p[1] == 'x') {
+
+        type = ngx_hextoi(p + 2, n - 2);
+        if (type == NGX_ERROR) {
+            ngx_log_error(NGX_LOG_ERR, c->log, 0,
+                          "invalid PROXY protocol TLV \"%V\"", name);
+            return NGX_ERROR;
+        }
+
+        return ngx_proxy_protocol_lookup_tlv(c, tlvs, type, value);
+    }
+
+    for ( /* void */ ; te->type; te++) {
+        if (te->name.len == n && ngx_strncmp(te->name.data, p, n) == 0) {
+            return ngx_proxy_protocol_lookup_tlv(c, tlvs, te->type, value);
+        }
+    }
+
+    ngx_log_error(NGX_LOG_ERR, c->log, 0,
+                  "unknown PROXY protocol TLV \"%V\"", name);
+
+    return NGX_DECLINED;
+}
+
+
+static ngx_int_t
+ngx_proxy_protocol_lookup_tlv(ngx_connection_t *c, ngx_str_t *tlvs,
+    ngx_uint_t type, ngx_str_t *value)
+{
+    u_char                    *p;
+    size_t                     n, len;
+    ngx_proxy_protocol_tlv_t  *tlv;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_CORE, c->log, 0,
+                   "PROXY protocol v2 lookup tlv:%02xi", type);
+
+    p = tlvs->data;
+    n = tlvs->len;
+
+    while (n) {
+        if (n < sizeof(ngx_proxy_protocol_tlv_t)) {
+            ngx_log_error(NGX_LOG_ERR, c->log, 0, "broken PROXY protocol TLV");
+            return NGX_ERROR;
+        }
+
+        tlv = (ngx_proxy_protocol_tlv_t *) p;
+        len = ngx_proxy_protocol_parse_uint16(tlv->len);
+
+        p += sizeof(ngx_proxy_protocol_tlv_t);
+        n -= sizeof(ngx_proxy_protocol_tlv_t);
+
+        if (n < len) {
+            ngx_log_error(NGX_LOG_ERR, c->log, 0, "broken PROXY protocol TLV");
+            return NGX_ERROR;
+        }
+
+        if (tlv->type == type) {
+            value->data = p;
+            value->len = len;
+            return NGX_OK;
+        }
+
+        p += len;
+        n -= len;
+    }
+
+    return NGX_DECLINED;
+}
diff --git a/src/core/ngx_proxy_protocol.h b/src/core/ngx_proxy_protocol.h
index b716220..d1749f5 100644
--- a/src/core/ngx_proxy_protocol.h
+++ b/src/core/ngx_proxy_protocol.h
@@ -13,7 +13,8 @@
 #include <ngx_core.h>
 
 
-#define NGX_PROXY_PROTOCOL_MAX_HEADER  107
+#define NGX_PROXY_PROTOCOL_V1_MAX_HEADER  107
+#define NGX_PROXY_PROTOCOL_MAX_HEADER     4096
 
 
 struct ngx_proxy_protocol_s {
@@ -21,6 +22,7 @@ struct ngx_proxy_protocol_s {
     ngx_str_t           dst_addr;
     in_port_t           src_port;
     in_port_t           dst_port;
+    ngx_str_t           tlvs;
 };
 
 
@@ -28,6 +30,8 @@ u_char *ngx_proxy_protocol_read(ngx_connection_t *c, u_char *buf,
     u_char *last);
 u_char *ngx_proxy_protocol_write(ngx_connection_t *c, u_char *buf,
     u_char *last);
+ngx_int_t ngx_proxy_protocol_get_tlv(ngx_connection_t *c, ngx_str_t *name,
+    ngx_str_t *value);
 
 
 #endif /* _NGX_PROXY_PROTOCOL_H_INCLUDED_ */
diff --git a/src/core/ngx_queue.c b/src/core/ngx_queue.c
index 3cacaf3..3d1d589 100644
--- a/src/core/ngx_queue.c
+++ b/src/core/ngx_queue.c
@@ -9,6 +9,10 @@
 #include <ngx_core.h>
 
 
+static void ngx_queue_merge(ngx_queue_t *queue, ngx_queue_t *tail,
+    ngx_int_t (*cmp)(const ngx_queue_t *, const ngx_queue_t *));
+
+
 /*
  * find the middle queue element if the queue has odd number of elements
  * or the first element of the queue's second part otherwise
@@ -45,13 +49,13 @@ ngx_queue_middle(ngx_queue_t *queue)
 }
 
 
-/* the stable insertion sort */
+/* the stable merge sort */
 
 void
 ngx_queue_sort(ngx_queue_t *queue,
     ngx_int_t (*cmp)(const ngx_queue_t *, const ngx_queue_t *))
 {
-    ngx_queue_t  *q, *prev, *next;
+    ngx_queue_t  *q, tail;
 
     q = ngx_queue_head(queue);
 
@@ -59,22 +63,44 @@ ngx_queue_sort(ngx_queue_t *queue,
         return;
     }
 
-    for (q = ngx_queue_next(q); q != ngx_queue_sentinel(queue); q = next) {
+    q = ngx_queue_middle(queue);
+
+    ngx_queue_split(queue, q, &tail);
+
+    ngx_queue_sort(queue, cmp);
+    ngx_queue_sort(&tail, cmp);
+
+    ngx_queue_merge(queue, &tail, cmp);
+}
+
 
-        prev = ngx_queue_prev(q);
-        next = ngx_queue_next(q);
+static void
+ngx_queue_merge(ngx_queue_t *queue, ngx_queue_t *tail,
+    ngx_int_t (*cmp)(const ngx_queue_t *, const ngx_queue_t *))
+{
+    ngx_queue_t  *q1, *q2;
 
-        ngx_queue_remove(q);
+    q1 = ngx_queue_head(queue);
+    q2 = ngx_queue_head(tail);
 
-        do {
-            if (cmp(prev, q) <= 0) {
-                break;
-            }
+    for ( ;; ) {
+        if (q1 == ngx_queue_sentinel(queue)) {
+            ngx_queue_add(queue, tail);
+            break;
+        }
 
-            prev = ngx_queue_prev(prev);
+        if (q2 == ngx_queue_sentinel(tail)) {
+            break;
+        }
+
+        if (cmp(q1, q2) <= 0) {
+            q1 = ngx_queue_next(q1);
+            continue;
+        }
 
-        } while (prev != ngx_queue_sentinel(queue));
+        ngx_queue_remove(q2);
+        ngx_queue_insert_before(q1, q2);
 
-        ngx_queue_insert_after(prev, q);
+        q2 = ngx_queue_head(tail);
     }
 }
diff --git a/src/core/ngx_queue.h b/src/core/ngx_queue.h
index 038bf12..0f82f17 100644
--- a/src/core/ngx_queue.h
+++ b/src/core/ngx_queue.h
@@ -47,6 +47,9 @@ struct ngx_queue_s {
     (h)->prev = x
 
 
+#define ngx_queue_insert_before   ngx_queue_insert_tail
+
+
 #define ngx_queue_head(h)                                                     \
     (h)->next
 
diff --git a/src/core/ngx_regex.c b/src/core/ngx_regex.c
index bebf3b6..5b13c5d 100644
--- a/src/core/ngx_regex.c
+++ b/src/core/ngx_regex.c
@@ -600,6 +600,8 @@ ngx_regex_cleanup(void *data)
      * the new cycle, these will be re-allocated.
      */
 
+    ngx_regex_malloc_init(NULL);
+
     if (ngx_regex_compile_context) {
         pcre2_compile_context_free(ngx_regex_compile_context);
         ngx_regex_compile_context = NULL;
@@ -611,6 +613,8 @@ ngx_regex_cleanup(void *data)
         ngx_regex_match_data_size = 0;
     }
 
+    ngx_regex_malloc_done();
+
 #endif
 }
 
@@ -706,9 +710,6 @@ ngx_regex_module_init(ngx_cycle_t *cycle)
     ngx_regex_malloc_done();
 
     ngx_regex_studies = NULL;
-#if (NGX_PCRE2)
-    ngx_regex_compile_context = NULL;
-#endif
 
     return NGX_OK;
 }
@@ -732,14 +733,14 @@ ngx_regex_create_conf(ngx_cycle_t *cycle)
         return NULL;
     }
 
-    cln->handler = ngx_regex_cleanup;
-    cln->data = rcf;
-
     rcf->studies = ngx_list_create(cycle->pool, 8, sizeof(ngx_regex_elt_t));
     if (rcf->studies == NULL) {
         return NULL;
     }
 
+    cln->handler = ngx_regex_cleanup;
+    cln->data = rcf;
+
     ngx_regex_studies = rcf->studies;
 
     return rcf;
diff --git a/src/core/ngx_resolver.c b/src/core/ngx_resolver.c
index 6d129e5..c76c178 100644
--- a/src/core/ngx_resolver.c
+++ b/src/core/ngx_resolver.c
@@ -157,6 +157,8 @@ ngx_resolver_create(ngx_conf_t *cf, ngx_str_t *names, ngx_uint_t n)
     cln->handler = ngx_resolver_cleanup;
     cln->data = r;
 
+    r->ipv4 = 1;
+
     ngx_rbtree_init(&r->name_rbtree, &r->name_sentinel,
                     ngx_resolver_rbtree_insert_value);
 
@@ -225,6 +227,23 @@ ngx_resolver_create(ngx_conf_t *cf, ngx_str_t *names, ngx_uint_t n)
         }
 
 #if (NGX_HAVE_INET6)
+        if (ngx_strncmp(names[i].data, "ipv4=", 5) == 0) {
+
+            if (ngx_strcmp(&names[i].data[5], "on") == 0) {
+                r->ipv4 = 1;
+
+            } else if (ngx_strcmp(&names[i].data[5], "off") == 0) {
+                r->ipv4 = 0;
+
+            } else {
+                ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
+                                   "invalid parameter: %V", &names[i]);
+                return NULL;
+            }
+
+            continue;
+        }
+
         if (ngx_strncmp(names[i].data, "ipv6=", 5) == 0) {
 
             if (ngx_strcmp(&names[i].data[5], "on") == 0) {
@@ -273,6 +292,14 @@ ngx_resolver_create(ngx_conf_t *cf, ngx_str_t *names, ngx_uint_t n)
         }
     }
 
+#if (NGX_HAVE_INET6)
+    if (r->ipv4 + r->ipv6 == 0) {
+        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
+                           "\"ipv4\" and \"ipv6\" cannot both be \"off\"");
+        return NULL;
+    }
+#endif
+
     if (n && r->connections.nelts == 0) {
         ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "no name servers defined");
         return NULL;
@@ -836,7 +863,7 @@ ngx_resolve_name_locked(ngx_resolver_t *r, ngx_resolver_ctx_t *ctx,
         r->last_connection = 0;
     }
 
-    rn->naddrs = (u_short) -1;
+    rn->naddrs = r->ipv4 ? (u_short) -1 : 0;
     rn->tcp = 0;
 #if (NGX_HAVE_INET6)
     rn->naddrs6 = r->ipv6 ? (u_short) -1 : 0;
@@ -1263,7 +1290,7 @@ ngx_resolver_send_query(ngx_resolver_t *r, ngx_resolver_node_t *rn)
         rec->log.action = "resolving";
     }
 
-    if (rn->naddrs == (u_short) -1) {
+    if (rn->query && rn->naddrs == (u_short) -1) {
         rc = rn->tcp ? ngx_resolver_send_tcp_query(r, rec, rn->query, rn->qlen)
                      : ngx_resolver_send_udp_query(r, rec, rn->query, rn->qlen);
 
@@ -1389,6 +1416,7 @@ ngx_resolver_send_tcp_query(ngx_resolver_t *r, ngx_resolver_connection_t *rec,
 
         rec->tcp->data = rec;
         rec->tcp->write->handler = ngx_resolver_tcp_write;
+        rec->tcp->write->cancelable = 1;
         rec->tcp->read->handler = ngx_resolver_tcp_read;
         rec->tcp->read->resolver = 1;
 
@@ -1764,10 +1792,13 @@ ngx_resolver_process_response(ngx_resolver_t *r, u_char *buf, size_t n,
              q = ngx_queue_next(q))
         {
             rn = ngx_queue_data(q, ngx_resolver_node_t, queue);
-            qident = (rn->query[0] << 8) + rn->query[1];
 
-            if (qident == ident) {
-                goto dns_error_name;
+            if (rn->query) {
+                qident = (rn->query[0] << 8) + rn->query[1];
+
+                if (qident == ident) {
+                    goto dns_error_name;
+                }
             }
 
 #if (NGX_HAVE_INET6)
@@ -3644,7 +3675,7 @@ ngx_resolver_create_name_query(ngx_resolver_t *r, ngx_resolver_node_t *rn,
     len = sizeof(ngx_resolver_hdr_t) + nlen + sizeof(ngx_resolver_qs_t);
 
 #if (NGX_HAVE_INET6)
-    p = ngx_resolver_alloc(r, r->ipv6 ? len * 2 : len);
+    p = ngx_resolver_alloc(r, len * (r->ipv4 + r->ipv6));
 #else
     p = ngx_resolver_alloc(r, len);
 #endif
@@ -3657,19 +3688,21 @@ ngx_resolver_create_name_query(ngx_resolver_t *r, ngx_resolver_node_t *rn,
 
 #if (NGX_HAVE_INET6)
     if (r->ipv6) {
-        rn->query6 = p + len;
+        rn->query6 = r->ipv4 ? (p + len) : p;
     }
 #endif
 
     query = (ngx_resolver_hdr_t *) p;
 
-    ident = ngx_random();
+    if (r->ipv4) {
+        ident = ngx_random();
 
-    ngx_log_debug2(NGX_LOG_DEBUG_CORE, r->log, 0,
-                   "resolve: \"%V\" A %i", name, ident & 0xffff);
+        ngx_log_debug2(NGX_LOG_DEBUG_CORE, r->log, 0,
+                       "resolve: \"%V\" A %i", name, ident & 0xffff);
 
-    query->ident_hi = (u_char) ((ident >> 8) & 0xff);
-    query->ident_lo = (u_char) (ident & 0xff);
+        query->ident_hi = (u_char) ((ident >> 8) & 0xff);
+        query->ident_lo = (u_char) (ident & 0xff);
+    }
 
     /* recursion query */
     query->flags_hi = 1; query->flags_lo = 0;
@@ -3730,7 +3763,9 @@ ngx_resolver_create_name_query(ngx_resolver_t *r, ngx_resolver_node_t *rn,
 
     p = rn->query6;
 
-    ngx_memcpy(p, rn->query, rn->qlen);
+    if (r->ipv4) {
+        ngx_memcpy(p, rn->query, rn->qlen);
+    }
 
     query = (ngx_resolver_hdr_t *) p;
 
diff --git a/src/core/ngx_resolver.h b/src/core/ngx_resolver.h
index 0bd3921..06026b7 100644
--- a/src/core/ngx_resolver.h
+++ b/src/core/ngx_resolver.h
@@ -175,8 +175,10 @@ struct ngx_resolver_s {
     ngx_queue_t               srv_expire_queue;
     ngx_queue_t               addr_expire_queue;
 
+    unsigned                  ipv4:1;
+
 #if (NGX_HAVE_INET6)
-    ngx_uint_t                ipv6;                 /* unsigned  ipv6:1; */
+    unsigned                  ipv6:1;
     ngx_rbtree_t              addr6_rbtree;
     ngx_rbtree_node_t         addr6_sentinel;
     ngx_queue_t               addr6_resend_queue;
diff --git a/src/core/ngx_string.c b/src/core/ngx_string.c
index 98f270a..f8f7384 100644
--- a/src/core/ngx_string.c
+++ b/src/core/ngx_string.c
@@ -1364,7 +1364,12 @@ ngx_utf8_decode(u_char **p, size_t n)
 
     u = **p;
 
-    if (u >= 0xf0) {
+    if (u >= 0xf8) {
+
+        (*p)++;
+        return 0xffffffff;
+
+    } else if (u >= 0xf0) {
 
         u &= 0x07;
         valid = 0xffff;
diff --git a/src/core/ngx_string.h b/src/core/ngx_string.h
index 0fb9be7..713eb42 100644
--- a/src/core/ngx_string.h
+++ b/src/core/ngx_string.h
@@ -140,12 +140,12 @@ ngx_copy(u_char *dst, u_char *src, size_t len)
 #endif
 
 
-#define ngx_memmove(dst, src, n)   (void) memmove(dst, src, n)
-#define ngx_movemem(dst, src, n)   (((u_char *) memmove(dst, src, n)) + (n))
+#define ngx_memmove(dst, src, n)  (void) memmove(dst, src, n)
+#define ngx_movemem(dst, src, n)  (((u_char *) memmove(dst, src, n)) + (n))
 
 
 /* msvc and icc7 compile memcmp() to the inline loop */
-#define ngx_memcmp(s1, s2, n)  memcmp((const char *) s1, (const char *) s2, n)
+#define ngx_memcmp(s1, s2, n)     memcmp(s1, s2, n)
 
 
 u_char *ngx_cpystrn(u_char *dst, u_char *src, size_t n);
diff --git a/src/core/ngx_syslog.c b/src/core/ngx_syslog.c
index 3c7b63a..bad45bd 100644
--- a/src/core/ngx_syslog.c
+++ b/src/core/ngx_syslog.c
@@ -18,6 +18,7 @@
 static char *ngx_syslog_parse_args(ngx_conf_t *cf, ngx_syslog_peer_t *peer);
 static ngx_int_t ngx_syslog_init_peer(ngx_syslog_peer_t *peer);
 static void ngx_syslog_cleanup(void *data);
+static u_char *ngx_syslog_log_error(ngx_log_t *log, u_char *buf, size_t len);
 
 
 static char  *facilities[] = {
@@ -66,6 +67,9 @@ ngx_syslog_process_conf(ngx_conf_t *cf, ngx_syslog_peer_t *peer)
         ngx_str_set(&peer->tag, "nginx");
     }
 
+    peer->hostname = &cf->cycle->hostname;
+    peer->logp = &cf->cycle->new_log;
+
     peer->conn.fd = (ngx_socket_t) -1;
 
     peer->conn.read = &ngx_syslog_dummy_event;
@@ -243,7 +247,7 @@ ngx_syslog_add_header(ngx_syslog_peer_t *peer, u_char *buf)
     }
 
     return ngx_sprintf(buf, "<%ui>%V %V %V: ", pri, &ngx_cached_syslog_time,
-                       &ngx_cycle->hostname, &peer->tag);
+                       peer->hostname, &peer->tag);
 }
 
 
@@ -286,15 +290,19 @@ ngx_syslog_send(ngx_syslog_peer_t *peer, u_char *buf, size_t len)
 {
     ssize_t  n;
 
+    if (peer->log.handler == NULL) {
+        peer->log = *peer->logp;
+        peer->log.handler = ngx_syslog_log_error;
+        peer->log.data = peer;
+        peer->log.action = "logging to syslog";
+    }
+
     if (peer->conn.fd == (ngx_socket_t) -1) {
         if (ngx_syslog_init_peer(peer) != NGX_OK) {
             return NGX_ERROR;
         }
     }
 
-    /* log syslog socket events with valid log */
-    peer->conn.log = ngx_cycle->log;
-
     if (ngx_send) {
         n = ngx_send(&peer->conn, buf, len);
 
@@ -306,7 +314,7 @@ ngx_syslog_send(ngx_syslog_peer_t *peer, u_char *buf, size_t len)
     if (n == NGX_ERROR) {
 
         if (ngx_close_socket(peer->conn.fd) == -1) {
-            ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, ngx_socket_errno,
+            ngx_log_error(NGX_LOG_ALERT, &peer->log, ngx_socket_errno,
                           ngx_close_socket_n " failed");
         }
 
@@ -324,24 +332,25 @@ ngx_syslog_init_peer(ngx_syslog_peer_t *peer)
 
     fd = ngx_socket(peer->server.sockaddr->sa_family, SOCK_DGRAM, 0);
     if (fd == (ngx_socket_t) -1) {
-        ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, ngx_socket_errno,
+        ngx_log_error(NGX_LOG_ALERT, &peer->log, ngx_socket_errno,
                       ngx_socket_n " failed");
         return NGX_ERROR;
     }
 
     if (ngx_nonblocking(fd) == -1) {
-        ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, ngx_socket_errno,
+        ngx_log_error(NGX_LOG_ALERT, &peer->log, ngx_socket_errno,
                       ngx_nonblocking_n " failed");
         goto failed;
     }
 
     if (connect(fd, peer->server.sockaddr, peer->server.socklen) == -1) {
-        ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, ngx_socket_errno,
+        ngx_log_error(NGX_LOG_ALERT, &peer->log, ngx_socket_errno,
                       "connect() failed");
         goto failed;
     }
 
     peer->conn.fd = fd;
+    peer->conn.log = &peer->log;
 
     /* UDP sockets are always ready to write */
     peer->conn.write->ready = 1;
@@ -351,7 +360,7 @@ ngx_syslog_init_peer(ngx_syslog_peer_t *peer)
 failed:
 
     if (ngx_close_socket(fd) == -1) {
-        ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, ngx_socket_errno,
+        ngx_log_error(NGX_LOG_ALERT, &peer->log, ngx_socket_errno,
                       ngx_close_socket_n " failed");
     }
 
@@ -372,7 +381,30 @@ ngx_syslog_cleanup(void *data)
     }
 
     if (ngx_close_socket(peer->conn.fd) == -1) {
-        ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, ngx_socket_errno,
+        ngx_log_error(NGX_LOG_ALERT, &peer->log, ngx_socket_errno,
                       ngx_close_socket_n " failed");
     }
 }
+
+
+static u_char *
+ngx_syslog_log_error(ngx_log_t *log, u_char *buf, size_t len)
+{
+    u_char             *p;
+    ngx_syslog_peer_t  *peer;
+
+    p = buf;
+
+    if (log->action) {
+        p = ngx_snprintf(buf, len, " while %s", log->action);
+        len -= p - buf;
+    }
+
+    peer = log->data;
+
+    if (peer) {
+        p = ngx_snprintf(p, len, ", server: %V", &peer->server.name);
+    }
+
+    return p;
+}
diff --git a/src/core/ngx_syslog.h b/src/core/ngx_syslog.h
index 50dcd35..e2d54ac 100644
--- a/src/core/ngx_syslog.h
+++ b/src/core/ngx_syslog.h
@@ -9,14 +9,20 @@
 
 
 typedef struct {
-    ngx_uint_t        facility;
-    ngx_uint_t        severity;
-    ngx_str_t         tag;
-
-    ngx_addr_t        server;
-    ngx_connection_t  conn;
-    unsigned          busy:1;
-    unsigned          nohostname:1;
+    ngx_uint_t         facility;
+    ngx_uint_t         severity;
+    ngx_str_t          tag;
+
+    ngx_str_t         *hostname;
+
+    ngx_addr_t         server;
+    ngx_connection_t   conn;
+
+    ngx_log_t          log;
+    ngx_log_t         *logp;
+
+    unsigned           busy:1;
+    unsigned           nohostname:1;
 } ngx_syslog_peer_t;
 
 
diff --git a/src/event/modules/ngx_iocp_module.c b/src/event/modules/ngx_iocp_module.c
new file mode 100644
index 0000000..dca5ced
--- /dev/null
+++ b/src/event/modules/ngx_iocp_module.c
@@ -0,0 +1,379 @@
+
+/*
+ * Copyright (C) Igor Sysoev
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_event.h>
+#include <ngx_iocp_module.h>
+
+
+static ngx_int_t ngx_iocp_init(ngx_cycle_t *cycle, ngx_msec_t timer);
+static ngx_thread_value_t __stdcall ngx_iocp_timer(void *data);
+static void ngx_iocp_done(ngx_cycle_t *cycle);
+static ngx_int_t ngx_iocp_add_event(ngx_event_t *ev, ngx_int_t event,
+    ngx_uint_t key);
+static ngx_int_t ngx_iocp_del_connection(ngx_connection_t *c, ngx_uint_t flags);
+static ngx_int_t ngx_iocp_process_events(ngx_cycle_t *cycle, ngx_msec_t timer,
+    ngx_uint_t flags);
+static void *ngx_iocp_create_conf(ngx_cycle_t *cycle);
+static char *ngx_iocp_init_conf(ngx_cycle_t *cycle, void *conf);
+
+
+static ngx_str_t      iocp_name = ngx_string("iocp");
+
+static ngx_command_t  ngx_iocp_commands[] = {
+
+    { ngx_string("iocp_threads"),
+      NGX_EVENT_CONF|NGX_CONF_TAKE1,
+      ngx_conf_set_num_slot,
+      0,
+      offsetof(ngx_iocp_conf_t, threads),
+      NULL },
+
+    { ngx_string("post_acceptex"),
+      NGX_EVENT_CONF|NGX_CONF_TAKE1,
+      ngx_conf_set_num_slot,
+      0,
+      offsetof(ngx_iocp_conf_t, post_acceptex),
+      NULL },
+
+    { ngx_string("acceptex_read"),
+      NGX_EVENT_CONF|NGX_CONF_FLAG,
+      ngx_conf_set_flag_slot,
+      0,
+      offsetof(ngx_iocp_conf_t, acceptex_read),
+      NULL },
+
+      ngx_null_command
+};
+
+
+static ngx_event_module_t  ngx_iocp_module_ctx = {
+    &iocp_name,
+    ngx_iocp_create_conf,                  /* create configuration */
+    ngx_iocp_init_conf,                    /* init configuration */
+
+    {
+        ngx_iocp_add_event,                /* add an event */
+        NULL,                              /* delete an event */
+        NULL,                              /* enable an event */
+        NULL,                              /* disable an event */
+        NULL,                              /* add an connection */
+        ngx_iocp_del_connection,           /* delete an connection */
+        NULL,                              /* trigger a notify */
+        ngx_iocp_process_events,           /* process the events */
+        ngx_iocp_init,                     /* init the events */
+        ngx_iocp_done                      /* done the events */
+    }
+
+};
+
+ngx_module_t  ngx_iocp_module = {
+    NGX_MODULE_V1,
+    &ngx_iocp_module_ctx,                  /* module context */
+    ngx_iocp_commands,                     /* module directives */
+    NGX_EVENT_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
+};
+
+
+ngx_os_io_t ngx_iocp_io = {
+    ngx_overlapped_wsarecv,
+    NULL,
+    ngx_udp_overlapped_wsarecv,
+    NULL,
+    NULL,
+    NULL,
+    ngx_overlapped_wsasend_chain,
+    0
+};
+
+
+static HANDLE      iocp;
+static ngx_tid_t   timer_thread;
+static ngx_msec_t  msec;
+
+
+static ngx_int_t
+ngx_iocp_init(ngx_cycle_t *cycle, ngx_msec_t timer)
+{
+    ngx_iocp_conf_t  *cf;
+
+    cf = ngx_event_get_conf(cycle->conf_ctx, ngx_iocp_module);
+
+    if (iocp == NULL) {
+        iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0,
+                                      cf->threads);
+    }
+
+    if (iocp == NULL) {
+        ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
+                      "CreateIoCompletionPort() failed");
+        return NGX_ERROR;
+    }
+
+    ngx_io = ngx_iocp_io;
+
+    ngx_event_actions = ngx_iocp_module_ctx.actions;
+
+    ngx_event_flags = NGX_USE_IOCP_EVENT;
+
+    if (timer == 0) {
+        return NGX_OK;
+    }
+
+    /*
+     * The waitable timer could not be used, because
+     * GetQueuedCompletionStatus() does not set a thread to alertable state
+     */
+
+    if (timer_thread == NULL) {
+
+        msec = timer;
+
+        if (ngx_create_thread(&timer_thread, ngx_iocp_timer, &msec, cycle->log)
+            != 0)
+        {
+            return NGX_ERROR;
+        }
+    }
+
+    ngx_event_flags |= NGX_USE_TIMER_EVENT;
+
+    return NGX_OK;
+}
+
+
+static ngx_thread_value_t __stdcall
+ngx_iocp_timer(void *data)
+{
+    ngx_msec_t  timer = *(ngx_msec_t *) data;
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, ngx_cycle->log, 0,
+                   "THREAD %p %p", &msec, data);
+
+    for ( ;; ) {
+        Sleep(timer);
+
+        ngx_time_update();
+#if 1
+        ngx_log_debug0(NGX_LOG_DEBUG_EVENT, ngx_cycle->log, 0, "timer");
+#endif
+    }
+
+#if defined(__WATCOMC__) || defined(__GNUC__)
+    return 0;
+#endif
+}
+
+
+static void
+ngx_iocp_done(ngx_cycle_t *cycle)
+{
+    if (CloseHandle(iocp) == -1) {
+        ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
+                      "iocp CloseHandle() failed");
+    }
+
+    iocp = NULL;
+}
+
+
+static ngx_int_t
+ngx_iocp_add_event(ngx_event_t *ev, ngx_int_t event, ngx_uint_t key)
+{
+    ngx_connection_t  *c;
+
+    c = (ngx_connection_t *) ev->data;
+
+    c->read->active = 1;
+    c->write->active = 1;
+
+    ngx_log_debug3(NGX_LOG_DEBUG_EVENT, ev->log, 0,
+                   "iocp add: fd:%d k:%ui ov:%p", c->fd, key, &ev->ovlp);
+
+    if (CreateIoCompletionPort((HANDLE) c->fd, iocp, key, 0) == NULL) {
+        ngx_log_error(NGX_LOG_ALERT, c->log, ngx_errno,
+                      "CreateIoCompletionPort() failed");
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_iocp_del_connection(ngx_connection_t *c, ngx_uint_t flags)
+{
+#if 0
+    if (flags & NGX_CLOSE_EVENT) {
+        return NGX_OK;
+    }
+
+    if (CancelIo((HANDLE) c->fd) == 0) {
+        ngx_log_error(NGX_LOG_ALERT, c->log, ngx_errno, "CancelIo() failed");
+        return NGX_ERROR;
+    }
+#endif
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_iocp_process_events(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags)
+{
+    int                rc;
+    u_int              key;
+    u_long             bytes;
+    ngx_err_t          err;
+    ngx_msec_t         delta;
+    ngx_event_t       *ev;
+    ngx_event_ovlp_t  *ovlp;
+
+    if (timer == NGX_TIMER_INFINITE) {
+        timer = INFINITE;
+    }
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0, "iocp timer: %M", timer);
+
+    rc = GetQueuedCompletionStatus(iocp, &bytes, (PULONG_PTR) &key,
+                                   (LPOVERLAPPED *) &ovlp, (u_long) timer);
+
+    if (rc == 0) {
+        err = ngx_errno;
+    } else {
+        err = 0;
+    }
+
+    delta = ngx_current_msec;
+
+    if (flags & NGX_UPDATE_TIME) {
+        ngx_time_update();
+    }
+
+    ngx_log_debug4(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
+                   "iocp: %d b:%d k:%d ov:%p", rc, bytes, key, ovlp);
+
+    if (timer != INFINITE) {
+        delta = ngx_current_msec - delta;
+
+        ngx_log_debug2(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
+                       "iocp timer: %M, delta: %M", timer, delta);
+    }
+
+    if (err) {
+        if (ovlp == NULL) {
+            if (err != WAIT_TIMEOUT) {
+                ngx_log_error(NGX_LOG_ALERT, cycle->log, err,
+                              "GetQueuedCompletionStatus() failed");
+
+                return NGX_ERROR;
+            }
+
+            return NGX_OK;
+        }
+
+        ovlp->error = err;
+    }
+
+    if (ovlp == NULL) {
+        ngx_log_error(NGX_LOG_ALERT, cycle->log, 0,
+                      "GetQueuedCompletionStatus() returned no operation");
+        return NGX_ERROR;
+    }
+
+
+    ev = ovlp->event;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, err, "iocp event:%p", ev);
+
+
+    if (err == ERROR_NETNAME_DELETED /* the socket was closed */
+        || err == ERROR_OPERATION_ABORTED /* the operation was canceled */)
+    {
+
+        /*
+         * the WSA_OPERATION_ABORTED completion notification
+         * for a file descriptor that was closed
+         */
+
+        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, err,
+                       "iocp: aborted event %p", ev);
+
+        return NGX_OK;
+    }
+
+    if (err) {
+        ngx_log_error(NGX_LOG_ALERT, cycle->log, err,
+                      "GetQueuedCompletionStatus() returned operation error");
+    }
+
+    switch (key) {
+
+    case NGX_IOCP_ACCEPT:
+        if (bytes) {
+            ev->ready = 1;
+        }
+        break;
+
+    case NGX_IOCP_IO:
+        ev->complete = 1;
+        ev->ready = 1;
+        break;
+
+    case NGX_IOCP_CONNECT:
+        ev->ready = 1;
+    }
+
+    ev->available = bytes;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
+                   "iocp event handler: %p", ev->handler);
+
+    ev->handler(ev);
+
+    return NGX_OK;
+}
+
+
+static void *
+ngx_iocp_create_conf(ngx_cycle_t *cycle)
+{
+    ngx_iocp_conf_t  *cf;
+
+    cf = ngx_palloc(cycle->pool, sizeof(ngx_iocp_conf_t));
+    if (cf == NULL) {
+        return NULL;
+    }
+
+    cf->threads = NGX_CONF_UNSET;
+    cf->post_acceptex = NGX_CONF_UNSET;
+    cf->acceptex_read = NGX_CONF_UNSET;
+
+    return cf;
+}
+
+
+static char *
+ngx_iocp_init_conf(ngx_cycle_t *cycle, void *conf)
+{
+    ngx_iocp_conf_t *cf = conf;
+
+    ngx_conf_init_value(cf->threads, 0);
+    ngx_conf_init_value(cf->post_acceptex, 10);
+    ngx_conf_init_value(cf->acceptex_read, 1);
+
+    return NGX_CONF_OK;
+}
diff --git a/src/event/modules/ngx_iocp_module.h b/src/event/modules/ngx_iocp_module.h
new file mode 100644
index 0000000..dc73983
--- /dev/null
+++ b/src/event/modules/ngx_iocp_module.h
@@ -0,0 +1,22 @@
+
+/*
+ * Copyright (C) Igor Sysoev
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_IOCP_MODULE_H_INCLUDED_
+#define _NGX_IOCP_MODULE_H_INCLUDED_
+
+
+typedef struct {
+    int  threads;
+    int  post_acceptex;
+    int  acceptex_read;
+} ngx_iocp_conf_t;
+
+
+extern ngx_module_t  ngx_iocp_module;
+
+
+#endif /* _NGX_IOCP_MODULE_H_INCLUDED_ */
diff --git a/src/event/ngx_event.c b/src/event/ngx_event.c
index 47229b5..ef525d9 100644
--- a/src/event/ngx_event.c
+++ b/src/event/ngx_event.c
@@ -267,6 +267,18 @@ ngx_process_events_and_timers(ngx_cycle_t *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) {
+        return NGX_OK;
+    }
+
+#endif
+
     if (ngx_event_flags & NGX_USE_CLEAR_EVENT) {
 
         /* kqueue, epoll */
@@ -337,9 +349,15 @@ ngx_handle_write_event(ngx_event_t *wev, size_t lowat)
 {
     ngx_connection_t  *c;
 
-    if (lowat) {
-        c = wev->data;
+    c = wev->data;
 
+#if (NGX_QUIC)
+    if (c->quic) {
+        return NGX_OK;
+    }
+#endif
+
+    if (lowat) {
         if (ngx_send_lowat(c, lowat) == NGX_ERROR) {
             return NGX_ERROR;
         }
@@ -416,6 +434,7 @@ ngx_event_init_conf(ngx_cycle_t *cycle, void *conf)
 {
 #if (NGX_HAVE_REUSEPORT)
     ngx_uint_t        i;
+    ngx_core_conf_t  *ccf;
     ngx_listening_t  *ls;
 #endif
 
@@ -442,7 +461,9 @@ ngx_event_init_conf(ngx_cycle_t *cycle, void *conf)
 
 #if (NGX_HAVE_REUSEPORT)
 
-    if (!ngx_test_config) {
+    ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module);
+
+    if (!ngx_test_config && ccf->master) {
 
         ls = cycle->listening.elts;
         for (i = 0; i < cycle->listening.nelts; i++) {
@@ -810,7 +831,9 @@ ngx_event_process_init(ngx_cycle_t *cycle)
         rev->deferred_accept = ls[i].deferred_accept;
 #endif
 
-        if (!(ngx_event_flags & NGX_USE_IOCP_EVENT)) {
+        if (!(ngx_event_flags & NGX_USE_IOCP_EVENT)
+            && cycle->old_cycle)
+        {
             if (ls[i].previous) {
 
                 /*
@@ -868,8 +891,16 @@ ngx_event_process_init(ngx_cycle_t *cycle)
 
 #else
 
-        rev->handler = (c->type == SOCK_STREAM) ? ngx_event_accept
-                                                : ngx_event_recvmsg;
+        if (c->type == SOCK_STREAM) {
+            rev->handler = ngx_event_accept;
+
+#if (NGX_QUIC)
+        } else if (ls[i].quic) {
+            rev->handler = ngx_quic_recvmsg;
+#endif
+        } else {
+            rev->handler = ngx_event_recvmsg;
+        }
 
 #if (NGX_HAVE_REUSEPORT)
 
diff --git a/src/event/ngx_event.h b/src/event/ngx_event.h
index 548c906..deac04e 100644
--- a/src/event/ngx_event.h
+++ b/src/event/ngx_event.h
@@ -494,12 +494,6 @@ extern ngx_module_t           ngx_event_core_module;
 
 
 void ngx_event_accept(ngx_event_t *ev);
-#if !(NGX_WIN32)
-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);
-#endif
-void ngx_delete_udp_connection(void *data);
 ngx_int_t ngx_trylock_accept_mutex(ngx_cycle_t *cycle);
 ngx_int_t ngx_enable_accept_events(ngx_cycle_t *cycle);
 u_char *ngx_accept_log_error(ngx_log_t *log, u_char *buf, size_t len);
@@ -529,6 +523,7 @@ ngx_int_t ngx_send_lowat(ngx_connection_t *c, size_t lowat);
 
 #include <ngx_event_timer.h>
 #include <ngx_event_posted.h>
+#include <ngx_event_udp.h>
 
 #if (NGX_WIN32)
 #include <ngx_iocp_module.h>
diff --git a/src/event/ngx_event_acceptex.c b/src/event/ngx_event_acceptex.c
new file mode 100644
index 0000000..f4a1c4b
--- /dev/null
+++ b/src/event/ngx_event_acceptex.c
@@ -0,0 +1,229 @@
+
+/*
+ * Copyright (C) Igor Sysoev
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_event.h>
+
+
+static void ngx_close_posted_connection(ngx_connection_t *c);
+
+
+void
+ngx_event_acceptex(ngx_event_t *rev)
+{
+    ngx_listening_t   *ls;
+    ngx_connection_t  *c;
+
+    c = rev->data;
+    ls = c->listening;
+
+    c->log->handler = ngx_accept_log_error;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, "AcceptEx: %d", c->fd);
+
+    if (rev->ovlp.error) {
+        ngx_log_error(NGX_LOG_CRIT, c->log, rev->ovlp.error,
+                      "AcceptEx() %V failed", &ls->addr_text);
+        return;
+    }
+
+    /* SO_UPDATE_ACCEPT_CONTEXT is required for shutdown() to work */
+
+    if (setsockopt(c->fd, SOL_SOCKET, SO_UPDATE_ACCEPT_CONTEXT,
+                   (char *) &ls->fd, sizeof(ngx_socket_t))
+        == -1)
+    {
+        ngx_log_error(NGX_LOG_CRIT, c->log, ngx_socket_errno,
+                      "setsockopt(SO_UPDATE_ACCEPT_CONTEXT) failed for %V",
+                      &c->addr_text);
+        /* TODO: close socket */
+        return;
+    }
+
+    ngx_getacceptexsockaddrs(c->buffer->pos,
+                             ls->post_accept_buffer_size,
+                             ls->socklen + 16,
+                             ls->socklen + 16,
+                             &c->local_sockaddr, &c->local_socklen,
+                             &c->sockaddr, &c->socklen);
+
+    if (ls->post_accept_buffer_size) {
+        c->buffer->last += rev->available;
+        c->buffer->end = c->buffer->start + ls->post_accept_buffer_size;
+
+    } else {
+        c->buffer = NULL;
+    }
+
+    if (ls->addr_ntop) {
+        c->addr_text.data = ngx_pnalloc(c->pool, ls->addr_text_max_len);
+        if (c->addr_text.data == NULL) {
+            /* TODO: close socket */
+            return;
+        }
+
+        c->addr_text.len = ngx_sock_ntop(c->sockaddr, c->socklen,
+                                         c->addr_text.data,
+                                         ls->addr_text_max_len, 0);
+        if (c->addr_text.len == 0) {
+            /* TODO: close socket */
+            return;
+        }
+    }
+
+    ngx_event_post_acceptex(ls, 1);
+
+    c->number = ngx_atomic_fetch_add(ngx_connection_counter, 1);
+
+    c->start_time = ngx_current_msec;
+
+    ls->handler(c);
+
+    return;
+
+}
+
+
+ngx_int_t
+ngx_event_post_acceptex(ngx_listening_t *ls, ngx_uint_t n)
+{
+    u_long             rcvd;
+    ngx_err_t          err;
+    ngx_log_t         *log;
+    ngx_uint_t         i;
+    ngx_event_t       *rev, *wev;
+    ngx_socket_t       s;
+    ngx_connection_t  *c;
+
+    for (i = 0; i < n; i++) {
+
+        /* TODO: look up reused sockets */
+
+        s = ngx_socket(ls->sockaddr->sa_family, ls->type, 0);
+
+        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, &ls->log, 0,
+                       ngx_socket_n " s:%d", s);
+
+        if (s == (ngx_socket_t) -1) {
+            ngx_log_error(NGX_LOG_ALERT, &ls->log, ngx_socket_errno,
+                          ngx_socket_n " failed");
+
+            return NGX_ERROR;
+        }
+
+        c = ngx_get_connection(s, &ls->log);
+
+        if (c == NULL) {
+            return NGX_ERROR;
+        }
+
+        c->pool = ngx_create_pool(ls->pool_size, &ls->log);
+        if (c->pool == NULL) {
+            ngx_close_posted_connection(c);
+            return NGX_ERROR;
+        }
+
+        log = ngx_palloc(c->pool, sizeof(ngx_log_t));
+        if (log == NULL) {
+            ngx_close_posted_connection(c);
+            return NGX_ERROR;
+        }
+
+        c->buffer = ngx_create_temp_buf(c->pool, ls->post_accept_buffer_size
+                                                 + 2 * (ls->socklen + 16));
+        if (c->buffer == NULL) {
+            ngx_close_posted_connection(c);
+            return NGX_ERROR;
+        }
+
+        c->local_sockaddr = ngx_palloc(c->pool, ls->socklen);
+        if (c->local_sockaddr == NULL) {
+            ngx_close_posted_connection(c);
+            return NGX_ERROR;
+        }
+
+        c->sockaddr = ngx_palloc(c->pool, ls->socklen);
+        if (c->sockaddr == NULL) {
+            ngx_close_posted_connection(c);
+            return NGX_ERROR;
+        }
+
+        *log = ls->log;
+        c->log = log;
+
+        c->recv = ngx_recv;
+        c->send = ngx_send;
+        c->recv_chain = ngx_recv_chain;
+        c->send_chain = ngx_send_chain;
+
+        c->listening = ls;
+
+        rev = c->read;
+        wev = c->write;
+
+        rev->ovlp.event = rev;
+        wev->ovlp.event = wev;
+        rev->handler = ngx_event_acceptex;
+
+        rev->ready = 1;
+        wev->ready = 1;
+
+        rev->log = c->log;
+        wev->log = c->log;
+
+        if (ngx_add_event(rev, 0, NGX_IOCP_IO) == NGX_ERROR) {
+            ngx_close_posted_connection(c);
+            return NGX_ERROR;
+        }
+
+        if (ngx_acceptex(ls->fd, s, c->buffer->pos, ls->post_accept_buffer_size,
+                         ls->socklen + 16, ls->socklen + 16,
+                         &rcvd, (LPOVERLAPPED) &rev->ovlp)
+            == 0)
+        {
+            err = ngx_socket_errno;
+            if (err != WSA_IO_PENDING) {
+                ngx_log_error(NGX_LOG_ALERT, &ls->log, err,
+                              "AcceptEx() %V failed", &ls->addr_text);
+
+                ngx_close_posted_connection(c);
+                return NGX_ERROR;
+            }
+        }
+    }
+
+    return NGX_OK;
+}
+
+
+static void
+ngx_close_posted_connection(ngx_connection_t *c)
+{
+    ngx_socket_t  fd;
+
+    ngx_free_connection(c);
+
+    fd = c->fd;
+    c->fd = (ngx_socket_t) -1;
+
+    if (ngx_close_socket(fd) == -1) {
+        ngx_log_error(NGX_LOG_ALERT, c->log, ngx_socket_errno,
+                      ngx_close_socket_n " failed");
+    }
+
+    if (c->pool) {
+        ngx_destroy_pool(c->pool);
+    }
+}
+
+
+u_char *
+ngx_acceptex_log_error(ngx_log_t *log, u_char *buf, size_t len)
+{
+    return ngx_snprintf(buf, len, " while posting AcceptEx() on %V", log->data);
+}
diff --git a/src/event/ngx_event_connect.c b/src/event/ngx_event_connect.c
index adbbde6..668084a 100644
--- a/src/event/ngx_event_connect.c
+++ b/src/event/ngx_event_connect.c
@@ -179,6 +179,8 @@ ngx_event_connect_peer(ngx_peer_connection_t *pc)
         c->recv = ngx_udp_recv;
         c->send = ngx_send;
         c->send_chain = ngx_udp_send_chain;
+
+        c->need_flush_buf = 1;
     }
 
     c->log_error = pc->log_error;
diff --git a/src/event/ngx_event_connectex.c b/src/event/ngx_event_connectex.c
new file mode 100644
index 0000000..36b1514
--- /dev/null
+++ b/src/event/ngx_event_connectex.c
@@ -0,0 +1,206 @@
+
+/*
+ * Copyright (C) Igor Sysoev
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_event.h>
+
+
+#define NGX_MAX_PENDING_CONN  10
+
+
+static CRITICAL_SECTION  connect_lock;
+static int               nconnects;
+static ngx_connection_t  pending_connects[NGX_MAX_PENDING_CONN];
+
+static HANDLE            pending_connect_event;
+
+__declspec(thread) int                nevents = 0;
+__declspec(thread) WSAEVENT           events[WSA_MAXIMUM_WAIT_EVENTS + 1];
+__declspec(thread) ngx_connection_t  *conn[WSA_MAXIMUM_WAIT_EVENTS + 1];
+
+
+
+int ngx_iocp_wait_connect(ngx_connection_t *c)
+{
+    for ( ;; ) {
+        EnterCriticalSection(&connect_lock);
+
+        if (nconnects < NGX_MAX_PENDING_CONN) {
+            pending_connects[--nconnects] = c;
+            LeaveCriticalSection(&connect_lock);
+
+            if (SetEvent(pending_connect_event) == 0) {
+                ngx_log_error(NGX_LOG_ALERT, c->log, ngx_errno,
+                              "SetEvent() failed");
+                return NGX_ERROR;
+
+            break;
+        }
+
+        LeaveCriticalSection(&connect_lock);
+        ngx_log_error(NGX_LOG_NOTICE, c->log, 0,
+                      "max number of pending connect()s is %d",
+                      NGX_MAX_PENDING_CONN);
+        msleep(100);
+    }
+
+    if (!started) {
+        if (ngx_iocp_new_thread(1) == NGX_ERROR) {
+            return NGX_ERROR;
+        }
+        started = 1;
+    }
+
+    return NGX_OK;
+}
+
+
+int ngx_iocp_new_thread(int main)
+{
+    u_int  id;
+
+    if (main) {
+        pending_connect_event = CreateEvent(NULL, 0, 1, NULL);
+        if (pending_connect_event == INVALID_HANDLE_VALUE) {
+            ngx_log_error(NGX_LOG_ALERT, c->log, ngx_errno,
+                          "CreateThread() failed");
+            return NGX_ERROR;
+        }
+    }
+
+    if (CreateThread(NULL, 0, ngx_iocp_wait_events, main, 0, &id)
+                                                       == INVALID_HANDLE_VALUE)
+    {
+        ngx_log_error(NGX_LOG_ALERT, c->log, ngx_errno,
+                      "CreateThread() failed");
+        return NGX_ERROR;
+    }
+
+    SetEvent(event) {
+        ngx_log_error(NGX_LOG_ALERT, c->log, ngx_errno,
+                      "SetEvent() failed");
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}
+
+
+int ngx_iocp_new_connect()
+{
+    EnterCriticalSection(&connect_lock);
+    c = pending_connects[--nconnects];
+    LeaveCriticalSection(&connect_lock);
+
+    conn[nevents] = c;
+
+    events[nevents] = WSACreateEvent();
+    if (events[nevents] == INVALID_HANDLE_VALUE) {
+        ngx_log_error(NGX_LOG_ALERT, c->log, ngx_socket_errno,
+                      "WSACreateEvent() failed");
+        return NGX_ERROR;
+    }
+
+    if (WSAEventSelect(c->fd, events[nevents], FD_CONNECT) == -1)
+        ngx_log_error(NGX_LOG_ALERT, c->log, ngx_socket_errno,
+                      "WSAEventSelect() failed");
+        return NGX_ERROR;
+    }
+
+    nevents++;
+
+    return NGX_OK;
+}
+
+
+void ngx_iocp_wait_events(int main)
+{
+    WSANETWORKEVENTS  ne;
+
+    nevents = 1;
+    events[0] = pending_connect_event;
+    conn[0] = NULL;
+
+    for ( ;; ) {
+        offset = (nevents == WSA_MAXIMUM_WAIT_EVENTS + 1) ? 1 : 0;
+        timeout = (nevents == 1 && !first) ? 60000 : INFINITE;
+
+        n = WSAWaitForMultipleEvents(nevents - offset, events[offset],
+                                     0, timeout, 0);
+        if (n == WAIT_FAILED) {
+            ngx_log_error(NGX_LOG_ALERT, log, ngx_socket_errno,
+                          "WSAWaitForMultipleEvents() failed");
+            continue;
+        }
+
+        if (n == WAIT_TIMEOUT) {
+            if (nevents == 2 && !main) {
+                ExitThread(0);
+            }
+
+            ngx_log_error(NGX_LOG_ALERT, log, 0,
+                          "WSAWaitForMultipleEvents() "
+                          "returned unexpected WAIT_TIMEOUT");
+            continue;
+        }
+
+        n -= WSA_WAIT_EVENT_0;
+
+        if (events[n] == NULL) {
+
+            /* the pending_connect_event */
+
+            if (nevents == WSA_MAXIMUM_WAIT_EVENTS) {
+                ngx_iocp_new_thread(0);
+            } else {
+                ngx_iocp_new_connect();
+            }
+
+            continue;
+        }
+
+        if (WSAEnumNetworkEvents(c[n].fd, events[n], &ne) == -1) {
+            ngx_log_error(NGX_LOG_ALERT, log, ngx_socket_errno,
+                          "WSAEnumNetworkEvents() failed");
+            continue;
+        }
+
+        if (ne.lNetworkEvents & FD_CONNECT) {
+            conn[n].write->ovlp.error = ne.iErrorCode[FD_CONNECT_BIT];
+
+            if (PostQueuedCompletionStatus(iocp, 0, NGX_IOCP_CONNECT,
+                                           &conn[n].write->ovlp) == 0)
+            {
+                ngx_log_error(NGX_LOG_ALERT, log, ngx_socket_errno,
+                              "PostQueuedCompletionStatus() failed");
+                continue;
+            }
+
+            if (n < nevents) {
+                conn[n] = conn[nevents];
+                events[n] = events[nevents];
+            }
+
+            nevents--;
+            continue;
+        }
+
+        if (ne.lNetworkEvents & FD_ACCEPT) {
+
+            /* CHECK ERROR ??? */
+
+            ngx_event_post_acceptex(conn[n].listening, 1);
+            continue;
+        }
+
+        ngx_log_error(NGX_LOG_ALERT, c[n].log, 0,
+                      "WSAWaitForMultipleEvents() "
+                      "returned unexpected network event %ul",
+                      ne.lNetworkEvents);
+    }
+}
diff --git a/src/event/ngx_event_openssl.c b/src/event/ngx_event_openssl.c
index 1e6fc96..89f277f 100644
--- a/src/event/ngx_event_openssl.c
+++ b/src/event/ngx_event_openssl.c
@@ -33,9 +33,6 @@ static int ngx_ssl_new_client_session(ngx_ssl_conn_t *ssl_conn,
 #ifdef SSL_READ_EARLY_DATA_SUCCESS
 static ngx_int_t ngx_ssl_try_early_data(ngx_connection_t *c);
 #endif
-#if (NGX_DEBUG)
-static void ngx_ssl_handshake_log(ngx_connection_t *c);
-#endif
 static void ngx_ssl_handshake_handler(ngx_event_t *ev);
 #ifdef SSL_READ_EARLY_DATA_SUCCESS
 static ssize_t ngx_ssl_recv_early(ngx_connection_t *c, u_char *buf,
@@ -71,10 +68,11 @@ static void ngx_ssl_session_rbtree_insert_value(ngx_rbtree_node_t *temp,
     ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel);
 
 #ifdef SSL_CTRL_SET_TLSEXT_TICKET_KEY_CB
-static int ngx_ssl_session_ticket_key_callback(ngx_ssl_conn_t *ssl_conn,
+static int ngx_ssl_ticket_key_callback(ngx_ssl_conn_t *ssl_conn,
     unsigned char *name, unsigned char *iv, EVP_CIPHER_CTX *ectx,
     HMAC_CTX *hctx, int enc);
-static void ngx_ssl_session_ticket_keys_cleanup(void *data);
+static ngx_int_t ngx_ssl_rotate_ticket_keys(SSL_CTX *ssl_ctx, ngx_log_t *log);
+static void ngx_ssl_ticket_keys_cleanup(void *data);
 #endif
 
 #ifndef X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT
@@ -131,7 +129,7 @@ ngx_module_t  ngx_openssl_module = {
 int  ngx_ssl_connection_index;
 int  ngx_ssl_server_conf_index;
 int  ngx_ssl_session_cache_index;
-int  ngx_ssl_session_ticket_keys_index;
+int  ngx_ssl_ticket_keys_index;
 int  ngx_ssl_ocsp_index;
 int  ngx_ssl_certificate_index;
 int  ngx_ssl_next_certificate_index;
@@ -142,13 +140,42 @@ int  ngx_ssl_stapling_index;
 ngx_int_t
 ngx_ssl_init(ngx_log_t *log)
 {
-#if OPENSSL_VERSION_NUMBER >= 0x10100003L
+#if (OPENSSL_INIT_LOAD_CONFIG && !defined LIBRESSL_VERSION_NUMBER)
+
+    uint64_t                opts;
+    OPENSSL_INIT_SETTINGS  *init;
+
+    opts = OPENSSL_INIT_LOAD_CONFIG;
+
+#if (NGX_OPENSSL_NO_CONFIG)
+
+    if (getenv("OPENSSL_CONF") == NULL) {
+        opts = OPENSSL_INIT_NO_LOAD_CONFIG;
+    }
+
+#endif
+
+    init = OPENSSL_INIT_new();
+    if (init == NULL) {
+        ngx_ssl_error(NGX_LOG_ALERT, log, 0, "OPENSSL_INIT_new() failed");
+        return NGX_ERROR;
+    }
+
+#ifndef OPENSSL_NO_STDIO
+    if (OPENSSL_INIT_set_config_appname(init, "nginx") == 0) {
+        ngx_ssl_error(NGX_LOG_ALERT, log, 0,
+                      "OPENSSL_INIT_set_config_appname() failed");
+        return NGX_ERROR;
+    }
+#endif
 
-    if (OPENSSL_init_ssl(OPENSSL_INIT_LOAD_CONFIG, NULL) == 0) {
+    if (OPENSSL_init_ssl(opts, init) == 0) {
         ngx_ssl_error(NGX_LOG_ALERT, log, 0, "OPENSSL_init_ssl() failed");
         return NGX_ERROR;
     }
 
+    OPENSSL_INIT_free(init);
+
     /*
      * OPENSSL_init_ssl() may leave errors in the error queue
      * while returning success
@@ -158,7 +185,15 @@ ngx_ssl_init(ngx_log_t *log)
 
 #else
 
-    OPENSSL_config(NULL);
+#if (NGX_OPENSSL_NO_CONFIG)
+
+    if (getenv("OPENSSL_CONF") == NULL) {
+        OPENSSL_no_config();
+    }
+
+#endif
+
+    OPENSSL_config("nginx");
 
     SSL_library_init();
     SSL_load_error_strings();
@@ -208,9 +243,9 @@ ngx_ssl_init(ngx_log_t *log)
         return NGX_ERROR;
     }
 
-    ngx_ssl_session_ticket_keys_index = SSL_CTX_get_ex_new_index(0, NULL, NULL,
-                                                                 NULL, NULL);
-    if (ngx_ssl_session_ticket_keys_index == -1) {
+    ngx_ssl_ticket_keys_index = SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL,
+                                                         NULL);
+    if (ngx_ssl_ticket_keys_index == -1) {
         ngx_ssl_error(NGX_LOG_ALERT, log, 0,
                       "SSL_CTX_get_ex_new_index() failed");
         return NGX_ERROR;
@@ -1070,7 +1105,8 @@ ngx_ssl_info_callback(const ngx_ssl_conn_t *ssl_conn, int where, int ret)
     BIO               *rbio, *wbio;
     ngx_connection_t  *c;
 
-#ifndef SSL_OP_NO_RENEGOTIATION
+#if (!defined SSL_OP_NO_RENEGOTIATION                                         \
+     && !defined SSL_OP_NO_CLIENT_RENEGOTIATION)
 
     if ((where & SSL_CB_HANDSHAKE_START)
         && SSL_is_server((ngx_ssl_conn_t *) ssl_conn))
@@ -1083,6 +1119,53 @@ ngx_ssl_info_callback(const ngx_ssl_conn_t *ssl_conn, int where, int ret)
         }
     }
 
+#endif
+
+#ifdef TLS1_3_VERSION
+
+    if ((where & SSL_CB_ACCEPT_LOOP) == SSL_CB_ACCEPT_LOOP
+        && SSL_version(ssl_conn) == TLS1_3_VERSION)
+    {
+        time_t        now, time, timeout, conf_timeout;
+        SSL_SESSION  *sess;
+
+        /*
+         * OpenSSL with TLSv1.3 updates the session creation time on
+         * session resumption and keeps the session timeout unmodified,
+         * making it possible to maintain the session forever, bypassing
+         * client certificate expiration and revocation.  To make sure
+         * session timeouts are actually used, we now update the session
+         * creation time and reduce the session timeout accordingly.
+         *
+         * BoringSSL with TLSv1.3 ignores configured session timeouts
+         * and uses a hardcoded timeout instead, 7 days.  So we update
+         * session timeout to the configured value as soon as a session
+         * is created.
+         */
+
+        c = ngx_ssl_get_connection((ngx_ssl_conn_t *) ssl_conn);
+        sess = SSL_get0_session(ssl_conn);
+
+        if (!c->ssl->session_timeout_set && sess) {
+            c->ssl->session_timeout_set = 1;
+
+            now = ngx_time();
+            time = SSL_SESSION_get_time(sess);
+            timeout = SSL_SESSION_get_timeout(sess);
+            conf_timeout = SSL_CTX_get_timeout(c->ssl->session_ctx);
+
+            timeout = ngx_min(timeout, conf_timeout);
+
+            if (now - time >= timeout) {
+                SSL_SESSION_set1_id_context(sess, (unsigned char *) "", 0);
+
+            } else {
+                SSL_SESSION_set_time(sess, now);
+                SSL_SESSION_set_timeout(sess, timeout - (now - time));
+            }
+        }
+    }
+
 #endif
 
     if ((where & SSL_CB_ACCEPT_LOOP) == SSL_CB_ACCEPT_LOOP) {
@@ -1426,9 +1509,9 @@ ngx_ssl_ecdh_curve(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_str_t *name)
 
     SSL_CTX_set_options(ssl->ctx, SSL_OP_SINGLE_ECDH_USE);
 
-#if SSL_CTRL_SET_ECDH_AUTO
+#ifdef SSL_CTRL_SET_ECDH_AUTO
     /* not needed in OpenSSL 1.1.0+ */
-    SSL_CTX_set_ecdh_auto(ssl->ctx, 1);
+    (void) SSL_CTX_set_ecdh_auto(ssl->ctx, 1);
 #endif
 
     if (ngx_strcmp(name->data, "auto") == 0) {
@@ -1756,9 +1839,10 @@ ngx_ssl_handshake(ngx_connection_t *c)
         c->read->ready = 1;
         c->write->ready = 1;
 
-#ifndef SSL_OP_NO_RENEGOTIATION
-#if OPENSSL_VERSION_NUMBER < 0x10100000L
-#ifdef SSL3_FLAGS_NO_RENEGOTIATE_CIPHERS
+#if (!defined SSL_OP_NO_RENEGOTIATION                                         \
+     && !defined SSL_OP_NO_CLIENT_RENEGOTIATION                               \
+     && defined SSL3_FLAGS_NO_RENEGOTIATE_CIPHERS                             \
+     && OPENSSL_VERSION_NUMBER < 0x10100000L)
 
         /* initial handshake done, disable renegotiation (CVE-2009-3555) */
         if (c->ssl->connection->s3 && SSL_is_server(c->ssl->connection)) {
@@ -1766,10 +1850,8 @@ ngx_ssl_handshake(ngx_connection_t *c)
         }
 
 #endif
-#endif
-#endif
 
-#ifdef BIO_get_ktls_send
+#if (defined BIO_get_ktls_send && !NGX_WIN32)
 
         if (BIO_get_ktls_send(SSL_get_wbio(c->ssl->connection)) == 1) {
             ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
@@ -1914,7 +1996,7 @@ ngx_ssl_try_early_data(ngx_connection_t *c)
         c->read->ready = 1;
         c->write->ready = 1;
 
-#ifdef BIO_get_ktls_send
+#if (defined BIO_get_ktls_send && !NGX_WIN32)
 
         if (BIO_get_ktls_send(SSL_get_wbio(c->ssl->connection)) == 1) {
             ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
@@ -2004,7 +2086,7 @@ ngx_ssl_try_early_data(ngx_connection_t *c)
 
 #if (NGX_DEBUG)
 
-static void
+void
 ngx_ssl_handshake_log(ngx_connection_t *c)
 {
     char         buf[129], *s, *d;
@@ -2156,6 +2238,7 @@ ngx_ssl_recv(ngx_connection_t *c, u_char *buf, size_t size)
 #endif
 
     if (c->ssl->last == NGX_ERROR) {
+        c->read->ready = 0;
         c->read->error = 1;
         return NGX_ERROR;
     }
@@ -2222,6 +2305,7 @@ ngx_ssl_recv(ngx_connection_t *c, u_char *buf, size_t size)
 #if (NGX_HAVE_FIONREAD)
 
                     if (ngx_socket_nread(c->fd, &c->read->available) == -1) {
+                        c->read->ready = 0;
                         c->read->error = 1;
                         ngx_connection_error(c, ngx_socket_errno,
                                              ngx_socket_nread_n " failed");
@@ -2258,6 +2342,7 @@ ngx_ssl_recv(ngx_connection_t *c, u_char *buf, size_t size)
             return 0;
 
         case NGX_ERROR:
+            c->read->ready = 0;
             c->read->error = 1;
 
             /* fall through */
@@ -2278,6 +2363,7 @@ ngx_ssl_recv_early(ngx_connection_t *c, u_char *buf, size_t size)
     size_t     readbytes;
 
     if (c->ssl->last == NGX_ERROR) {
+        c->read->ready = 0;
         c->read->error = 1;
         return NGX_ERROR;
     }
@@ -2377,6 +2463,7 @@ ngx_ssl_recv_early(ngx_connection_t *c, u_char *buf, size_t size)
             return 0;
 
         case NGX_ERROR:
+            c->read->ready = 0;
             c->read->error = 1;
 
             /* fall through */
@@ -2396,7 +2483,8 @@ ngx_ssl_handle_recv(ngx_connection_t *c, int n)
     int        sslerr;
     ngx_err_t  err;
 
-#ifndef SSL_OP_NO_RENEGOTIATION
+#if (!defined SSL_OP_NO_RENEGOTIATION                                         \
+     && !defined SSL_OP_NO_CLIENT_RENEGOTIATION)
 
     if (c->ssl->renegotiation) {
         /*
@@ -2943,7 +3031,7 @@ ngx_ssl_write_early(ngx_connection_t *c, u_char *data, size_t size)
 static ssize_t
 ngx_ssl_sendfile(ngx_connection_t *c, ngx_buf_t *file, size_t size)
 {
-#ifdef BIO_get_ktls_send
+#if (defined BIO_get_ktls_send && !NGX_WIN32)
 
     int        sslerr, flags;
     ssize_t    n;
@@ -2972,7 +3060,7 @@ ngx_ssl_sendfile(ngx_connection_t *c, ngx_buf_t *file, size_t size)
     n = SSL_sendfile(c->ssl->connection, file->file->fd, file->file_pos,
                      size, flags);
 
-    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, "SSL_sendfile: %d", n);
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, "SSL_sendfile: %z", n);
 
     if (n > 0) {
 
@@ -3149,6 +3237,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
+
     rc = NGX_OK;
 
     ngx_ssl_ocsp_cleanup(c);
@@ -3336,30 +3431,74 @@ ngx_ssl_connection_error(ngx_connection_t *c, int sslerr, ngx_err_t err,
 
     } else if (sslerr == SSL_ERROR_SSL) {
 
-        n = ERR_GET_REASON(ERR_peek_error());
+        n = ERR_GET_REASON(ERR_peek_last_error());
 
             /* handshake failures */
         if (n == SSL_R_BAD_CHANGE_CIPHER_SPEC                        /*  103 */
 #ifdef SSL_R_NO_SUITABLE_KEY_SHARE
             || n == SSL_R_NO_SUITABLE_KEY_SHARE                      /*  101 */
 #endif
+#ifdef SSL_R_BAD_ALERT
+            || n == SSL_R_BAD_ALERT                                  /*  102 */
+#endif
+#ifdef SSL_R_BAD_KEY_SHARE
+            || n == SSL_R_BAD_KEY_SHARE                              /*  108 */
+#endif
+#ifdef SSL_R_BAD_EXTENSION
+            || n == SSL_R_BAD_EXTENSION                              /*  110 */
+#endif
+            || n == SSL_R_BAD_DIGEST_LENGTH                          /*  111 */
+#ifdef SSL_R_MISSING_SIGALGS_EXTENSION
+            || n == SSL_R_MISSING_SIGALGS_EXTENSION                  /*  112 */
+#endif
+            || n == SSL_R_BAD_PACKET_LENGTH                          /*  115 */
 #ifdef SSL_R_NO_SUITABLE_SIGNATURE_ALGORITHM
             || n == SSL_R_NO_SUITABLE_SIGNATURE_ALGORITHM            /*  118 */
+#endif
+#ifdef SSL_R_BAD_KEY_UPDATE
+            || n == SSL_R_BAD_KEY_UPDATE                             /*  122 */
 #endif
             || n == SSL_R_BLOCK_CIPHER_PAD_IS_WRONG                  /*  129 */
+            || n == SSL_R_CCS_RECEIVED_EARLY                         /*  133 */
+#ifdef SSL_R_DECODE_ERROR
+            || n == SSL_R_DECODE_ERROR                               /*  137 */
+#endif
+#ifdef SSL_R_DATA_BETWEEN_CCS_AND_FINISHED
+            || n == SSL_R_DATA_BETWEEN_CCS_AND_FINISHED              /*  145 */
+#endif
+            || n == SSL_R_DATA_LENGTH_TOO_LONG                       /*  146 */
             || n == SSL_R_DIGEST_CHECK_FAILED                        /*  149 */
+            || n == SSL_R_ENCRYPTED_LENGTH_TOO_LONG                  /*  150 */
             || n == SSL_R_ERROR_IN_RECEIVED_CIPHER_LIST              /*  151 */
             || n == SSL_R_EXCESSIVE_MESSAGE_SIZE                     /*  152 */
+#ifdef SSL_R_GOT_A_FIN_BEFORE_A_CCS
+            || n == SSL_R_GOT_A_FIN_BEFORE_A_CCS                     /*  154 */
+#endif
             || n == SSL_R_HTTPS_PROXY_REQUEST                        /*  155 */
             || n == SSL_R_HTTP_REQUEST                               /*  156 */
             || n == SSL_R_LENGTH_MISMATCH                            /*  159 */
+#ifdef SSL_R_LENGTH_TOO_SHORT
+            || n == SSL_R_LENGTH_TOO_SHORT                           /*  160 */
+#endif
+#ifdef SSL_R_NO_RENEGOTIATION
+            || n == SSL_R_NO_RENEGOTIATION                           /*  182 */
+#endif
 #ifdef SSL_R_NO_CIPHERS_PASSED
             || n == SSL_R_NO_CIPHERS_PASSED                          /*  182 */
 #endif
             || n == SSL_R_NO_CIPHERS_SPECIFIED                       /*  183 */
+#ifdef SSL_R_BAD_CIPHER
+            || n == SSL_R_BAD_CIPHER                                 /*  186 */
+#endif
             || n == SSL_R_NO_COMPRESSION_SPECIFIED                   /*  187 */
             || n == SSL_R_NO_SHARED_CIPHER                           /*  193 */
+#ifdef SSL_R_PACKET_LENGTH_TOO_LONG
+            || n == SSL_R_PACKET_LENGTH_TOO_LONG                     /*  198 */
+#endif
             || n == SSL_R_RECORD_LENGTH_MISMATCH                     /*  213 */
+#ifdef SSL_R_TOO_MANY_WARNING_ALERTS
+            || n == SSL_R_TOO_MANY_WARNING_ALERTS                    /*  220 */
+#endif
 #ifdef SSL_R_CLIENTHELLO_TLSEXT
             || n == SSL_R_CLIENTHELLO_TLSEXT                         /*  226 */
 #endif
@@ -3369,6 +3508,9 @@ ngx_ssl_connection_error(ngx_connection_t *c, int sslerr, ngx_err_t err,
 #ifdef SSL_R_CALLBACK_FAILED
             || n == SSL_R_CALLBACK_FAILED                            /*  234 */
 #endif
+#ifdef SSL_R_TLS_RSA_ENCRYPTED_VALUE_LENGTH_IS_WRONG
+            || n == SSL_R_TLS_RSA_ENCRYPTED_VALUE_LENGTH_IS_WRONG    /*  234 */
+#endif
 #ifdef SSL_R_NO_APPLICATION_PROTOCOL
             || n == SSL_R_NO_APPLICATION_PROTOCOL                    /*  235 */
 #endif
@@ -3378,13 +3520,44 @@ ngx_ssl_connection_error(ngx_connection_t *c, int sslerr, ngx_err_t err,
             || n == SSL_R_UNKNOWN_PROTOCOL                           /*  252 */
 #ifdef SSL_R_NO_COMMON_SIGNATURE_ALGORITHMS
             || n == SSL_R_NO_COMMON_SIGNATURE_ALGORITHMS             /*  253 */
+#endif
+#ifdef SSL_R_INVALID_COMPRESSION_LIST
+            || n == SSL_R_INVALID_COMPRESSION_LIST                   /*  256 */
+#endif
+#ifdef SSL_R_MISSING_KEY_SHARE
+            || n == SSL_R_MISSING_KEY_SHARE                          /*  258 */
 #endif
             || n == SSL_R_UNSUPPORTED_PROTOCOL                       /*  258 */
 #ifdef SSL_R_NO_SHARED_GROUP
             || n == SSL_R_NO_SHARED_GROUP                            /*  266 */
 #endif
             || n == SSL_R_WRONG_VERSION_NUMBER                       /*  267 */
+#ifdef SSL_R_TOO_MUCH_SKIPPED_EARLY_DATA
+            || n == SSL_R_TOO_MUCH_SKIPPED_EARLY_DATA                /*  270 */
+#endif
+            || n == SSL_R_BAD_LENGTH                                 /*  271 */
             || n == SSL_R_DECRYPTION_FAILED_OR_BAD_RECORD_MAC        /*  281 */
+#ifdef SSL_R_APPLICATION_DATA_AFTER_CLOSE_NOTIFY
+            || n == SSL_R_APPLICATION_DATA_AFTER_CLOSE_NOTIFY        /*  291 */
+#endif
+#ifdef SSL_R_APPLICATION_DATA_ON_SHUTDOWN
+            || n == SSL_R_APPLICATION_DATA_ON_SHUTDOWN               /*  291 */
+#endif
+#ifdef SSL_R_BAD_LEGACY_VERSION
+            || n == SSL_R_BAD_LEGACY_VERSION                         /*  292 */
+#endif
+#ifdef SSL_R_MIXED_HANDSHAKE_AND_NON_HANDSHAKE_DATA
+            || n == SSL_R_MIXED_HANDSHAKE_AND_NON_HANDSHAKE_DATA     /*  293 */
+#endif
+#ifdef SSL_R_RECORD_TOO_SMALL
+            || n == SSL_R_RECORD_TOO_SMALL                           /*  298 */
+#endif
+#ifdef SSL_R_SSL3_SESSION_ID_TOO_LONG
+            || n == SSL_R_SSL3_SESSION_ID_TOO_LONG                   /*  300 */
+#endif
+#ifdef SSL_R_BAD_ECPOINT
+            || n == SSL_R_BAD_ECPOINT                                /*  306 */
+#endif
 #ifdef SSL_R_RENEGOTIATE_EXT_TOO_LONG
             || n == SSL_R_RENEGOTIATE_EXT_TOO_LONG                   /*  335 */
             || n == SSL_R_RENEGOTIATION_ENCODING_ERR                 /*  336 */
@@ -3399,11 +3572,23 @@ ngx_ssl_connection_error(ngx_connection_t *c, int sslerr, ngx_err_t err,
 #ifdef SSL_R_INAPPROPRIATE_FALLBACK
             || n == SSL_R_INAPPROPRIATE_FALLBACK                     /*  373 */
 #endif
+#ifdef SSL_R_NO_SHARED_SIGNATURE_ALGORITHMS
+            || n == SSL_R_NO_SHARED_SIGNATURE_ALGORITHMS             /*  376 */
+#endif
+#ifdef SSL_R_NO_SHARED_SIGATURE_ALGORITHMS
+            || n == SSL_R_NO_SHARED_SIGATURE_ALGORITHMS              /*  376 */
+#endif
 #ifdef SSL_R_CERT_CB_ERROR
             || n == SSL_R_CERT_CB_ERROR                              /*  377 */
 #endif
 #ifdef SSL_R_VERSION_TOO_LOW
             || n == SSL_R_VERSION_TOO_LOW                            /*  396 */
+#endif
+#ifdef SSL_R_TOO_MANY_WARN_ALERTS
+            || n == SSL_R_TOO_MANY_WARN_ALERTS                       /*  409 */
+#endif
+#ifdef SSL_R_BAD_RECORD_TYPE
+            || n == SSL_R_BAD_RECORD_TYPE                            /*  443 */
 #endif
             || n == 1000 /* SSL_R_SSLV3_ALERT_CLOSE_NOTIFY */
 #ifdef SSL_R_SSLV3_ALERT_UNEXPECTED_MESSAGE
@@ -3749,6 +3934,12 @@ ngx_ssl_session_cache_init(ngx_shm_zone_t *shm_zone, void *data)
 
     ngx_queue_init(&cache->expire_queue);
 
+    cache->ticket_keys[0].expire = 0;
+    cache->ticket_keys[1].expire = 0;
+    cache->ticket_keys[2].expire = 0;
+
+    cache->fail_time = 0;
+
     len = sizeof(" in SSL session shared cache \"\"") + shm_zone->shm.name.len;
 
     shpool->log_ctx = ngx_slab_alloc(shpool, len);
@@ -3767,16 +3958,16 @@ ngx_ssl_session_cache_init(ngx_shm_zone_t *shm_zone, void *data)
 
 /*
  * The length of the session id is 16 bytes for SSLv2 sessions and
- * between 1 and 32 bytes for SSLv3/TLSv1, typically 32 bytes.
- * It seems that the typical length of the external ASN1 representation
- * of a session is 118 or 119 bytes for SSLv3/TSLv1.
+ * between 1 and 32 bytes for SSLv3 and TLS, typically 32 bytes.
+ * Typical length of the external ASN1 representation of a session
+ * is about 150 bytes plus SNI server name.
  *
- * Thus on 32-bit platforms we allocate separately an rbtree node,
- * a session id, and an ASN1 representation, they take accordingly
- * 64, 32, and 128 bytes.
+ * On 32-bit platforms we allocate an rbtree node, a session id, and
+ * an ASN1 representation in a single allocation, it typically takes
+ * 256 bytes.
  *
  * On 64-bit platforms we allocate separately an rbtree node + session_id,
- * and an ASN1 representation, they take accordingly 128 and 128 bytes.
+ * and an ASN1 representation, they take accordingly 128 and 256 bytes.
  *
  * OpenSSL's i2d_SSL_SESSION() and d2i_SSL_SESSION are slow,
  * so they are outside the code locked by shared pool mutex
@@ -3786,7 +3977,8 @@ static int
 ngx_ssl_new_session(ngx_ssl_conn_t *ssl_conn, ngx_ssl_session_t *sess)
 {
     int                       len;
-    u_char                   *p, *id, *cached_sess, *session_id;
+    u_char                   *p, *session_id;
+    size_t                    n;
     uint32_t                  hash;
     SSL_CTX                  *ssl_ctx;
     unsigned int              session_id_length;
@@ -3797,17 +3989,42 @@ ngx_ssl_new_session(ngx_ssl_conn_t *ssl_conn, ngx_ssl_session_t *sess)
     ngx_ssl_session_cache_t  *cache;
     u_char                    buf[NGX_SSL_MAX_SESSION_SIZE];
 
+#ifdef TLS1_3_VERSION
+
+    /*
+     * OpenSSL tries to save TLSv1.3 sessions into session cache
+     * even when using tickets for stateless session resumption,
+     * "because some applications just want to know about the creation
+     * of a session"; do not cache such sessions
+     */
+
+    if (SSL_version(ssl_conn) == TLS1_3_VERSION
+        && (SSL_get_options(ssl_conn) & SSL_OP_NO_TICKET) == 0)
+    {
+        return 0;
+    }
+
+#endif
+
     len = i2d_SSL_SESSION(sess, NULL);
 
     /* do not cache too big session */
 
-    if (len > (int) NGX_SSL_MAX_SESSION_SIZE) {
+    if (len > NGX_SSL_MAX_SESSION_SIZE) {
         return 0;
     }
 
     p = buf;
     i2d_SSL_SESSION(sess, &p);
 
+    session_id = (u_char *) SSL_SESSION_get_id(sess, &session_id_length);
+
+    /* do not cache sessions with too long session id */
+
+    if (session_id_length > 32) {
+        return 0;
+    }
+
     c = ngx_ssl_get_connection(ssl_conn);
 
     ssl_ctx = c->ssl->session_ctx;
@@ -3821,23 +4038,13 @@ ngx_ssl_new_session(ngx_ssl_conn_t *ssl_conn, ngx_ssl_session_t *sess)
     /* drop one or two expired sessions */
     ngx_ssl_expire_sessions(cache, shpool, 1);
 
-    cached_sess = ngx_slab_alloc_locked(shpool, len);
-
-    if (cached_sess == NULL) {
-
-        /* drop the oldest non-expired session and try once more */
-
-        ngx_ssl_expire_sessions(cache, shpool, 0);
-
-        cached_sess = ngx_slab_alloc_locked(shpool, len);
-
-        if (cached_sess == NULL) {
-            sess_id = NULL;
-            goto failed;
-        }
-    }
+#if (NGX_PTR_SIZE == 8)
+    n = sizeof(ngx_ssl_sess_id_t);
+#else
+    n = offsetof(ngx_ssl_sess_id_t, session) + len;
+#endif
 
-    sess_id = ngx_slab_alloc_locked(shpool, sizeof(ngx_ssl_sess_id_t));
+    sess_id = ngx_slab_alloc_locked(shpool, n);
 
     if (sess_id == NULL) {
 
@@ -3845,41 +4052,34 @@ ngx_ssl_new_session(ngx_ssl_conn_t *ssl_conn, ngx_ssl_session_t *sess)
 
         ngx_ssl_expire_sessions(cache, shpool, 0);
 
-        sess_id = ngx_slab_alloc_locked(shpool, sizeof(ngx_ssl_sess_id_t));
+        sess_id = ngx_slab_alloc_locked(shpool, n);
 
         if (sess_id == NULL) {
             goto failed;
         }
     }
 
-    session_id = (u_char *) SSL_SESSION_get_id(sess, &session_id_length);
-
 #if (NGX_PTR_SIZE == 8)
 
-    id = sess_id->sess_id;
-
-#else
-
-    id = ngx_slab_alloc_locked(shpool, session_id_length);
+    sess_id->session = ngx_slab_alloc_locked(shpool, len);
 
-    if (id == NULL) {
+    if (sess_id->session == NULL) {
 
         /* drop the oldest non-expired session and try once more */
 
         ngx_ssl_expire_sessions(cache, shpool, 0);
 
-        id = ngx_slab_alloc_locked(shpool, session_id_length);
+        sess_id->session = ngx_slab_alloc_locked(shpool, len);
 
-        if (id == NULL) {
+        if (sess_id->session == NULL) {
             goto failed;
         }
     }
 
 #endif
 
-    ngx_memcpy(cached_sess, buf, len);
-
-    ngx_memcpy(id, session_id, session_id_length);
+    ngx_memcpy(sess_id->session, buf, len);
+    ngx_memcpy(sess_id->id, session_id, session_id_length);
 
     hash = ngx_crc32_short(session_id, session_id_length);
 
@@ -3889,9 +4089,7 @@ ngx_ssl_new_session(ngx_ssl_conn_t *ssl_conn, ngx_ssl_session_t *sess)
 
     sess_id->node.key = hash;
     sess_id->node.data = (u_char) session_id_length;
-    sess_id->id = id;
     sess_id->len = len;
-    sess_id->session = cached_sess;
 
     sess_id->expire = ngx_time() + SSL_CTX_get_timeout(ssl_ctx);
 
@@ -3905,18 +4103,17 @@ ngx_ssl_new_session(ngx_ssl_conn_t *ssl_conn, ngx_ssl_session_t *sess)
 
 failed:
 
-    if (cached_sess) {
-        ngx_slab_free_locked(shpool, cached_sess);
-    }
-
     if (sess_id) {
         ngx_slab_free_locked(shpool, sess_id);
     }
 
     ngx_shmtx_unlock(&shpool->mutex);
 
-    ngx_log_error(NGX_LOG_ALERT, c->log, 0,
-                  "could not allocate new session%s", shpool->log_ctx);
+    if (cache->fail_time != ngx_time()) {
+        cache->fail_time = ngx_time();
+        ngx_log_error(NGX_LOG_WARN, c->log, 0,
+                      "could not allocate new session%s", shpool->log_ctx);
+    }
 
     return 0;
 }
@@ -4002,9 +4199,10 @@ ngx_ssl_get_cached_session(ngx_ssl_conn_t *ssl_conn,
 
             ngx_rbtree_delete(&cache->session_rbtree, node);
 
+            ngx_explicit_memzero(sess_id->session, sess_id->len);
+
+#if (NGX_PTR_SIZE == 8)
             ngx_slab_free_locked(shpool, sess_id->session);
-#if (NGX_PTR_SIZE == 4)
-            ngx_slab_free_locked(shpool, sess_id->id);
 #endif
             ngx_slab_free_locked(shpool, sess_id);
 
@@ -4092,9 +4290,10 @@ ngx_ssl_remove_session(SSL_CTX *ssl, ngx_ssl_session_t *sess)
 
             ngx_rbtree_delete(&cache->session_rbtree, node);
 
+            ngx_explicit_memzero(sess_id->session, sess_id->len);
+
+#if (NGX_PTR_SIZE == 8)
             ngx_slab_free_locked(shpool, sess_id->session);
-#if (NGX_PTR_SIZE == 4)
-            ngx_slab_free_locked(shpool, sess_id->id);
 #endif
             ngx_slab_free_locked(shpool, sess_id);
 
@@ -4141,9 +4340,10 @@ ngx_ssl_expire_sessions(ngx_ssl_session_cache_t *cache,
 
         ngx_rbtree_delete(&cache->session_rbtree, &sess_id->node);
 
+        ngx_explicit_memzero(sess_id->session, sess_id->len);
+
+#if (NGX_PTR_SIZE == 8)
         ngx_slab_free_locked(shpool, sess_id->session);
-#if (NGX_PTR_SIZE == 4)
-        ngx_slab_free_locked(shpool, sess_id->id);
 #endif
         ngx_slab_free_locked(shpool, sess_id);
     }
@@ -4197,23 +4397,25 @@ ngx_ssl_session_rbtree_insert_value(ngx_rbtree_node_t *temp,
 ngx_int_t
 ngx_ssl_session_ticket_keys(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_array_t *paths)
 {
-    u_char                         buf[80];
-    size_t                         size;
-    ssize_t                        n;
-    ngx_str_t                     *path;
-    ngx_file_t                     file;
-    ngx_uint_t                     i;
-    ngx_array_t                   *keys;
-    ngx_file_info_t                fi;
-    ngx_pool_cleanup_t            *cln;
-    ngx_ssl_session_ticket_key_t  *key;
-
-    if (paths == NULL) {
+    u_char                 buf[80];
+    size_t                 size;
+    ssize_t                n;
+    ngx_str_t             *path;
+    ngx_file_t             file;
+    ngx_uint_t             i;
+    ngx_array_t           *keys;
+    ngx_file_info_t        fi;
+    ngx_pool_cleanup_t    *cln;
+    ngx_ssl_ticket_key_t  *key;
+
+    if (paths == NULL
+        && SSL_CTX_get_ex_data(ssl->ctx, ngx_ssl_session_cache_index) == NULL)
+    {
         return NGX_OK;
     }
 
-    keys = ngx_array_create(cf->pool, paths->nelts,
-                            sizeof(ngx_ssl_session_ticket_key_t));
+    keys = ngx_array_create(cf->pool, paths ? paths->nelts : 3,
+                            sizeof(ngx_ssl_ticket_key_t));
     if (keys == NULL) {
         return NGX_ERROR;
     }
@@ -4223,9 +4425,41 @@ ngx_ssl_session_ticket_keys(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_array_t *paths)
         return NGX_ERROR;
     }
 
-    cln->handler = ngx_ssl_session_ticket_keys_cleanup;
+    cln->handler = ngx_ssl_ticket_keys_cleanup;
     cln->data = keys;
 
+    if (SSL_CTX_set_ex_data(ssl->ctx, ngx_ssl_ticket_keys_index, keys) == 0) {
+        ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0,
+                      "SSL_CTX_set_ex_data() failed");
+        return NGX_ERROR;
+    }
+
+    if (SSL_CTX_set_tlsext_ticket_key_cb(ssl->ctx, ngx_ssl_ticket_key_callback)
+        == 0)
+    {
+        ngx_log_error(NGX_LOG_WARN, cf->log, 0,
+                      "nginx was built with Session Tickets support, however, "
+                      "now it is linked dynamically to an OpenSSL library "
+                      "which has no tlsext support, therefore Session Tickets "
+                      "are not available");
+        return NGX_OK;
+    }
+
+    if (paths == NULL) {
+
+        /* placeholder for keys in shared memory */
+
+        key = ngx_array_push_n(keys, 3);
+        key[0].shared = 1;
+        key[0].expire = 0;
+        key[1].shared = 1;
+        key[1].expire = 0;
+        key[2].shared = 1;
+        key[2].expire = 0;
+
+        return NGX_OK;
+    }
+
     path = paths->elts;
     for (i = 0; i < paths->nelts; i++) {
 
@@ -4280,6 +4514,9 @@ ngx_ssl_session_ticket_keys(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_array_t *paths)
             goto failed;
         }
 
+        key->shared = 0;
+        key->expire = 1;
+
         if (size == 48) {
             key->size = 48;
             ngx_memcpy(key->name, buf, 16);
@@ -4301,25 +4538,6 @@ ngx_ssl_session_ticket_keys(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_array_t *paths)
         ngx_explicit_memzero(&buf, 80);
     }
 
-    if (SSL_CTX_set_ex_data(ssl->ctx, ngx_ssl_session_ticket_keys_index, keys)
-        == 0)
-    {
-        ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0,
-                      "SSL_CTX_set_ex_data() failed");
-        return NGX_ERROR;
-    }
-
-    if (SSL_CTX_set_tlsext_ticket_key_cb(ssl->ctx,
-                                         ngx_ssl_session_ticket_key_callback)
-        == 0)
-    {
-        ngx_log_error(NGX_LOG_WARN, cf->log, 0,
-                      "nginx was built with Session Tickets support, however, "
-                      "now it is linked dynamically to an OpenSSL library "
-                      "which has no tlsext support, therefore Session Tickets "
-                      "are not available");
-    }
-
     return NGX_OK;
 
 failed:
@@ -4336,29 +4554,33 @@ failed:
 
 
 static int
-ngx_ssl_session_ticket_key_callback(ngx_ssl_conn_t *ssl_conn,
+ngx_ssl_ticket_key_callback(ngx_ssl_conn_t *ssl_conn,
     unsigned char *name, unsigned char *iv, EVP_CIPHER_CTX *ectx,
     HMAC_CTX *hctx, int enc)
 {
-    size_t                         size;
-    SSL_CTX                       *ssl_ctx;
-    ngx_uint_t                     i;
-    ngx_array_t                   *keys;
-    ngx_connection_t              *c;
-    ngx_ssl_session_ticket_key_t  *key;
-    const EVP_MD                  *digest;
-    const EVP_CIPHER              *cipher;
+    size_t                 size;
+    SSL_CTX               *ssl_ctx;
+    ngx_uint_t             i;
+    ngx_array_t           *keys;
+    ngx_connection_t      *c;
+    ngx_ssl_ticket_key_t  *key;
+    const EVP_MD          *digest;
+    const EVP_CIPHER      *cipher;
 
     c = ngx_ssl_get_connection(ssl_conn);
     ssl_ctx = c->ssl->session_ctx;
 
+    if (ngx_ssl_rotate_ticket_keys(ssl_ctx, c->log) != NGX_OK) {
+        return -1;
+    }
+
 #ifdef OPENSSL_NO_SHA256
     digest = EVP_sha1();
 #else
     digest = EVP_sha256();
 #endif
 
-    keys = SSL_CTX_get_ex_data(ssl_ctx, ngx_ssl_session_ticket_keys_index);
+    keys = SSL_CTX_get_ex_data(ssl_ctx, ngx_ssl_ticket_keys_index);
     if (keys == NULL) {
         return -1;
     }
@@ -4369,7 +4591,7 @@ ngx_ssl_session_ticket_key_callback(ngx_ssl_conn_t *ssl_conn,
         /* encrypt session ticket */
 
         ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                       "ssl session ticket encrypt, key: \"%*xs\" (%s session)",
+                       "ssl ticket encrypt, key: \"%*xs\" (%s session)",
                        (size_t) 16, key[0].name,
                        SSL_session_reused(ssl_conn) ? "reused" : "new");
 
@@ -4416,7 +4638,7 @@ ngx_ssl_session_ticket_key_callback(ngx_ssl_conn_t *ssl_conn,
         }
 
         ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                       "ssl session ticket decrypt, key: \"%*xs\" not found",
+                       "ssl ticket decrypt, key: \"%*xs\" not found",
                        (size_t) 16, name);
 
         return 0;
@@ -4424,7 +4646,7 @@ ngx_ssl_session_ticket_key_callback(ngx_ssl_conn_t *ssl_conn,
     found:
 
         ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                       "ssl session ticket decrypt, key: \"%*xs\"%s",
+                       "ssl ticket decrypt, key: \"%*xs\"%s",
                        (size_t) 16, key[i].name, (i == 0) ? " (default)" : "");
 
         if (key[i].size == 48) {
@@ -4461,7 +4683,7 @@ ngx_ssl_session_ticket_key_callback(ngx_ssl_conn_t *ssl_conn,
 
         /* renew if non-default key */
 
-        if (i != 0) {
+        if (i != 0 && key[i].expire) {
             return 2;
         }
 
@@ -4470,13 +4692,142 @@ ngx_ssl_session_ticket_key_callback(ngx_ssl_conn_t *ssl_conn,
 }
 
 
+static ngx_int_t
+ngx_ssl_rotate_ticket_keys(SSL_CTX *ssl_ctx, ngx_log_t *log)
+{
+    time_t                    now, expire;
+    ngx_array_t              *keys;
+    ngx_shm_zone_t           *shm_zone;
+    ngx_slab_pool_t          *shpool;
+    ngx_ssl_ticket_key_t     *key;
+    ngx_ssl_session_cache_t  *cache;
+    u_char                    buf[80];
+
+    keys = SSL_CTX_get_ex_data(ssl_ctx, ngx_ssl_ticket_keys_index);
+    if (keys == NULL) {
+        return NGX_OK;
+    }
+
+    key = keys->elts;
+
+    if (!key[0].shared) {
+        return NGX_OK;
+    }
+
+    /*
+     * if we don't need to update expiration of the current key
+     * and the previous key is still needed, don't sync with shared
+     * memory to save some work; in the worst case other worker process
+     * will switch to the next key, but this process will still be able
+     * to decrypt tickets encrypted with it
+     */
+
+    now = ngx_time();
+    expire = now + SSL_CTX_get_timeout(ssl_ctx);
+
+    if (key[0].expire >= expire && key[1].expire >= now) {
+        return NGX_OK;
+    }
+
+    shm_zone = SSL_CTX_get_ex_data(ssl_ctx, ngx_ssl_session_cache_index);
+
+    cache = shm_zone->data;
+    shpool = (ngx_slab_pool_t *) shm_zone->shm.addr;
+
+    ngx_shmtx_lock(&shpool->mutex);
+
+    key = cache->ticket_keys;
+
+    if (key[0].expire == 0) {
+
+        /* initialize the current key */
+
+        if (RAND_bytes(buf, 80) != 1) {
+            ngx_ssl_error(NGX_LOG_ALERT, log, 0, "RAND_bytes() failed");
+            ngx_shmtx_unlock(&shpool->mutex);
+            return NGX_ERROR;
+        }
+
+        key[0].shared = 1;
+        key[0].expire = expire;
+        key[0].size = 80;
+        ngx_memcpy(key[0].name, buf, 16);
+        ngx_memcpy(key[0].hmac_key, buf + 16, 32);
+        ngx_memcpy(key[0].aes_key, buf + 48, 32);
+
+        ngx_explicit_memzero(&buf, 80);
+
+        ngx_log_debug2(NGX_LOG_DEBUG_EVENT, log, 0,
+                       "ssl ticket key: \"%*xs\"",
+                       (size_t) 16, key[0].name);
+
+        /*
+         * copy the current key to the next key, as initialization of
+         * the previous key will replace the current key with the next
+         * key
+         */
+
+        key[2] = key[0];
+    }
+
+    if (key[1].expire < now) {
+
+        /*
+         * if the previous key is no longer needed (or not initialized),
+         * replace it with the current key, replace the current key with
+         * the next key, and generate new next key
+         */
+
+        key[1] = key[0];
+        key[0] = key[2];
+
+        if (RAND_bytes(buf, 80) != 1) {
+            ngx_ssl_error(NGX_LOG_ALERT, log, 0, "RAND_bytes() failed");
+            ngx_shmtx_unlock(&shpool->mutex);
+            return NGX_ERROR;
+        }
+
+        key[2].shared = 1;
+        key[2].expire = 0;
+        key[2].size = 80;
+        ngx_memcpy(key[2].name, buf, 16);
+        ngx_memcpy(key[2].hmac_key, buf + 16, 32);
+        ngx_memcpy(key[2].aes_key, buf + 48, 32);
+
+        ngx_explicit_memzero(&buf, 80);
+
+        ngx_log_debug2(NGX_LOG_DEBUG_EVENT, log, 0,
+                       "ssl ticket key: \"%*xs\"",
+                       (size_t) 16, key[2].name);
+    }
+
+    /*
+     * update expiration of the current key: it is going to be needed
+     * at least till the session being created expires
+     */
+
+    if (expire > key[0].expire) {
+        key[0].expire = expire;
+    }
+
+    /* sync keys to the worker process memory */
+
+    ngx_memcpy(keys->elts, cache->ticket_keys,
+               2 * sizeof(ngx_ssl_ticket_key_t));
+
+    ngx_shmtx_unlock(&shpool->mutex);
+
+    return NGX_OK;
+}
+
+
 static void
-ngx_ssl_session_ticket_keys_cleanup(void *data)
+ngx_ssl_ticket_keys_cleanup(void *data)
 {
     ngx_array_t  *keys = data;
 
     ngx_explicit_memzero(keys->elts,
-                         keys->nelts * sizeof(ngx_ssl_session_ticket_key_t));
+                         keys->nelts * sizeof(ngx_ssl_ticket_key_t));
 }
 
 #else
@@ -4836,6 +5187,9 @@ ngx_ssl_get_curves(ngx_connection_t *c, ngx_pool_t *pool, ngx_str_t *s)
     }
 
     curves = ngx_palloc(pool, n * sizeof(int));
+    if (curves == NULL) {
+        return NGX_ERROR;
+    }
 
     n = SSL_get1_curves(c->ssl->connection, curves);
     len = 0;
diff --git a/src/event/ngx_event_openssl.h b/src/event/ngx_event_openssl.h
index c9e86d9..ebb2c35 100644
--- a/src/event/ngx_event_openssl.h
+++ b/src/event/ngx_event_openssl.h
@@ -24,6 +24,14 @@
 #include <openssl/engine.h>
 #endif
 #include <openssl/evp.h>
+#if (NGX_QUIC)
+#ifdef OPENSSL_IS_BORINGSSL
+#include <openssl/hkdf.h>
+#include <openssl/chacha.h>
+#else
+#include <openssl/kdf.h>
+#endif
+#endif
 #include <openssl/hmac.h>
 #ifndef OPENSSL_NO_OCSP
 #include <openssl/ocsp.h>
@@ -37,7 +45,7 @@
 
 #if (defined LIBRESSL_VERSION_NUMBER && OPENSSL_VERSION_NUMBER == 0x20000000L)
 #undef OPENSSL_VERSION_NUMBER
-#if (LIBRESSL_VERSION_NUMBER >= 0x2080000fL)
+#if (LIBRESSL_VERSION_NUMBER >= 0x3050000fL)
 #define OPENSSL_VERSION_NUMBER  0x1010000fL
 #else
 #define OPENSSL_VERSION_NUMBER  0x1000107fL
@@ -114,6 +122,7 @@ struct ngx_ssl_connection_s {
     unsigned                    no_send_shutdown:1;
     unsigned                    shutdown_without_free:1;
     unsigned                    handshake_buffer_set:1;
+    unsigned                    session_timeout_set:1;
     unsigned                    try_early_data:1;
     unsigned                    in_early:1;
     unsigned                    in_ocsp:1;
@@ -134,35 +143,35 @@ typedef struct ngx_ssl_sess_id_s  ngx_ssl_sess_id_t;
 
 struct ngx_ssl_sess_id_s {
     ngx_rbtree_node_t           node;
-    u_char                     *id;
     size_t                      len;
-    u_char                     *session;
     ngx_queue_t                 queue;
     time_t                      expire;
+    u_char                      id[32];
 #if (NGX_PTR_SIZE == 8)
-    void                       *stub;
-    u_char                      sess_id[32];
+    u_char                     *session;
+#else
+    u_char                      session[1];
 #endif
 };
 
 
 typedef struct {
-    ngx_rbtree_t                session_rbtree;
-    ngx_rbtree_node_t           sentinel;
-    ngx_queue_t                 expire_queue;
-} ngx_ssl_session_cache_t;
-
-
-#ifdef SSL_CTRL_SET_TLSEXT_TICKET_KEY_CB
-
-typedef struct {
-    size_t                      size;
     u_char                      name[16];
     u_char                      hmac_key[32];
     u_char                      aes_key[32];
-} ngx_ssl_session_ticket_key_t;
+    time_t                      expire;
+    unsigned                    size:8;
+    unsigned                    shared:1;
+} ngx_ssl_ticket_key_t;
 
-#endif
+
+typedef struct {
+    ngx_rbtree_t                session_rbtree;
+    ngx_rbtree_node_t           sentinel;
+    ngx_queue_t                 expire_queue;
+    ngx_ssl_ticket_key_t        ticket_keys[3];
+    time_t                      fail_time;
+} ngx_ssl_session_cache_t;
 
 
 #define NGX_SSL_SSLv2    0x0002
@@ -204,10 +213,12 @@ ngx_int_t ngx_ssl_ocsp(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_str_t *responder,
     ngx_uint_t depth, ngx_shm_zone_t *shm_zone);
 ngx_int_t ngx_ssl_ocsp_resolver(ngx_conf_t *cf, ngx_ssl_t *ssl,
     ngx_resolver_t *resolver, ngx_msec_t resolver_timeout);
+
 ngx_int_t ngx_ssl_ocsp_validate(ngx_connection_t *c);
 ngx_int_t ngx_ssl_ocsp_get_status(ngx_connection_t *c, const char **s);
 void ngx_ssl_ocsp_cleanup(ngx_connection_t *c);
 ngx_int_t ngx_ssl_ocsp_cache_init(ngx_shm_zone_t *shm_zone, void *data);
+
 ngx_array_t *ngx_ssl_read_password_file(ngx_conf_t *cf, ngx_str_t *file);
 ngx_array_t *ngx_ssl_preserve_passwords(ngx_conf_t *cf,
     ngx_array_t *passwords);
@@ -299,6 +310,9 @@ ngx_int_t ngx_ssl_get_client_v_remain(ngx_connection_t *c, ngx_pool_t *pool,
 
 
 ngx_int_t ngx_ssl_handshake(ngx_connection_t *c);
+#if (NGX_DEBUG)
+void ngx_ssl_handshake_log(ngx_connection_t *c);
+#endif
 ssize_t ngx_ssl_recv(ngx_connection_t *c, u_char *buf, size_t size);
 ssize_t ngx_ssl_write(ngx_connection_t *c, u_char *data, size_t size);
 ssize_t ngx_ssl_recv_chain(ngx_connection_t *c, ngx_chain_t *cl, off_t limit);
@@ -314,7 +328,7 @@ void ngx_ssl_cleanup_ctx(void *data);
 extern int  ngx_ssl_connection_index;
 extern int  ngx_ssl_server_conf_index;
 extern int  ngx_ssl_session_cache_index;
-extern int  ngx_ssl_session_ticket_keys_index;
+extern int  ngx_ssl_ticket_keys_index;
 extern int  ngx_ssl_ocsp_index;
 extern int  ngx_ssl_certificate_index;
 extern int  ngx_ssl_next_certificate_index;
diff --git a/src/event/ngx_event_openssl_stapling.c b/src/event/ngx_event_openssl_stapling.c
index e3fa8c4..e9bb835 100644
--- a/src/event/ngx_event_openssl_stapling.c
+++ b/src/event/ngx_event_openssl_stapling.c
@@ -893,7 +893,7 @@ ngx_ssl_ocsp_validate(ngx_connection_t *c)
     ocsp->cert_status = V_OCSP_CERTSTATUS_GOOD;
     ocsp->conf = ocf;
 
-#if (OPENSSL_VERSION_NUMBER >= 0x10100000L && !defined LIBRESSL_VERSION_NUMBER)
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L
 
     ocsp->certs = SSL_get0_verified_chain(c->ssl->connection);
 
diff --git a/src/event/ngx_event_pipe.c b/src/event/ngx_event_pipe.c
index 54412e1..d774903 100644
--- a/src/event/ngx_event_pipe.c
+++ b/src/event/ngx_event_pipe.c
@@ -57,7 +57,9 @@ ngx_event_pipe(ngx_event_pipe_t *p, ngx_int_t do_write)
         do_write = 1;
     }
 
-    if (p->upstream->fd != (ngx_socket_t) -1) {
+    if (p->upstream
+        && p->upstream->fd != (ngx_socket_t) -1)
+    {
         rev = p->upstream->read;
 
         flags = (rev->eof || rev->error) ? NGX_CLOSE_EVENT : 0;
@@ -108,7 +110,9 @@ ngx_event_pipe_read_upstream(ngx_event_pipe_t *p)
     ngx_msec_t    delay;
     ngx_chain_t  *chain, *cl, *ln;
 
-    if (p->upstream_eof || p->upstream_error || p->upstream_done) {
+    if (p->upstream_eof || p->upstream_error || p->upstream_done
+        || p->upstream == NULL)
+    {
         return NGX_OK;
     }
 
diff --git a/src/event/ngx_event_udp.c b/src/event/ngx_event_udp.c
index a524ae0..43fa621 100644
--- a/src/event/ngx_event_udp.c
+++ b/src/event/ngx_event_udp.c
@@ -12,13 +12,6 @@
 
 #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);
@@ -46,18 +39,8 @@ ngx_event_recvmsg(ngx_event_t *ev)
     ngx_connection_t  *c, *lc;
     static u_char      buffer[65535];
 
-#if (NGX_HAVE_MSGHDR_MSG_CONTROL)
-
-#if (NGX_HAVE_IP_RECVDSTADDR)
-    u_char             msg_control[CMSG_SPACE(sizeof(struct in_addr))];
-#elif (NGX_HAVE_IP_PKTINFO)
-    u_char             msg_control[CMSG_SPACE(sizeof(struct in_pktinfo))];
-#endif
-
-#if (NGX_HAVE_INET6 && NGX_HAVE_IPV6_RECVPKTINFO)
-    u_char             msg_control6[CMSG_SPACE(sizeof(struct in6_pktinfo))];
-#endif
-
+#if (NGX_HAVE_ADDRINFO_CMSG)
+    u_char             msg_control[CMSG_SPACE(sizeof(ngx_addrinfo_t))];
 #endif
 
     if (ev->timedout) {
@@ -92,25 +75,13 @@ ngx_event_recvmsg(ngx_event_t *ev)
         msg.msg_iov = iov;
         msg.msg_iovlen = 1;
 
-#if (NGX_HAVE_MSGHDR_MSG_CONTROL)
-
+#if (NGX_HAVE_ADDRINFO_CMSG)
         if (ls->wildcard) {
+            msg.msg_control = &msg_control;
+            msg.msg_controllen = sizeof(msg_control);
 
-#if (NGX_HAVE_IP_RECVDSTADDR || NGX_HAVE_IP_PKTINFO)
-            if (ls->sockaddr->sa_family == AF_INET) {
-                msg.msg_control = &msg_control;
-                msg.msg_controllen = sizeof(msg_control);
-            }
-#endif
-
-#if (NGX_HAVE_INET6 && NGX_HAVE_IPV6_RECVPKTINFO)
-            if (ls->sockaddr->sa_family == AF_INET6) {
-                msg.msg_control = &msg_control6;
-                msg.msg_controllen = sizeof(msg_control6);
-            }
-#endif
+            ngx_memzero(&msg_control, sizeof(msg_control));
         }
-
 #endif
 
         n = recvmsg(lc->fd, &msg, 0);
@@ -129,7 +100,7 @@ ngx_event_recvmsg(ngx_event_t *ev)
             return;
         }
 
-#if (NGX_HAVE_MSGHDR_MSG_CONTROL)
+#if (NGX_HAVE_ADDRINFO_CMSG)
         if (msg.msg_flags & (MSG_TRUNC|MSG_CTRUNC)) {
             ngx_log_error(NGX_LOG_ALERT, ev->log, 0,
                           "recvmsg() truncated data");
@@ -159,7 +130,7 @@ ngx_event_recvmsg(ngx_event_t *ev)
         local_sockaddr = ls->sockaddr;
         local_socklen = ls->socklen;
 
-#if (NGX_HAVE_MSGHDR_MSG_CONTROL)
+#if (NGX_HAVE_ADDRINFO_CMSG)
 
         if (ls->wildcard) {
             struct cmsghdr  *cmsg;
@@ -171,59 +142,9 @@ ngx_event_recvmsg(ngx_event_t *ev)
                  cmsg != NULL;
                  cmsg = CMSG_NXTHDR(&msg, cmsg))
             {
-
-#if (NGX_HAVE_IP_RECVDSTADDR)
-
-                if (cmsg->cmsg_level == IPPROTO_IP
-                    && cmsg->cmsg_type == IP_RECVDSTADDR
-                    && local_sockaddr->sa_family == AF_INET)
-                {
-                    struct in_addr      *addr;
-                    struct sockaddr_in  *sin;
-
-                    addr = (struct in_addr *) CMSG_DATA(cmsg);
-                    sin = (struct sockaddr_in *) local_sockaddr;
-                    sin->sin_addr = *addr;
-
+                if (ngx_get_srcaddr_cmsg(cmsg, local_sockaddr) == NGX_OK) {
                     break;
                 }
-
-#elif (NGX_HAVE_IP_PKTINFO)
-
-                if (cmsg->cmsg_level == IPPROTO_IP
-                    && cmsg->cmsg_type == IP_PKTINFO
-                    && local_sockaddr->sa_family == AF_INET)
-                {
-                    struct in_pktinfo   *pkt;
-                    struct sockaddr_in  *sin;
-
-                    pkt = (struct in_pktinfo *) CMSG_DATA(cmsg);
-                    sin = (struct sockaddr_in *) local_sockaddr;
-                    sin->sin_addr = pkt->ipi_addr;
-
-                    break;
-                }
-
-#endif
-
-#if (NGX_HAVE_INET6 && NGX_HAVE_IPV6_RECVPKTINFO)
-
-                if (cmsg->cmsg_level == IPPROTO_IPV6
-                    && cmsg->cmsg_type == IPV6_PKTINFO
-                    && local_sockaddr->sa_family == AF_INET6)
-                {
-                    struct in6_pktinfo   *pkt6;
-                    struct sockaddr_in6  *sin6;
-
-                    pkt6 = (struct in6_pktinfo *) CMSG_DATA(cmsg);
-                    sin6 = (struct sockaddr_in6 *) local_sockaddr;
-                    sin6->sin6_addr = pkt6->ipi6_addr;
-
-                    break;
-                }
-
-#endif
-
             }
         }
 
@@ -318,6 +239,8 @@ ngx_event_recvmsg(ngx_event_t *ev)
         c->send = ngx_udp_send;
         c->send_chain = ngx_udp_send_chain;
 
+        c->need_flush_buf = 1;
+
         c->log = log;
         c->pool->log = log;
         c->listening = ls;
@@ -494,8 +417,8 @@ ngx_udp_rbtree_insert_value(ngx_rbtree_node_t *temp,
             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 && c->listening->wildcard) {
                 rc = ngx_cmp_sockaddr(c->local_sockaddr, c->local_socklen,
@@ -548,6 +471,8 @@ ngx_insert_udp_connection(ngx_connection_t *c)
     ngx_crc32_final(hash);
 
     udp->node.key = hash;
+    udp->key.data = (u_char *) c->sockaddr;
+    udp->key.len = c->socklen;
 
     cln = ngx_pool_cleanup_add(c->pool, 0);
     if (cln == NULL) {
diff --git a/src/event/ngx_event_udp.h b/src/event/ngx_event_udp.h
new file mode 100644
index 0000000..e5ddf1b
--- /dev/null
+++ b/src/event/ngx_event_udp.h
@@ -0,0 +1,66 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_EVENT_UDP_H_INCLUDED_
+#define _NGX_EVENT_UDP_H_INCLUDED_
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+
+#if !(NGX_WIN32)
+
+#if ((NGX_HAVE_MSGHDR_MSG_CONTROL)                                            \
+     && (NGX_HAVE_IP_SENDSRCADDR || NGX_HAVE_IP_RECVDSTADDR                   \
+         || NGX_HAVE_IP_PKTINFO                                               \
+         || (NGX_HAVE_INET6 && NGX_HAVE_IPV6_RECVPKTINFO)))
+#define NGX_HAVE_ADDRINFO_CMSG  1
+
+#endif
+
+
+struct ngx_udp_connection_s {
+    ngx_rbtree_node_t   node;
+    ngx_connection_t   *connection;
+    ngx_buf_t          *buffer;
+    ngx_str_t           key;
+};
+
+
+#if (NGX_HAVE_ADDRINFO_CMSG)
+
+typedef union {
+#if (NGX_HAVE_IP_SENDSRCADDR || NGX_HAVE_IP_RECVDSTADDR)
+    struct in_addr        addr;
+#endif
+
+#if (NGX_HAVE_IP_PKTINFO)
+    struct in_pktinfo     pkt;
+#endif
+
+#if (NGX_HAVE_INET6 && NGX_HAVE_IPV6_RECVPKTINFO)
+    struct in6_pktinfo    pkt6;
+#endif
+} ngx_addrinfo_t;
+
+size_t ngx_set_srcaddr_cmsg(struct cmsghdr *cmsg,
+    struct sockaddr *local_sockaddr);
+ngx_int_t ngx_get_srcaddr_cmsg(struct cmsghdr *cmsg,
+    struct sockaddr *local_sockaddr);
+
+#endif
+
+void ngx_event_recvmsg(ngx_event_t *ev);
+ssize_t ngx_sendmsg(ngx_connection_t *c, struct msghdr *msg, int flags);
+void ngx_udp_rbtree_insert_value(ngx_rbtree_node_t *temp,
+    ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel);
+#endif
+
+void ngx_delete_udp_connection(void *data);
+
+
+#endif /* _NGX_EVENT_UDP_H_INCLUDED_ */
diff --git a/src/event/quic/bpf/bpfgen.sh b/src/event/quic/bpf/bpfgen.sh
new file mode 100644
index 0000000..78cbdac
--- /dev/null
+++ b/src/event/quic/bpf/bpfgen.sh
@@ -0,0 +1,113 @@
+#!/bin/bash
+
+export LANG=C
+
+set -e
+
+if [ $# -lt 1 ]; then
+    echo "Usage: PROGNAME=foo LICENSE=bar $0 <bpf object file>"
+    exit 1
+fi
+
+
+self=$0
+filename=$1
+funcname=$PROGNAME
+
+generate_head()
+{
+    cat << END
+/* AUTO-GENERATED, DO NOT EDIT. */
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include "ngx_bpf.h"
+
+
+END
+}
+
+generate_tail()
+{
+    cat << END
+
+ngx_bpf_program_t $PROGNAME = {
+    .relocs = bpf_reloc_prog_$funcname,
+    .nrelocs = sizeof(bpf_reloc_prog_$funcname)
+               / sizeof(bpf_reloc_prog_$funcname[0]),
+    .ins = bpf_insn_prog_$funcname,
+    .nins = sizeof(bpf_insn_prog_$funcname)
+            / sizeof(bpf_insn_prog_$funcname[0]),
+    .license = "$LICENSE",
+    .type = BPF_PROG_TYPE_SK_REUSEPORT,
+};
+
+END
+}
+
+process_relocations()
+{
+    echo "static ngx_bpf_reloc_t bpf_reloc_prog_$funcname[] = {"
+
+    objdump -r $filename | awk '{
+
+    if (enabled && $NF > 0) {
+        off = strtonum(sprintf("0x%s", $1));
+        name = $3;
+
+        printf("    { \"%s\", %d },\n", name, off/8);
+    }
+
+    if ($1 == "OFFSET") {
+        enabled=1;
+    }
+}'
+    echo "};"
+    echo
+}
+
+process_section()
+{
+    echo "static struct bpf_insn bpf_insn_prog_$funcname[] = {"
+    echo "    /* opcode dst          src         offset imm */"
+
+    section_info=$(objdump -h $filename --section=$funcname | grep "1 $funcname")
+
+    # dd doesn't know hex
+    length=$(printf "%d" 0x$(echo $section_info | cut -d ' ' -f3))
+    offset=$(printf "%d" 0x$(echo $section_info | cut -d ' ' -f6))
+
+    for ins in $(dd if="$filename" bs=1 count=$length skip=$offset status=none | xxd -p -c 8)
+    do
+        opcode=0x${ins:0:2}
+        srcdst=0x${ins:2:2}
+
+        # bytes are dumped in LE order
+        offset=0x${ins:6:2}${ins:4:2}                        # short
+        immedi=0x${ins:14:2}${ins:12:2}${ins:10:2}${ins:8:2} # int
+
+        dst="$(($srcdst & 0xF))"
+        src="$(($srcdst & 0xF0))"
+        src="$(($src >> 4))"
+
+        opcode=$(printf "0x%x" $opcode)
+        dst=$(printf "BPF_REG_%d" $dst)
+        src=$(printf "BPF_REG_%d" $src)
+        offset=$(printf "%d" $offset)
+        immedi=$(printf "0x%x" $immedi)
+
+        printf "    { %4s, %11s, %11s, (int16_t) %6s, %10s },\n" $opcode $dst $src $offset $immedi
+    done
+
+cat << END
+};
+
+END
+}
+
+generate_head
+process_relocations
+process_section
+generate_tail
+
diff --git a/src/event/quic/bpf/makefile b/src/event/quic/bpf/makefile
new file mode 100644
index 0000000..b4d758f
--- /dev/null
+++ b/src/event/quic/bpf/makefile
@@ -0,0 +1,30 @@
+CFLAGS=-O2 -Wall
+
+LICENSE=BSD
+
+PROGNAME=ngx_quic_reuseport_helper
+RESULT=ngx_event_quic_bpf_code
+DEST=../$(RESULT).c
+
+all: $(RESULT)
+
+$(RESULT): $(PROGNAME).o
+	LICENSE=$(LICENSE) PROGNAME=$(PROGNAME) bash ./bpfgen.sh $< > $@
+
+DEFS=-DPROGNAME=\"$(PROGNAME)\"                                               \
+     -DLICENSE_$(LICENSE)                                                     \
+     -DLICENSE=\"$(LICENSE)\"                                                 \
+
+$(PROGNAME).o: $(PROGNAME).c
+	clang $(CFLAGS) $(DEFS) -target bpf -c $< -o $@
+
+install: $(RESULT)
+	cp $(RESULT) $(DEST)
+
+clean:
+	@rm -f $(RESULT) *.o
+
+debug: $(PROGNAME).o
+	llvm-objdump -S -no-show-raw-insn $<
+
+.DELETE_ON_ERROR:
diff --git a/src/event/quic/bpf/ngx_quic_reuseport_helper.c b/src/event/quic/bpf/ngx_quic_reuseport_helper.c
new file mode 100644
index 0000000..999e760
--- /dev/null
+++ b/src/event/quic/bpf/ngx_quic_reuseport_helper.c
@@ -0,0 +1,140 @@
+#include <errno.h>
+#include <linux/string.h>
+#include <linux/udp.h>
+#include <linux/bpf.h>
+/*
+ * the bpf_helpers.h is not included into linux-headers, only available
+ * with kernel sources in "tools/lib/bpf/bpf_helpers.h" or in libbpf.
+ */
+#include <bpf/bpf_helpers.h>
+
+
+#if !defined(SEC)
+#define SEC(NAME)  __attribute__((section(NAME), used))
+#endif
+
+
+#if defined(LICENSE_GPL)
+
+/*
+ * To see debug:
+ *
+ *  echo 1 > /sys/kernel/debug/tracing/events/bpf_trace/enable
+ *  cat /sys/kernel/debug/tracing/trace_pipe
+ *  echo 0 > /sys/kernel/debug/tracing/events/bpf_trace/enable
+ */
+
+#define debugmsg(fmt, ...)                                                    \
+do {                                                                          \
+    char __buf[] = fmt;                                                       \
+    bpf_trace_printk(__buf, sizeof(__buf), ##__VA_ARGS__);                    \
+} while (0)
+
+#else
+
+#define debugmsg(fmt, ...)
+
+#endif
+
+char _license[] SEC("license") = LICENSE;
+
+/*****************************************************************************/
+
+#define NGX_QUIC_PKT_LONG        0x80  /* header form */
+#define NGX_QUIC_SERVER_CID_LEN  20
+
+
+#define advance_data(nbytes)                                                  \
+    offset += nbytes;                                                         \
+    if (start + offset > end) {                                               \
+        debugmsg("cannot read %ld bytes at offset %ld", nbytes, offset);      \
+        goto failed;                                                          \
+    }                                                                         \
+    data = start + offset - 1;
+
+
+#define ngx_quic_parse_uint64(p)                                              \
+    (((__u64)(p)[0] << 56) |                                                  \
+     ((__u64)(p)[1] << 48) |                                                  \
+     ((__u64)(p)[2] << 40) |                                                  \
+     ((__u64)(p)[3] << 32) |                                                  \
+     ((__u64)(p)[4] << 24) |                                                  \
+     ((__u64)(p)[5] << 16) |                                                  \
+     ((__u64)(p)[6] << 8)  |                                                  \
+     ((__u64)(p)[7]))
+
+/*
+ * actual map object is created by the "bpf" system call,
+ * all pointers to this variable are replaced by the bpf loader
+ */
+struct bpf_map_def SEC("maps") ngx_quic_sockmap;
+
+
+SEC(PROGNAME)
+int ngx_quic_select_socket_by_dcid(struct sk_reuseport_md *ctx)
+{
+    int             rc;
+    __u64           key;
+    size_t          len, offset;
+    unsigned char  *start, *end, *data, *dcid;
+
+    start = ctx->data;
+    end = (unsigned char *) ctx->data_end;
+    offset = 0;
+
+    advance_data(sizeof(struct udphdr)); /* data at UDP header */
+    advance_data(1); /* data at QUIC flags */
+
+    if (data[0] & NGX_QUIC_PKT_LONG) {
+
+        advance_data(4); /* data at QUIC version */
+        advance_data(1); /* data at DCID len */
+
+        len = data[0];   /* read DCID length */
+
+        if (len < 8) {
+            /* it's useless to search for key in such short DCID */
+            return SK_PASS;
+        }
+
+    } else {
+        len = NGX_QUIC_SERVER_CID_LEN;
+    }
+
+    dcid = &data[1];
+    advance_data(len); /* we expect the packet to have full DCID */
+
+    /* make verifier happy */
+    if (dcid + sizeof(__u64) > end) {
+        goto failed;
+    }
+
+    key = ngx_quic_parse_uint64(dcid);
+
+    rc = bpf_sk_select_reuseport(ctx, &ngx_quic_sockmap, &key, 0);
+
+    switch (rc) {
+    case 0:
+        debugmsg("nginx quic socket selected by key 0x%llx", key);
+        return SK_PASS;
+
+    /* kernel returns positive error numbers, errno.h defines positive */
+    case -ENOENT:
+        debugmsg("nginx quic default route for key 0x%llx", key);
+        /* let the default reuseport logic decide which socket to choose */
+        return SK_PASS;
+
+    default:
+        debugmsg("nginx quic bpf_sk_select_reuseport err: %d key 0x%llx",
+                 rc, key);
+        goto failed;
+    }
+
+failed:
+    /*
+     * SK_DROP will generate ICMP, but we may want to process "invalid" packet
+     * in userspace quic to investigate further and finally react properly
+     * (maybe ignore, maybe send something in response or close connection)
+     */
+    return SK_PASS;
+}
diff --git a/src/event/quic/ngx_event_quic.c b/src/event/quic/ngx_event_quic.c
new file mode 100644
index 0000000..e4690f7
--- /dev/null
+++ b/src/event/quic/ngx_event_quic.c
@@ -0,0 +1,1452 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_event.h>
+#include <ngx_event_quic_connection.h>
+
+
+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_handle_stateless_reset(ngx_connection_t *c,
+    ngx_quic_header_t *pkt);
+static void ngx_quic_input_handler(ngx_event_t *rev);
+static void ngx_quic_close_handler(ngx_event_t *ev);
+
+static ngx_int_t ngx_quic_handle_datagram(ngx_connection_t *c, ngx_buf_t *b,
+    ngx_quic_conf_t *conf);
+static ngx_int_t ngx_quic_handle_packet(ngx_connection_t *c,
+    ngx_quic_conf_t *conf, ngx_quic_header_t *pkt);
+static ngx_int_t ngx_quic_handle_payload(ngx_connection_t *c,
+    ngx_quic_header_t *pkt);
+static ngx_int_t ngx_quic_check_csid(ngx_quic_connection_t *qc,
+    ngx_quic_header_t *pkt);
+static ngx_int_t ngx_quic_handle_frames(ngx_connection_t *c,
+    ngx_quic_header_t *pkt);
+
+static void ngx_quic_push_handler(ngx_event_t *ev);
+
+
+static ngx_core_module_t  ngx_quic_module_ctx = {
+    ngx_string("quic"),
+    NULL,
+    NULL
+};
+
+
+ngx_module_t  ngx_quic_module = {
+    NGX_MODULE_V1,
+    &ngx_quic_module_ctx,                  /* module context */
+    NULL,                                  /* module directives */
+    NGX_CORE_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
+};
+
+
+#if (NGX_DEBUG)
+
+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 != (ngx_uint_t) -1) {
+            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->shutdown ? " shutdown" : "");
+        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" : "");
+
+    } 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);
+}
+
+#endif
+
+
+ngx_int_t
+ngx_quic_apply_transport_params(ngx_connection_t *c, ngx_quic_tp_t *ctp)
+{
+    ngx_str_t               scid;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    scid.data = qc->path->cid->id;
+    scid.len = qc->path->cid->len;
+
+    if (scid.len != ctp->initial_scid.len
+        || ngx_memcmp(scid.data, ctp->initial_scid.data, scid.len) != 0)
+    {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "quic client initial_source_connection_id mismatch");
+        return NGX_ERROR;
+    }
+
+    if (ctp->max_udp_payload_size < NGX_QUIC_MIN_INITIAL_SIZE
+        || 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 NGX_ERROR;
+    }
+
+    if (ctp->active_connection_id_limit < 2) {
+        qc->error = NGX_QUIC_ERR_TRANSPORT_PARAMETER_ERROR;
+        qc->error_reason = "invalid active_connection_id_limit";
+
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "quic active_connection_id_limit is invalid");
+        return NGX_ERROR;
+    }
+
+    if (ctp->ack_delay_exponent > 20) {
+        qc->error = NGX_QUIC_ERR_TRANSPORT_PARAMETER_ERROR;
+        qc->error_reason = "invalid ack_delay_exponent";
+
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "quic ack_delay_exponent is invalid");
+        return NGX_ERROR;
+    }
+
+    if (ctp->max_ack_delay >= 16384) {
+        qc->error = NGX_QUIC_ERR_TRANSPORT_PARAMETER_ERROR;
+        qc->error_reason = "invalid max_ack_delay";
+
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "quic max_ack_delay is invalid");
+        return NGX_ERROR;
+    }
+
+    if (ctp->max_idle_timeout > 0
+        && ctp->max_idle_timeout < qc->tp.max_idle_timeout)
+    {
+        qc->tp.max_idle_timeout = ctp->max_idle_timeout;
+    }
+
+    qc->streams.server_max_streams_bidi = ctp->initial_max_streams_bidi;
+    qc->streams.server_max_streams_uni = ctp->initial_max_streams_uni;
+
+    ngx_memcpy(&qc->ctp, ctp, sizeof(ngx_quic_tp_t));
+
+    return NGX_OK;
+}
+
+
+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_handle_datagram(c, c->buffer, conf);
+    if (rc != NGX_OK) {
+        ngx_quic_close_connection(c, rc);
+        return;
+    }
+
+    /* quic connection is now created */
+    qc = ngx_quic_get_connection(c);
+
+    ngx_add_timer(c->read, qc->tp.max_idle_timeout);
+
+    if (!qc->streams.initialized) {
+        ngx_add_timer(&qc->close, qc->conf->handshake_timeout);
+    }
+
+    ngx_quic_connstate_dbg(c);
+
+    c->read->handler = ngx_quic_input_handler;
+
+    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_connection_t  *qc;
+
+    qc = ngx_pcalloc(c->pool, sizeof(ngx_quic_connection_t));
+    if (qc == NULL) {
+        return NULL;
+    }
+
+    qc->keys = ngx_pcalloc(c->pool, sizeof(ngx_quic_keys_t));
+    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].sending);
+        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;
+
+    ngx_queue_init(&qc->free_frames);
+
+    ngx_quic_init_rtt(qc);
+
+    qc->pto.log = c->log;
+    qc->pto.data = c;
+    qc->pto.handler = ngx_quic_pto_handler;
+
+    qc->push.log = c->log;
+    qc->push.data = c;
+    qc->push.handler = ngx_quic_push_handler;
+
+    qc->close.log = c->log;
+    qc->close.data = c;
+    qc->close.handler = ngx_quic_close_handler;
+
+    qc->path_validation.log = c->log;
+    qc->path_validation.data = c;
+    qc->path_validation.handler = ngx_quic_path_handler;
+
+    qc->key_update.log = c->log;
+    qc->key_update.data = c;
+    qc->key_update.handler = ngx_quic_keys_update;
+
+    qc->conf = conf;
+
+    if (ngx_quic_init_transport_params(&qc->tp, conf) != NGX_OK) {
+        return NULL;
+    }
+
+    ctp = &qc->ctp;
+
+    /* defaults to be used before actual client parameters are received */
+    ctp->max_udp_payload_size = NGX_QUIC_MAX_UDP_PAYLOAD_SIZE;
+    ctp->ack_delay_exponent = NGX_QUIC_DEFAULT_ACK_DELAY_EXPONENT;
+    ctp->max_ack_delay = NGX_QUIC_DEFAULT_MAX_ACK_DELAY;
+    ctp->active_connection_id_limit = 2;
+
+    ngx_queue_init(&qc->streams.uninitialized);
+    ngx_queue_init(&qc->streams.free);
+
+    qc->streams.recv_max_data = qc->tp.initial_max_data;
+    qc->streams.recv_window = qc->streams.recv_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;
+
+    if (pkt->validated && pkt->retried) {
+        qc->tp.retry_scid.len = pkt->dcid.len;
+        qc->tp.retry_scid.data = ngx_pstrdup(c->pool, &pkt->dcid);
+        if (qc->tp.retry_scid.data == NULL) {
+            return NULL;
+        }
+    }
+
+    if (ngx_quic_keys_set_initial_secret(qc->keys, &pkt->dcid, c->log)
+        != NGX_OK)
+    {
+        return NULL;
+    }
+
+    qc->validated = pkt->validated;
+
+    if (ngx_quic_open_sockets(c, qc, pkt) != NGX_OK) {
+        ngx_quic_keys_cleanup(qc->keys);
+        return NULL;
+    }
+
+    c->idle = 1;
+    ngx_reusable_connection(c, 1);
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic connection created");
+
+    return qc;
+}
+
+
+static ngx_int_t
+ngx_quic_handle_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->first) {
+        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 || !cid->used) {
+            /*
+             * No stateless reset token in initial connection id.
+             * Don't accept a token from an unused 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 void
+ngx_quic_input_handler(ngx_event_t *rev)
+{
+    ngx_int_t               rc;
+    ngx_buf_t              *b;
+    ngx_connection_t       *c;
+    ngx_quic_connection_t  *qc;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, rev->log, 0, "quic input handler");
+
+    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) {
+        c->close = 0;
+
+        if (!ngx_exiting || !qc->streams.initialized) {
+            qc->error = NGX_QUIC_ERR_NO_ERROR;
+            qc->error_reason = "graceful shutdown";
+            ngx_quic_close_connection(c, NGX_ERROR);
+            return;
+        }
+
+        if (!qc->closing && qc->conf->shutdown) {
+            qc->conf->shutdown(c);
+        }
+
+        return;
+    }
+
+    b = c->udp->buffer;
+    if (b == NULL) {
+        return;
+    }
+
+    rc = ngx_quic_handle_datagram(c, b, NULL);
+
+    if (rc == NGX_ERROR) {
+        ngx_quic_close_connection(c, NGX_ERROR);
+        return;
+    }
+
+    if (rc == NGX_DONE) {
+        return;
+    }
+
+    /* rc == NGX_OK */
+
+    qc->send_timer_set = 0;
+    ngx_add_timer(rev, qc->tp.max_idle_timeout);
+
+    ngx_quic_connstate_dbg(c);
+}
+
+
+void
+ngx_quic_close_connection(ngx_connection_t *c, ngx_int_t rc)
+{
+    ngx_uint_t              i;
+    ngx_pool_t             *pool;
+    ngx_quic_send_ctx_t    *ctx;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    if (qc == NULL) {
+        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic packet rejected rc:%i, cleanup connection", rc);
+        goto quic_done;
+    }
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic close %s rc:%i",
+                   qc->closing ? "resumed": "initiated", rc);
+
+    if (!qc->closing) {
+
+        /* drop packets from retransmit queues, no ack is expected */
+        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);
+        }
+
+        if (qc->close.timer_set) {
+            ngx_del_timer(&qc->close);
+        }
+
+        if (rc == NGX_DONE) {
+
+            /*
+             * RFC 9000, 10.1.  Idle Timeout
+             *
+             *  If a max_idle_timeout is specified by either endpoint in its
+             *  transport parameters (Section 18.2), the connection is silently
+             *  closed and its state is discarded when it remains idle
+             */
+
+            /* this case also handles some errors from ngx_quic_run() */
+
+            ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                           "quic close silent drain:%d timedout:%d",
+                           qc->draining, c->read->timedout);
+        } else {
+
+            /*
+             * RFC 9000, 10.2.  Immediate Close
+             *
+             *  An endpoint sends a CONNECTION_CLOSE frame (Section 19.19)
+             *  to terminate the connection immediately.
+             */
+
+            if (qc->error == (ngx_uint_t) -1) {
+                qc->error = NGX_QUIC_ERR_INTERNAL_ERROR;
+                qc->error_app = 0;
+            }
+
+            ngx_log_debug5(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                           "quic close immediate term:%d drain:%d "
+                           "%serror:%ui \"%s\"",
+                           rc == NGX_ERROR ? 1 : 0, qc->draining,
+                           qc->error_app ? "app " : "", qc->error,
+                           qc->error_reason ? qc->error_reason : "");
+
+            for (i = 0; i < NGX_QUIC_SEND_CTX_LAST; i++) {
+                ctx = &qc->send_ctx[i];
+
+                if (!ngx_quic_keys_available(qc->keys, ctx->level, 1)) {
+                    continue;
+                }
+
+                qc->error_level = ctx->level;
+                (void) ngx_quic_send_cc(c);
+
+                if (rc == NGX_OK) {
+                    ngx_add_timer(&qc->close, 3 * ngx_quic_pto(c, ctx));
+                }
+            }
+        }
+
+        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;
+    }
+
+    if (qc->push.timer_set) {
+        ngx_del_timer(&qc->push);
+    }
+
+    if (qc->pto.timer_set) {
+        ngx_del_timer(&qc->pto);
+    }
+
+    if (qc->path_validation.timer_set) {
+        ngx_del_timer(&qc->path_validation);
+    }
+
+    if (qc->push.posted) {
+        ngx_delete_posted_event(&qc->push);
+    }
+
+    if (qc->key_update.posted) {
+        ngx_delete_posted_event(&qc->key_update);
+    }
+
+    if (qc->close.timer_set) {
+        return;
+    }
+
+    if (qc->close.posted) {
+        ngx_delete_posted_event(&qc->close);
+    }
+
+    ngx_quic_close_sockets(c);
+
+    ngx_quic_keys_cleanup(qc->keys);
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, "quic close completed");
+
+    /* may be tested from SSL callback during SSL shutdown */
+    c->udp = NULL;
+
+quic_done:
+
+    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);
+}
+
+
+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);
+
+    if (qc->closing) {
+        return;
+    }
+
+    qc->error = err;
+    qc->error_reason = reason;
+    qc->error_app = 1;
+    qc->error_ftype = 0;
+
+    ngx_post_event(&qc->close, &ngx_posted_events);
+}
+
+
+void
+ngx_quic_shutdown_connection(ngx_connection_t *c, ngx_uint_t err,
+    const char *reason)
+{
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+    qc->shutdown = 1;
+    qc->shutdown_code = err;
+    qc->shutdown_reason = reason;
+
+    ngx_quic_shutdown_quic(c);
+}
+
+
+static void
+ngx_quic_close_handler(ngx_event_t *ev)
+{
+    ngx_connection_t  *c;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, ev->log, 0, "quic close handler");
+
+    c = ev->data;
+
+    ngx_quic_close_connection(c, NGX_OK);
+}
+
+
+static ngx_int_t
+ngx_quic_handle_datagram(ngx_connection_t *c, ngx_buf_t *b,
+    ngx_quic_conf_t *conf)
+{
+    size_t                  size;
+    u_char                 *p, *start;
+    ngx_int_t               rc;
+    ngx_uint_t              good;
+    ngx_quic_path_t        *path;
+    ngx_quic_header_t       pkt;
+    ngx_quic_connection_t  *qc;
+
+    good = 0;
+    path = NULL;
+
+    size = b->last - b->pos;
+
+    p = start = 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.first = (p == start) ? 1 : 0;
+        pkt.path = path;
+        pkt.flags = p[0];
+        pkt.raw->pos++;
+
+        rc = ngx_quic_handle_packet(c, conf, &pkt);
+
+#if (NGX_DEBUG)
+        if (pkt.parsed) {
+            ngx_log_debug5(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                           "quic packet done rc:%i level:%s"
+                           " decr:%d pn:%L perr:%ui",
+                           rc, ngx_quic_level_name(pkt.level),
+                           pkt.decrypted, pkt.pn, pkt.error);
+        } else {
+            ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                           "quic packet done rc:%i parse failed", rc);
+        }
+#endif
+
+        if (rc == NGX_ERROR || rc == NGX_DONE) {
+            return rc;
+        }
+
+        if (rc == NGX_OK) {
+            good = 1;
+        }
+
+        path = pkt.path; /* preserve packet path from 1st packet */
+
+        /* 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:
+         *
+         * RFC 9000, 12.2.  Coalescing Packets
+         *
+         * For example, if decryption fails (because the keys are
+         * not available or for 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;
+
+        p = b->pos;
+    }
+
+    if (!good) {
+        return NGX_DONE;
+    }
+
+    qc = ngx_quic_get_connection(c);
+
+    if (qc) {
+        qc->received += size;
+
+        if ((uint64_t) (c->sent + qc->received) / 8 >
+            (qc->streams.sent + qc->streams.recv_last) + 1048576)
+        {
+            ngx_log_error(NGX_LOG_INFO, c->log, 0, "quic flood detected");
+
+            qc->error = NGX_QUIC_ERR_NO_ERROR;
+            qc->error_reason = "QUIC flood detected";
+            return NGX_ERROR;
+        }
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_handle_packet(ngx_connection_t *c, ngx_quic_conf_t *conf,
+    ngx_quic_header_t *pkt)
+{
+    ngx_int_t               rc;
+    ngx_quic_socket_t      *qsock;
+    ngx_quic_connection_t  *qc;
+
+    c->log->action = "parsing quic packet";
+
+    rc = ngx_quic_parse_packet(pkt);
+
+    if (rc == NGX_ERROR) {
+        return NGX_DECLINED;
+    }
+
+    pkt->parsed = 1;
+
+    c->log->action = "handling quic packet";
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic packet rx dcid len:%uz %xV",
+                   pkt->dcid.len, &pkt->dcid);
+
+#if (NGX_DEBUG)
+    if (pkt->level != ssl_encryption_application) {
+        ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic packet rx scid len:%uz %xV",
+                       pkt->scid.len, &pkt->scid);
+    }
+
+    if (pkt->level == ssl_encryption_initial) {
+        ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic address validation token len:%uz %xV",
+                       pkt->token.len, &pkt->token);
+    }
+#endif
+
+    qc = ngx_quic_get_connection(c);
+
+    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 (pkt->first) {
+                qsock = ngx_quic_get_socket(c);
+
+                if (ngx_cmp_sockaddr(&qsock->sockaddr.sockaddr, qsock->socklen,
+                                     qc->path->sockaddr, qc->path->socklen, 1)
+                    != NGX_OK)
+                {
+                    /* packet comes from unknown path, possibly migration */
+                    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                                   "quic too early migration attempt");
+                    return NGX_DONE;
+                }
+            }
+
+            if (ngx_quic_check_csid(qc, pkt) != NGX_OK) {
+                return NGX_DECLINED;
+            }
+
+        }
+
+        rc = ngx_quic_handle_payload(c, pkt);
+
+        if (rc == NGX_DECLINED && pkt->level == ssl_encryption_application) {
+            if (ngx_quic_handle_stateless_reset(c, pkt) == NGX_OK) {
+                ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                              "quic stateless reset packet detected");
+
+                qc->draining = 1;
+                ngx_post_event(&qc->close, &ngx_posted_events);
+
+                return NGX_OK;
+            }
+        }
+
+        return rc;
+    }
+
+    /* packet does not belong to a connection */
+
+    if (rc == NGX_ABORT) {
+        return ngx_quic_negotiate_version(c, pkt);
+    }
+
+    if (pkt->level == ssl_encryption_application) {
+        return ngx_quic_send_stateless_reset(c, conf, pkt);
+    }
+
+    if (pkt->level != ssl_encryption_initial) {
+        ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic expected initial, got handshake");
+        return NGX_ERROR;
+    }
+
+    c->log->action = "handling initial packet";
+
+    if (pkt->dcid.len < NGX_QUIC_CID_LEN_MIN) {
+        /* RFC 9000, 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;
+    }
+
+    /* process retry and initialize connection IDs */
+
+    if (pkt->token.len) {
+
+        rc = ngx_quic_validate_token(c, conf->av_token_key, pkt);
+
+        if (rc == NGX_ERROR) {
+            /* internal error */
+            return NGX_ERROR;
+
+        } else if (rc == NGX_ABORT) {
+            /* token cannot be decrypted */
+            return ngx_quic_send_early_cc(c, pkt,
+                                          NGX_QUIC_ERR_INVALID_TOKEN,
+                                          "cannot decrypt token");
+        } else if (rc == NGX_DECLINED) {
+            /* token is invalid */
+
+            if (pkt->retried) {
+                /* invalid address validation token */
+                return ngx_quic_send_early_cc(c, pkt,
+                                          NGX_QUIC_ERR_INVALID_TOKEN,
+                                          "invalid address validation token");
+            } else if (conf->retry) {
+                /* invalid NEW_TOKEN */
+                return ngx_quic_send_retry(c, conf, pkt);
+            }
+        }
+
+        /* NGX_OK */
+
+    } else if (conf->retry) {
+        return ngx_quic_send_retry(c, conf, pkt);
+
+    } else {
+        pkt->odcid = pkt->dcid;
+    }
+
+    if (ngx_terminate || ngx_exiting) {
+        if (conf->retry) {
+            return ngx_quic_send_retry(c, conf, pkt);
+        }
+
+        return NGX_ERROR;
+    }
+
+    c->log->action = "creating quic connection";
+
+    qc = ngx_quic_new_connection(c, conf, pkt);
+    if (qc == NULL) {
+        return NGX_ERROR;
+    }
+
+    return ngx_quic_handle_payload(c, pkt);
+}
+
+
+static ngx_int_t
+ngx_quic_handle_payload(ngx_connection_t *c, 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];
+
+    qc = ngx_quic_get_connection(c);
+
+    qc->error = (ngx_uint_t) -1;
+    qc->error_reason = 0;
+
+    c->log->action = "decrypting packet";
+
+    if (!ngx_quic_keys_available(qc->keys, pkt->level, 0)) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "quic no %s keys, ignoring packet",
+                      ngx_quic_level_name(pkt->level));
+        return NGX_DECLINED;
+    }
+
+#if !defined (OPENSSL_IS_BORINGSSL)
+    /* OpenSSL provides read keys for an application level before it's ready */
+
+    if (pkt->level == ssl_encryption_application && !c->ssl->handshaked) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "quic no %s keys ready, ignoring packet",
+                      ngx_quic_level_name(pkt->level));
+        return NGX_DECLINED;
+    }
+#endif
+
+    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;
+
+    c->log->action = "handling decrypted packet";
+
+    if (pkt->path == NULL) {
+        rc = ngx_quic_set_path(c, pkt);
+        if (rc != NGX_OK) {
+            return rc;
+        }
+    }
+
+    if (c->ssl == NULL) {
+        if (ngx_quic_init_connection(c) != NGX_OK) {
+            return NGX_ERROR;
+        }
+    }
+
+    if (pkt->level == ssl_encryption_handshake) {
+        /*
+         * RFC 9001, 4.9.1.  Discarding Initial Keys
+         *
+         * 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->path->validated) {
+            qc->path->validated = 1;
+            ngx_quic_path_dbg(c, "in handshake", qc->path);
+            ngx_post_event(&qc->push, &ngx_posted_events);
+        }
+    }
+
+    if (qc->closing) {
+        /*
+         * RFC 9000, 10.2.  Immediate Close
+         *
+         * ... delayed or reordered packets are properly discarded.
+         *
+         *  In the closing state, 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);
+    }
+
+    pkt->received = ngx_current_msec;
+
+    c->log->action = "handling payload";
+
+    if (pkt->level != ssl_encryption_application) {
+        return ngx_quic_handle_frames(c, pkt);
+    }
+
+    if (!pkt->key_update) {
+        return ngx_quic_handle_frames(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_handle_frames(c, pkt);
+    if (rc != NGX_OK) {
+        return rc;
+    }
+
+    ngx_post_event(&qc->key_update, &ngx_posted_events);
+
+    return NGX_OK;
+}
+
+
+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_socket_t      *qsock;
+    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, 0)
+        && !ngx_quic_keys_available(qc->keys, level, 1))
+    {
+        return;
+    }
+
+    ngx_quic_keys_discard(qc->keys, level);
+
+    qc->pto_count = 0;
+
+    ctx = ngx_quic_get_send_ctx(qc, level);
+
+    ngx_quic_free_buffer(c, &ctx->crypto);
+
+    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_free_frame(c, f);
+    }
+
+    if (level == ssl_encryption_initial) {
+        /* close temporary listener with initial dcid */
+        qsock = ngx_quic_find_socket(c, NGX_QUIC_UNSET_PN);
+        if (qsock) {
+            ngx_quic_close_socket(c, qsock);
+        }
+    }
+
+    ctx->send_ack = 0;
+
+    ngx_quic_set_lost_timer(c);
+}
+
+
+static ngx_int_t
+ngx_quic_check_csid(ngx_quic_connection_t *qc, ngx_quic_header_t *pkt)
+{
+    ngx_queue_t           *q;
+    ngx_quic_client_id_t  *cid;
+
+    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_handle_frames(ngx_connection_t *c, ngx_quic_header_t *pkt)
+{
+    u_char                 *end, *p;
+    ssize_t                 len;
+    ngx_buf_t               buf;
+    ngx_uint_t              do_close, nonprobing;
+    ngx_chain_t             chain;
+    ngx_quic_frame_t        frame;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    p = pkt->payload.data;
+    end = p + pkt->payload.len;
+
+    do_close = 0;
+    nonprobing = 0;
+
+    while (p < end) {
+
+        c->log->action = "parsing frames";
+
+        ngx_memzero(&frame, sizeof(ngx_quic_frame_t));
+        ngx_memzero(&buf, sizeof(ngx_buf_t));
+        buf.temporary = 1;
+
+        chain.buf = &buf;
+        chain.next = NULL;
+        frame.data = &chain;
+
+        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) {
+        /* probing frames */
+        case NGX_QUIC_FT_PADDING:
+        case NGX_QUIC_FT_PATH_CHALLENGE:
+        case NGX_QUIC_FT_PATH_RESPONSE:
+        case NGX_QUIC_FT_NEW_CONNECTION_ID:
+            break;
+
+        /* non-probing frames */
+        default:
+            nonprobing = 1;
+            break;
+        }
+
+        switch (frame.type) {
+
+        case NGX_QUIC_FT_ACK:
+            if (ngx_quic_handle_ack_frame(c, pkt, &frame) != 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_STREAM:
+
+            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_DATA_BLOCKED:
+
+            if (ngx_quic_handle_data_blocked_frame(c, pkt,
+                                                   &frame.u.data_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_PATH_RESPONSE:
+
+            if (ngx_quic_handle_path_response_frame(c, &frame.u.path_response)
+                != NGX_OK)
+            {
+                return NGX_ERROR;
+            }
+
+            break;
+
+        case NGX_QUIC_FT_NEW_CONNECTION_ID:
+
+            if (ngx_quic_handle_new_connection_id_frame(c, &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,
+                                                           &frame.u.retire_cid)
+                != NGX_OK)
+            {
+                return NGX_ERROR;
+            }
+
+            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_post_event(&qc->close, &ngx_posted_events);
+    }
+
+    if (pkt->path != qc->path && nonprobing) {
+
+        /*
+         * RFC 9000, 9.2.  Initiating Connection Migration
+         *
+         * An endpoint can migrate a connection to a new local
+         * address by sending packets containing non-probing frames
+         * from that address.
+         */
+        if (ngx_quic_handle_migration(c, pkt) != NGX_OK) {
+            return NGX_ERROR;
+        }
+    }
+
+    if (ngx_quic_ack_packet(c, pkt) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}
+
+
+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 handler");
+
+    c = ev->data;
+
+    if (ngx_quic_output(c) != NGX_OK) {
+        ngx_quic_close_connection(c, NGX_ERROR);
+        return;
+    }
+
+    ngx_quic_connstate_dbg(c);
+}
+
+
+void
+ngx_quic_shutdown_quic(ngx_connection_t *c)
+{
+    ngx_quic_connection_t  *qc;
+
+    if (c->reusable) {
+        qc = ngx_quic_get_connection(c);
+        ngx_quic_finalize_connection(c, qc->shutdown_code, qc->shutdown_reason);
+    }
+}
diff --git a/src/event/quic/ngx_event_quic.h b/src/event/quic/ngx_event_quic.h
new file mode 100644
index 0000000..1520167
--- /dev/null
+++ b/src/event/quic/ngx_event_quic.h
@@ -0,0 +1,129 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_EVENT_QUIC_H_INCLUDED_
+#define _NGX_EVENT_QUIC_H_INCLUDED_
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+
+#define NGX_QUIC_MAX_UDP_PAYLOAD_SIZE        65527
+
+#define NGX_QUIC_DEFAULT_ACK_DELAY_EXPONENT  3
+#define NGX_QUIC_DEFAULT_MAX_ACK_DELAY       25
+#define NGX_QUIC_DEFAULT_HOST_KEY_LEN        32
+#define NGX_QUIC_SR_KEY_LEN                  32
+#define NGX_QUIC_AV_KEY_LEN                  32
+
+#define NGX_QUIC_SR_TOKEN_LEN                16
+
+#define NGX_QUIC_MIN_INITIAL_SIZE            1200
+
+#define NGX_QUIC_STREAM_SERVER_INITIATED     0x01
+#define NGX_QUIC_STREAM_UNIDIRECTIONAL       0x02
+
+
+typedef ngx_int_t (*ngx_quic_init_pt)(ngx_connection_t *c);
+typedef void (*ngx_quic_shutdown_pt)(ngx_connection_t *c);
+
+
+typedef enum {
+    NGX_QUIC_STREAM_SEND_READY = 0,
+    NGX_QUIC_STREAM_SEND_SEND,
+    NGX_QUIC_STREAM_SEND_DATA_SENT,
+    NGX_QUIC_STREAM_SEND_DATA_RECVD,
+    NGX_QUIC_STREAM_SEND_RESET_SENT,
+    NGX_QUIC_STREAM_SEND_RESET_RECVD
+} ngx_quic_stream_send_state_e;
+
+
+typedef enum {
+    NGX_QUIC_STREAM_RECV_RECV = 0,
+    NGX_QUIC_STREAM_RECV_SIZE_KNOWN,
+    NGX_QUIC_STREAM_RECV_DATA_RECVD,
+    NGX_QUIC_STREAM_RECV_DATA_READ,
+    NGX_QUIC_STREAM_RECV_RESET_RECVD,
+    NGX_QUIC_STREAM_RECV_RESET_READ
+} ngx_quic_stream_recv_state_e;
+
+
+typedef struct {
+    uint64_t                       size;
+    uint64_t                       offset;
+    uint64_t                       last_offset;
+    ngx_chain_t                   *chain;
+    ngx_chain_t                   *last_chain;
+} ngx_quic_buffer_t;
+
+
+typedef struct {
+    ngx_ssl_t                     *ssl;
+
+    ngx_flag_t                     retry;
+    ngx_flag_t                     gso_enabled;
+    ngx_flag_t                     disable_active_migration;
+    ngx_msec_t                     handshake_timeout;
+    ngx_msec_t                     idle_timeout;
+    ngx_str_t                      host_key;
+    size_t                         stream_buffer_size;
+    ngx_uint_t                     max_concurrent_streams_bidi;
+    ngx_uint_t                     max_concurrent_streams_uni;
+    ngx_uint_t                     active_connection_id_limit;
+    ngx_int_t                      stream_close_code;
+    ngx_int_t                      stream_reject_code_uni;
+    ngx_int_t                      stream_reject_code_bidi;
+
+    ngx_quic_init_pt               init;
+    ngx_quic_shutdown_pt           shutdown;
+
+    u_char                         av_token_key[NGX_QUIC_AV_KEY_LEN];
+    u_char                         sr_token_key[NGX_QUIC_SR_KEY_LEN];
+} ngx_quic_conf_t;
+
+
+struct ngx_quic_stream_s {
+    ngx_rbtree_node_t              node;
+    ngx_queue_t                    queue;
+    ngx_connection_t              *parent;
+    ngx_connection_t              *connection;
+    uint64_t                       id;
+    uint64_t                       sent;
+    uint64_t                       acked;
+    uint64_t                       send_max_data;
+    uint64_t                       send_offset;
+    uint64_t                       send_final_size;
+    uint64_t                       recv_max_data;
+    uint64_t                       recv_offset;
+    uint64_t                       recv_window;
+    uint64_t                       recv_last;
+    uint64_t                       recv_final_size;
+    ngx_quic_buffer_t              send;
+    ngx_quic_buffer_t              recv;
+    ngx_quic_stream_send_state_e   send_state;
+    ngx_quic_stream_recv_state_e   recv_state;
+    unsigned                       cancelable:1;
+    unsigned                       fin_acked:1;
+};
+
+
+void ngx_quic_recvmsg(ngx_event_t *ev);
+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);
+void ngx_quic_shutdown_connection(ngx_connection_t *c, ngx_uint_t err,
+    const char *reason);
+ngx_int_t ngx_quic_reset_stream(ngx_connection_t *c, ngx_uint_t err);
+ngx_int_t ngx_quic_shutdown_stream(ngx_connection_t *c, int how);
+void ngx_quic_cancelable_stream(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);
+ngx_int_t ngx_quic_derive_key(ngx_log_t *log, const char *label,
+    ngx_str_t *secret, ngx_str_t *salt, u_char *out, size_t len);
+
+#endif /* _NGX_EVENT_QUIC_H_INCLUDED_ */
diff --git a/src/event/quic/ngx_event_quic_ack.c b/src/event/quic/ngx_event_quic_ack.c
new file mode 100644
index 0000000..c7ffd44
--- /dev/null
+++ b/src/event/quic/ngx_event_quic_ack.c
@@ -0,0 +1,1188 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_event.h>
+#include <ngx_event_quic_connection.h>
+
+
+#define NGX_QUIC_MAX_ACK_GAP                 2
+
+/* RFC 9002, 6.1.1. Packet Threshold: kPacketThreshold */
+#define NGX_QUIC_PKT_THR                     3 /* packets */
+/* RFC 9002, 6.1.2. Time Threshold: kGranularity */
+#define NGX_QUIC_TIME_GRANULARITY            1 /* ms */
+
+/* RFC 9002, 7.6.1. Duration: kPersistentCongestionThreshold */
+#define NGX_QUIC_PERSISTENT_CONGESTION_THR   3
+
+
+/* send time of ACK'ed packets */
+typedef struct {
+    ngx_msec_t                               max_pn;
+    ngx_msec_t                               oldest;
+    ngx_msec_t                               newest;
+} ngx_quic_ack_stat_t;
+
+
+static ngx_inline ngx_msec_t ngx_quic_lost_threshold(ngx_quic_connection_t *qc);
+static void ngx_quic_rtt_sample(ngx_connection_t *c, ngx_quic_ack_frame_t *ack,
+    enum ssl_encryption_level_t level, ngx_msec_t send_time);
+static ngx_int_t ngx_quic_handle_ack_frame_range(ngx_connection_t *c,
+    ngx_quic_send_ctx_t *ctx, uint64_t min, uint64_t max,
+    ngx_quic_ack_stat_t *st);
+static void ngx_quic_drop_ack_ranges(ngx_connection_t *c,
+    ngx_quic_send_ctx_t *ctx, uint64_t pn);
+static ngx_int_t ngx_quic_detect_lost(ngx_connection_t *c,
+    ngx_quic_ack_stat_t *st);
+static ngx_msec_t ngx_quic_pcg_duration(ngx_connection_t *c);
+static void ngx_quic_persistent_congestion(ngx_connection_t *c);
+static void ngx_quic_congestion_lost(ngx_connection_t *c,
+    ngx_quic_frame_t *frame);
+static void ngx_quic_lost_handler(ngx_event_t *ev);
+
+
+/* RFC 9002, 6.1.2. Time Threshold: kTimeThreshold, kGranularity */
+static ngx_inline ngx_msec_t
+ngx_quic_lost_threshold(ngx_quic_connection_t *qc)
+{
+    ngx_msec_t  thr;
+
+    thr = ngx_max(qc->latest_rtt, qc->avg_rtt);
+    thr += thr >> 3;
+
+    return ngx_max(thr, NGX_QUIC_TIME_GRANULARITY);
+}
+
+
+ngx_int_t
+ngx_quic_handle_ack_frame(ngx_connection_t *c, ngx_quic_header_t *pkt,
+    ngx_quic_frame_t *f)
+{
+    ssize_t                 n;
+    u_char                 *pos, *end;
+    uint64_t                min, max, gap, range;
+    ngx_uint_t              i;
+    ngx_quic_ack_stat_t     send_time;
+    ngx_quic_send_ctx_t    *ctx;
+    ngx_quic_ack_frame_t   *ack;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    ctx = ngx_quic_get_send_ctx(qc, pkt->level);
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic ngx_quic_handle_ack_frame level:%d", pkt->level);
+
+    ack = &f->u.ack;
+
+    /*
+     * RFC 9000, 19.3.1.  ACK Ranges
+     *
+     *  If any computed packet number is negative, an endpoint MUST
+     *  generate a connection error of type FRAME_ENCODING_ERROR.
+     */
+
+    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;
+
+    send_time.oldest = NGX_TIMER_INFINITE;
+    send_time.newest = NGX_TIMER_INFINITE;
+
+    if (ngx_quic_handle_ack_frame_range(c, ctx, min, max, &send_time)
+        != NGX_OK)
+    {
+        return NGX_ERROR;
+    }
+
+    /* RFC 9000, 13.2.4.  Limiting Ranges by Tracking 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);
+
+        /*
+         * RFC 9002, 5.1.  Generating RTT Samples
+         *
+         *  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.max_pn != NGX_TIMER_INFINITE) {
+            ngx_quic_rtt_sample(c, ack, pkt->level, send_time.max_pn);
+        }
+    }
+
+    if (f->data) {
+        pos = f->data->buf->pos;
+        end = f->data->buf->last;
+
+    } else {
+        pos = NULL;
+        end = NULL;
+    }
+
+    for (i = 0; i < ack->range_count; i++) {
+
+        n = ngx_quic_parse_ack_range(pkt->log, pos, end, &gap, &range);
+        if (n == NGX_ERROR) {
+            return NGX_ERROR;
+        }
+        pos += n;
+
+        if (gap + 2 > min) {
+            qc->error = NGX_QUIC_ERR_FRAME_ENCODING_ERROR;
+            ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                          "quic invalid range:%ui in ack frame", i);
+            return NGX_ERROR;
+        }
+
+        max = min - gap - 2;
+
+        if (range > max) {
+            qc->error = NGX_QUIC_ERR_FRAME_ENCODING_ERROR;
+            ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                          "quic invalid range:%ui in ack frame", i);
+            return NGX_ERROR;
+        }
+
+        min = max - range;
+
+        if (ngx_quic_handle_ack_frame_range(c, ctx, min, max, &send_time)
+            != NGX_OK)
+        {
+            return NGX_ERROR;
+        }
+    }
+
+    return ngx_quic_detect_lost(c, &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)
+{
+    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;
+        qc->first_rtt = ngx_current_msec;
+
+    } else {
+        qc->min_rtt = ngx_min(qc->min_rtt, latest_rtt);
+
+        ack_delay = (ack->delay << qc->ctp.ack_delay_exponent) / 1000;
+
+        if (c->ssl->handshaked) {
+            ack_delay = ngx_min(ack_delay, qc->ctp.max_ack_delay);
+        }
+
+        adjusted_rtt = latest_rtt;
+
+        if (qc->min_rtt + ack_delay < latest_rtt) {
+            adjusted_rtt -= ack_delay;
+        }
+
+        rttvar_sample = ngx_abs((ngx_msec_int_t) (qc->avg_rtt - adjusted_rtt));
+        qc->rttvar += (rttvar_sample >> 2) - (qc->rttvar >> 2);
+        qc->avg_rtt += (adjusted_rtt >> 3) - (qc->avg_rtt >> 3);
+    }
+
+    ngx_log_debug4(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic rtt sample latest:%M min:%M avg:%M var:%M",
+                   latest_rtt, qc->min_rtt, qc->avg_rtt, qc->rttvar);
+}
+
+
+static ngx_int_t
+ngx_quic_handle_ack_frame_range(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx,
+    uint64_t min, uint64_t max, ngx_quic_ack_stat_t *st)
+{
+    ngx_uint_t              found;
+    ngx_queue_t            *q;
+    ngx_quic_frame_t       *f;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    if (ctx->level == ssl_encryption_application) {
+        if (ngx_quic_handle_path_mtu(c, qc->path, min, max) != NGX_OK) {
+            return NGX_ERROR;
+        }
+    }
+
+    st->max_pn = NGX_TIMER_INFINITE;
+    found = 0;
+
+    q = ngx_queue_head(&ctx->sent);
+
+    while (q != ngx_queue_sentinel(&ctx->sent)) {
+
+        f = ngx_queue_data(q, ngx_quic_frame_t, queue);
+        q = ngx_queue_next(q);
+
+        if (f->pnum > max) {
+            break;
+        }
+
+        if (f->pnum >= min) {
+            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_STREAM:
+            case NGX_QUIC_FT_RESET_STREAM:
+                ngx_quic_handle_stream_ack(c, f);
+                break;
+            }
+
+            if (f->pnum == max) {
+                st->max_pn = f->send_time;
+            }
+
+            /* save earliest and latest send times of frames ack'ed */
+            if (st->oldest == NGX_TIMER_INFINITE || f->send_time < st->oldest) {
+                st->oldest = f->send_time;
+            }
+
+            if (st->newest == NGX_TIMER_INFINITE || f->send_time > st->newest) {
+                st->newest = f->send_time;
+            }
+
+            ngx_queue_remove(&f->queue);
+            ngx_quic_free_frame(c, f);
+            found = 1;
+        }
+    }
+
+    if (!found) {
+
+        if (max < ctx->pnum) {
+            /* duplicate ACK or ACK for non-ack-eliciting frame */
+            return NGX_OK;
+        }
+
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "quic ACK for the packet not sent");
+
+        qc->error = NGX_QUIC_ERR_PROTOCOL_VIOLATION;
+        qc->error_ftype = NGX_QUIC_FT_ACK;
+        qc->error_reason = "unknown packet number";
+
+        return NGX_ERROR;
+    }
+
+    if (!qc->push.timer_set) {
+        ngx_post_event(&qc->push, &ngx_posted_events);
+    }
+
+    qc->pto_count = 0;
+
+    return NGX_OK;
+}
+
+
+void
+ngx_quic_congestion_ack(ngx_connection_t *c, ngx_quic_frame_t *f)
+{
+    ngx_uint_t              blocked;
+    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;
+
+    if (f->pnum < qc->rst_pnum) {
+        return;
+    }
+
+    blocked = (cg->in_flight >= cg->window) ? 1 : 0;
+
+    cg->in_flight -= f->plen;
+
+    timer = f->send_time - 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);
+
+        goto done;
+    }
+
+    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;
+    }
+
+done:
+
+    if (blocked && cg->in_flight < cg->window) {
+        ngx_post_event(&qc->push, &ngx_posted_events);
+    }
+}
+
+
+static void
+ngx_quic_drop_ack_ranges(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx,
+    uint64_t pn)
+{
+    uint64_t               base;
+    ngx_uint_t             i, smallest, largest;
+    ngx_quic_ack_range_t  *r;
+
+    ngx_log_debug4(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic ngx_quic_drop_ack_ranges pn:%uL largest:%uL"
+                   " fr:%uL nranges:%ui", pn, ctx->largest_range,
+                   ctx->first_range, ctx->nranges);
+
+    base = ctx->largest_range;
+
+    if (base == NGX_QUIC_UNSET_PN) {
+        return;
+    }
+
+    if (ctx->pending_ack != NGX_QUIC_UNSET_PN && pn >= ctx->pending_ack) {
+        ctx->pending_ack = NGX_QUIC_UNSET_PN;
+    }
+
+    largest = base;
+    smallest = largest - ctx->first_range;
+
+    if (pn >= largest) {
+        ctx->largest_range = NGX_QUIC_UNSET_PN;
+        ctx->first_range = 0;
+        ctx->nranges = 0;
+        return;
+    }
+
+    if (pn >= smallest) {
+        ctx->first_range = largest - pn - 1;
+        ctx->nranges = 0;
+        return;
+    }
+
+    for (i = 0; i < ctx->nranges; i++) {
+        r = &ctx->ranges[i];
+
+        largest = smallest - r->gap - 2;
+        smallest = largest - r->range;
+
+        if (pn >= largest) {
+            ctx->nranges = i;
+            return;
+        }
+        if (pn >= smallest) {
+            r->range = largest - pn - 1;
+            ctx->nranges = i + 1;
+            return;
+        }
+    }
+}
+
+
+static ngx_int_t
+ngx_quic_detect_lost(ngx_connection_t *c, ngx_quic_ack_stat_t *st)
+{
+    ngx_uint_t              i, nlost;
+    ngx_msec_t              now, wait, thr, oldest, newest;
+    ngx_queue_t            *q;
+    ngx_quic_frame_t       *start;
+    ngx_quic_send_ctx_t    *ctx;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+    now = ngx_current_msec;
+    thr = ngx_quic_lost_threshold(qc);
+
+    /* send time of lost packets across all send contexts */
+    oldest = NGX_TIMER_INFINITE;
+    newest = NGX_TIMER_INFINITE;
+
+    nlost = 0;
+
+    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->send_time + thr - now;
+
+            ngx_log_debug4(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                           "quic detect_lost pnum:%uL thr:%M wait:%i level:%d",
+                           start->pnum, thr, (ngx_int_t) wait, start->level);
+
+            if ((ngx_msec_int_t) wait > 0
+                && ctx->largest_ack - start->pnum < NGX_QUIC_PKT_THR)
+            {
+                break;
+            }
+
+            if (start->send_time > qc->first_rtt) {
+
+                if (oldest == NGX_TIMER_INFINITE || start->send_time < oldest) {
+                    oldest = start->send_time;
+                }
+
+                if (newest == NGX_TIMER_INFINITE || start->send_time > newest) {
+                    newest = start->send_time;
+                }
+
+                nlost++;
+            }
+
+            ngx_quic_resend_frames(c, ctx);
+        }
+    }
+
+
+    /* RFC 9002, 7.6.2.  Establishing Persistent Congestion */
+
+    /*
+     * Once acknowledged, packets are no longer tracked. Thus no send time
+     * information is available for such packets. This limits persistent
+     * congestion algorithm to packets mentioned within ACK ranges of the
+     * latest ACK frame.
+     */
+
+    if (st && nlost >= 2 && (st->newest < oldest || st->oldest > newest)) {
+
+        if (newest - oldest > ngx_quic_pcg_duration(c)) {
+            ngx_quic_persistent_congestion(c);
+        }
+    }
+
+    ngx_quic_set_lost_timer(c);
+
+    return NGX_OK;
+}
+
+
+static ngx_msec_t
+ngx_quic_pcg_duration(ngx_connection_t *c)
+{
+    ngx_msec_t              duration;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    duration = qc->avg_rtt;
+    duration += ngx_max(4 * qc->rttvar, NGX_QUIC_TIME_GRANULARITY);
+    duration += qc->ctp.max_ack_delay;
+    duration *= NGX_QUIC_PERSISTENT_CONGESTION_THR;
+
+    return duration;
+}
+
+
+static void
+ngx_quic_persistent_congestion(ngx_connection_t *c)
+{
+    ngx_quic_congestion_t  *cg;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+    cg = &qc->congestion;
+
+    cg->recovery_start = ngx_current_msec;
+    cg->window = qc->tp.max_udp_payload_size * 2;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic persistent congestion win:%uz", cg->window);
+}
+
+
+void
+ngx_quic_resend_frames(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx)
+{
+    uint64_t                pnum;
+    ngx_queue_t            *q;
+    ngx_quic_frame_t       *f, *start;
+    ngx_quic_stream_t      *qs;
+    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);
+    pnum = start->pnum;
+
+    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 != pnum) {
+            break;
+        }
+
+        q = ngx_queue_next(q);
+
+        ngx_queue_remove(&f->queue);
+
+        switch (f->type) {
+        case NGX_QUIC_FT_ACK:
+        case NGX_QUIC_FT_ACK_ECN:
+            if (ctx->level == ssl_encryption_application) {
+                /* force generation of most recent acknowledgment */
+                ctx->send_ack = NGX_QUIC_MAX_ACK_GAP;
+            }
+
+            ngx_quic_free_frame(c, f);
+            break;
+
+        case NGX_QUIC_FT_PING:
+        case NGX_QUIC_FT_PATH_CHALLENGE:
+        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:
+            qs = ngx_quic_find_stream(&qc->streams.tree,
+                                      f->u.max_stream_data.id);
+            if (qs == NULL) {
+                ngx_quic_free_frame(c, f);
+                break;
+            }
+
+            f->u.max_stream_data.limit = qs->recv_max_data;
+            ngx_quic_queue_frame(qc, f);
+            break;
+
+        case NGX_QUIC_FT_STREAM:
+            qs = ngx_quic_find_stream(&qc->streams.tree, f->u.stream.stream_id);
+
+            if (qs) {
+                if (qs->send_state == NGX_QUIC_STREAM_SEND_RESET_SENT
+                    || qs->send_state == NGX_QUIC_STREAM_SEND_RESET_RECVD)
+                {
+                    ngx_quic_free_frame(c, f);
+                    break;
+                }
+            }
+
+            /* fall through */
+
+        default:
+            ngx_queue_insert_tail(&ctx->frames, &f->queue);
+        }
+
+    } while (q != ngx_queue_sentinel(&ctx->sent));
+
+    if (qc->closing) {
+        return;
+    }
+
+    ngx_post_event(&qc->push, &ngx_posted_events);
+}
+
+
+static void
+ngx_quic_congestion_lost(ngx_connection_t *c, ngx_quic_frame_t *f)
+{
+    ngx_uint_t              blocked;
+    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;
+
+    if (f->pnum < qc->rst_pnum) {
+        return;
+    }
+
+    blocked = (cg->in_flight >= cg->window) ? 1 : 0;
+
+    cg->in_flight -= f->plen;
+    f->plen = 0;
+
+    timer = f->send_time - 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);
+
+        goto done;
+    }
+
+    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);
+
+done:
+
+    if (blocked && cg->in_flight < cg->window) {
+        ngx_post_event(&qc->push, &ngx_posted_events);
+    }
+}
+
+
+void
+ngx_quic_set_lost_timer(ngx_connection_t *c)
+{
+    ngx_uint_t              i;
+    ngx_msec_t              now;
+    ngx_queue_t            *q;
+    ngx_msec_int_t          lost, pto, w;
+    ngx_quic_frame_t       *f;
+    ngx_quic_send_ctx_t    *ctx;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+    now = ngx_current_msec;
+
+    lost = -1;
+    pto = -1;
+
+    for (i = 0; i < NGX_QUIC_SEND_CTX_LAST; i++) {
+        ctx = &qc->send_ctx[i];
+
+        if (ngx_queue_empty(&ctx->sent)) {
+            continue;
+        }
+
+        if (ctx->largest_ack != NGX_QUIC_UNSET_PN) {
+            q = ngx_queue_head(&ctx->sent);
+            f = ngx_queue_data(q, ngx_quic_frame_t, queue);
+            w = (ngx_msec_int_t)
+                            (f->send_time + ngx_quic_lost_threshold(qc) - now);
+
+            if (f->pnum <= ctx->largest_ack) {
+                if (w < 0 || ctx->largest_ack - f->pnum >= NGX_QUIC_PKT_THR) {
+                    w = 0;
+                }
+
+                if (lost == -1 || w < lost) {
+                    lost = w;
+                }
+            }
+        }
+
+        q = ngx_queue_last(&ctx->sent);
+        f = ngx_queue_data(q, ngx_quic_frame_t, queue);
+        w = (ngx_msec_int_t)
+                (f->send_time + (ngx_quic_pto(c, ctx) << qc->pto_count) - now);
+
+        if (w < 0) {
+            w = 0;
+        }
+
+        if (pto == -1 || w < pto) {
+            pto = w;
+        }
+    }
+
+    if (qc->pto.timer_set) {
+        ngx_del_timer(&qc->pto);
+    }
+
+    if (lost != -1) {
+        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic lost timer lost:%M", lost);
+
+        qc->pto.handler = ngx_quic_lost_handler;
+        ngx_add_timer(&qc->pto, lost);
+        return;
+    }
+
+    if (pto != -1) {
+        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic lost timer pto:%M", pto);
+
+        qc->pto.handler = ngx_quic_pto_handler;
+        ngx_add_timer(&qc->pto, pto);
+        return;
+    }
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, "quic lost timer unset");
+}
+
+
+ngx_msec_t
+ngx_quic_pto(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx)
+{
+    ngx_msec_t              duration;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    /* RFC 9002, Appendix A.8.  Setting the Loss Detection Timer */
+
+    duration = qc->avg_rtt;
+    duration += ngx_max(4 * qc->rttvar, NGX_QUIC_TIME_GRANULARITY);
+
+    if (ctx->level == ssl_encryption_application && c->ssl->handshaked) {
+        duration += qc->ctp.max_ack_delay;
+    }
+
+    return duration;
+}
+
+
+static
+void ngx_quic_lost_handler(ngx_event_t *ev)
+{
+    ngx_connection_t  *c;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, ev->log, 0, "quic lost timer");
+
+    c = ev->data;
+
+    if (ngx_quic_detect_lost(c, NULL) != NGX_OK) {
+        ngx_quic_close_connection(c, NGX_ERROR);
+        return;
+    }
+
+    ngx_quic_connstate_dbg(c);
+}
+
+
+void
+ngx_quic_pto_handler(ngx_event_t *ev)
+{
+    ngx_uint_t              i, n;
+    ngx_msec_t              now;
+    ngx_queue_t            *q;
+    ngx_msec_int_t          w;
+    ngx_connection_t       *c;
+    ngx_quic_frame_t       *f;
+    ngx_quic_send_ctx_t    *ctx;
+    ngx_quic_connection_t  *qc;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, ev->log, 0, "quic pto timer");
+
+    c = ev->data;
+    qc = ngx_quic_get_connection(c);
+    now = ngx_current_msec;
+
+    for (i = 0; i < NGX_QUIC_SEND_CTX_LAST; i++) {
+
+        ctx = &qc->send_ctx[i];
+
+        if (ngx_queue_empty(&ctx->sent)) {
+            continue;
+        }
+
+        q = ngx_queue_last(&ctx->sent);
+        f = ngx_queue_data(q, ngx_quic_frame_t, queue);
+        w = (ngx_msec_int_t)
+                (f->send_time + (ngx_quic_pto(c, ctx) << qc->pto_count) - now);
+
+        if (f->pnum <= ctx->largest_ack
+            && ctx->largest_ack != NGX_QUIC_UNSET_PN)
+        {
+            continue;
+        }
+
+        if (w > 0) {
+            continue;
+        }
+
+        ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic pto %s pto_count:%ui",
+                       ngx_quic_level_name(ctx->level), qc->pto_count);
+
+        for (n = 0; n < 2; n++) {
+
+            f = ngx_quic_alloc_frame(c);
+            if (f == NULL) {
+                goto failed;
+            }
+
+            f->level = ctx->level;
+            f->type = NGX_QUIC_FT_PING;
+            f->ignore_congestion = 1;
+
+            if (ngx_quic_frame_sendto(c, f, 0, qc->path) == NGX_ERROR) {
+                goto failed;
+            }
+        }
+    }
+
+    qc->pto_count++;
+
+    ngx_quic_set_lost_timer(c);
+
+    ngx_quic_connstate_dbg(c);
+
+    return;
+
+failed:
+
+    ngx_quic_close_connection(c, NGX_ERROR);
+    return;
+}
+
+
+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);
+
+    if (!ngx_quic_keys_available(qc->keys, ctx->level, 1)) {
+        return NGX_OK;
+    }
+
+    prev_pending = ctx->pending_ack;
+
+    if (pkt->need_ack) {
+
+        ngx_post_event(&qc->push, &ngx_posted_events);
+
+        if (ctx->send_ack == 0) {
+            ctx->ack_delay_start = ngx_current_msec;
+        }
+
+        ctx->send_ack++;
+
+        if (ctx->pending_ack == NGX_QUIC_UNSET_PN
+            || ctx->pending_ack < pkt->pn)
+        {
+            ctx->pending_ack = pkt->pn;
+        }
+    }
+
+    base = ctx->largest_range;
+    pn = pkt->pn;
+
+    if (base == NGX_QUIC_UNSET_PN) {
+        ctx->largest_range = pn;
+        ctx->largest_received = pkt->received;
+        return NGX_OK;
+    }
+
+    if (base == pn) {
+        return NGX_OK;
+    }
+
+    largest = base;
+    smallest = largest - ctx->first_range;
+
+    if (pn > base) {
+
+        if (pn - base == 1) {
+            ctx->first_range++;
+            ctx->largest_range = pn;
+            ctx->largest_received = pkt->received;
+
+            return NGX_OK;
+
+        } else {
+            /* new gap in front of current largest */
+
+            /* no place for new range, send current range as is */
+            if (ctx->nranges == NGX_QUIC_MAX_RANGES) {
+
+                if (prev_pending != NGX_QUIC_UNSET_PN) {
+                    if (ngx_quic_send_ack(c, ctx) != NGX_OK) {
+                        return NGX_ERROR;
+                    }
+                }
+
+                if (prev_pending == ctx->pending_ack || !pkt->need_ack) {
+                    ctx->pending_ack = NGX_QUIC_UNSET_PN;
+                }
+            }
+
+            gap = pn - base - 2;
+            range = ctx->first_range;
+
+            ctx->first_range = 0;
+            ctx->largest_range = pn;
+            ctx->largest_received = pkt->received;
+
+            /* packet is out of order, force send */
+            if (pkt->need_ack) {
+                ctx->send_ack = NGX_QUIC_MAX_ACK_GAP;
+            }
+
+            i = 0;
+
+            goto insert;
+        }
+    }
+
+    /*  pn < base, perform lookup in existing ranges */
+
+    /* packet is out of order */
+    if (pkt->need_ack) {
+        ctx->send_ack = NGX_QUIC_MAX_ACK_GAP;
+    }
+
+    if (pn >= smallest && pn <= largest) {
+        return NGX_OK;
+    }
+
+#if (NGX_SUPPRESS_WARN)
+    r = NULL;
+#endif
+
+    for (i = 0; i < ctx->nranges; i++) {
+        r = &ctx->ranges[i];
+
+        ge = smallest - 1;
+        gs = ge - r->gap;
+
+        if (pn >= gs && pn <= ge) {
+
+            if (gs == ge) {
+                /* gap size is exactly one packet, now filled */
+
+                /* data moves to previous range, current is removed */
+
+                if (i == 0) {
+                    ctx->first_range += r->range + 2;
+
+                } else {
+                    ctx->ranges[i - 1].range += r->range + 2;
+                }
+
+                nr = ctx->nranges - i - 1;
+                if (nr) {
+                    ngx_memmove(&ctx->ranges[i], &ctx->ranges[i + 1],
+                                sizeof(ngx_quic_ack_range_t) * nr);
+                }
+
+                ctx->nranges--;
+
+            } else if (pn == gs) {
+                /* current gap shrinks from tail (current range grows) */
+                r->gap--;
+                r->range++;
+
+            } else if (pn == ge) {
+                /* current gap shrinks from head (previous range grows) */
+                r->gap--;
+
+                if (i == 0) {
+                    ctx->first_range++;
+
+                } else {
+                    ctx->ranges[i - 1].range++;
+                }
+
+            } else {
+                /* current gap is split into two parts */
+
+                gap = ge - pn - 1;
+                range = 0;
+
+                if (ctx->nranges == NGX_QUIC_MAX_RANGES) {
+                    if (prev_pending != NGX_QUIC_UNSET_PN) {
+                        if (ngx_quic_send_ack(c, ctx) != NGX_OK) {
+                            return NGX_ERROR;
+                        }
+                    }
+
+                    if (prev_pending == ctx->pending_ack || !pkt->need_ack) {
+                        ctx->pending_ack = NGX_QUIC_UNSET_PN;
+                    }
+                }
+
+                r->gap = pn - gs - 1;
+                goto insert;
+            }
+
+            return NGX_OK;
+        }
+
+        largest = smallest - r->gap - 2;
+        smallest = largest - r->range;
+
+        if (pn >= smallest && pn <= largest) {
+            /* this packet number is already known */
+            return NGX_OK;
+        }
+
+    }
+
+    if (pn == smallest - 1) {
+        /* extend first or last range */
+
+        if (i == 0) {
+            ctx->first_range++;
+
+        } else {
+            r->range++;
+        }
+
+        return NGX_OK;
+    }
+
+    /* nothing found, add new range at the tail  */
+
+    if (ctx->nranges == NGX_QUIC_MAX_RANGES) {
+        /* packet is too old to keep it */
+
+        if (pkt->need_ack) {
+            return ngx_quic_send_ack_range(c, ctx, pn, pn);
+        }
+
+        return NGX_OK;
+    }
+
+    gap = smallest - 2 - pn;
+    range = 0;
+
+insert:
+
+    if (ctx->nranges < NGX_QUIC_MAX_RANGES) {
+        ctx->nranges++;
+    }
+
+    ngx_memmove(&ctx->ranges[i + 1], &ctx->ranges[i],
+                sizeof(ngx_quic_ack_range_t) * (ctx->nranges - i - 1));
+
+    ctx->ranges[i].gap = gap;
+    ctx->ranges[i].range = range;
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_quic_generate_ack(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx)
+{
+    ngx_msec_t              delay;
+    ngx_quic_connection_t  *qc;
+
+    if (!ctx->send_ack) {
+        return NGX_OK;
+    }
+
+    if (ctx->level == ssl_encryption_application) {
+
+        delay = ngx_current_msec - ctx->ack_delay_start;
+        qc = ngx_quic_get_connection(c);
+
+        if (ngx_queue_empty(&ctx->frames)
+            && ctx->send_ack < NGX_QUIC_MAX_ACK_GAP
+            && delay < qc->tp.max_ack_delay)
+        {
+            if (!qc->push.timer_set && !qc->closing) {
+                ngx_add_timer(&qc->push,
+                              qc->tp.max_ack_delay - delay);
+            }
+
+            return NGX_OK;
+        }
+    }
+
+    if (ngx_quic_send_ack(c, ctx) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    ctx->send_ack = 0;
+
+    return NGX_OK;
+}
diff --git a/src/event/quic/ngx_event_quic_ack.h b/src/event/quic/ngx_event_quic_ack.h
new file mode 100644
index 0000000..56920c2
--- /dev/null
+++ b/src/event/quic/ngx_event_quic_ack.h
@@ -0,0 +1,30 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_EVENT_QUIC_ACK_H_INCLUDED_
+#define _NGX_EVENT_QUIC_ACK_H_INCLUDED_
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+
+ngx_int_t ngx_quic_handle_ack_frame(ngx_connection_t *c,
+    ngx_quic_header_t *pkt, ngx_quic_frame_t *f);
+
+void ngx_quic_congestion_ack(ngx_connection_t *c,
+    ngx_quic_frame_t *frame);
+void ngx_quic_resend_frames(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx);
+void ngx_quic_set_lost_timer(ngx_connection_t *c);
+void ngx_quic_pto_handler(ngx_event_t *ev);
+ngx_msec_t ngx_quic_pto(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx);
+
+ngx_int_t ngx_quic_ack_packet(ngx_connection_t *c,
+    ngx_quic_header_t *pkt);
+ngx_int_t ngx_quic_generate_ack(ngx_connection_t *c,
+    ngx_quic_send_ctx_t *ctx);
+
+#endif /* _NGX_EVENT_QUIC_ACK_H_INCLUDED_ */
diff --git a/src/event/quic/ngx_event_quic_bpf.c b/src/event/quic/ngx_event_quic_bpf.c
new file mode 100644
index 0000000..ab024ad
--- /dev/null
+++ b/src/event/quic/ngx_event_quic_bpf.c
@@ -0,0 +1,657 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+
+#define NGX_QUIC_BPF_VARNAME  "NGINX_BPF_MAPS"
+#define NGX_QUIC_BPF_VARSEP    ';'
+#define NGX_QUIC_BPF_ADDRSEP   '#'
+
+
+#define ngx_quic_bpf_get_conf(cycle)                                          \
+    (ngx_quic_bpf_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_quic_bpf_module)
+
+#define ngx_quic_bpf_get_old_conf(cycle)                                      \
+    cycle->old_cycle->conf_ctx ? ngx_quic_bpf_get_conf(cycle->old_cycle)      \
+                               : NULL
+
+#define ngx_core_get_conf(cycle)                                              \
+    (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module)
+
+
+typedef struct {
+    ngx_queue_t           queue;
+    int                   map_fd;
+
+    struct sockaddr      *sockaddr;
+    socklen_t             socklen;
+    ngx_uint_t            unused;     /* unsigned  unused:1; */
+} ngx_quic_sock_group_t;
+
+
+typedef struct {
+    ngx_flag_t            enabled;
+    ngx_uint_t            map_size;
+    ngx_queue_t           groups;     /* of ngx_quic_sock_group_t */
+} ngx_quic_bpf_conf_t;
+
+
+static void *ngx_quic_bpf_create_conf(ngx_cycle_t *cycle);
+static ngx_int_t ngx_quic_bpf_module_init(ngx_cycle_t *cycle);
+
+static void ngx_quic_bpf_cleanup(void *data);
+static ngx_inline void ngx_quic_bpf_close(ngx_log_t *log, int fd,
+    const char *name);
+
+static ngx_quic_sock_group_t *ngx_quic_bpf_find_group(ngx_quic_bpf_conf_t *bcf,
+    ngx_listening_t *ls);
+static ngx_quic_sock_group_t *ngx_quic_bpf_alloc_group(ngx_cycle_t *cycle,
+    struct sockaddr *sa, socklen_t socklen);
+static ngx_quic_sock_group_t *ngx_quic_bpf_create_group(ngx_cycle_t *cycle,
+    ngx_listening_t *ls);
+static ngx_quic_sock_group_t *ngx_quic_bpf_get_group(ngx_cycle_t *cycle,
+    ngx_listening_t *ls);
+static ngx_int_t ngx_quic_bpf_group_add_socket(ngx_cycle_t *cycle,
+    ngx_listening_t *ls);
+static uint64_t ngx_quic_bpf_socket_key(ngx_fd_t fd, ngx_log_t *log);
+
+static ngx_int_t ngx_quic_bpf_export_maps(ngx_cycle_t *cycle);
+static ngx_int_t ngx_quic_bpf_import_maps(ngx_cycle_t *cycle);
+
+extern ngx_bpf_program_t  ngx_quic_reuseport_helper;
+
+
+static ngx_command_t  ngx_quic_bpf_commands[] = {
+
+    { ngx_string("quic_bpf"),
+      NGX_MAIN_CONF|NGX_DIRECT_CONF|NGX_CONF_FLAG,
+      ngx_conf_set_flag_slot,
+      0,
+      offsetof(ngx_quic_bpf_conf_t, enabled),
+      NULL },
+
+      ngx_null_command
+};
+
+
+static ngx_core_module_t  ngx_quic_bpf_module_ctx = {
+    ngx_string("quic_bpf"),
+    ngx_quic_bpf_create_conf,
+    NULL
+};
+
+
+ngx_module_t  ngx_quic_bpf_module = {
+    NGX_MODULE_V1,
+    &ngx_quic_bpf_module_ctx,              /* module context */
+    ngx_quic_bpf_commands,                 /* module directives */
+    NGX_CORE_MODULE,                       /* module type */
+    NULL,                                  /* init master */
+    ngx_quic_bpf_module_init,              /* init module */
+    NULL,                                  /* init process */
+    NULL,                                  /* init thread */
+    NULL,                                  /* exit thread */
+    NULL,                                  /* exit process */
+    NULL,                                  /* exit master */
+    NGX_MODULE_V1_PADDING
+};
+
+
+static void *
+ngx_quic_bpf_create_conf(ngx_cycle_t *cycle)
+{
+    ngx_quic_bpf_conf_t  *bcf;
+
+    bcf = ngx_pcalloc(cycle->pool, sizeof(ngx_quic_bpf_conf_t));
+    if (bcf == NULL) {
+        return NULL;
+    }
+
+    bcf->enabled = NGX_CONF_UNSET;
+    bcf->map_size = NGX_CONF_UNSET_UINT;
+
+    ngx_queue_init(&bcf->groups);
+
+    return bcf;
+}
+
+
+static ngx_int_t
+ngx_quic_bpf_module_init(ngx_cycle_t *cycle)
+{
+    ngx_uint_t            i;
+    ngx_listening_t      *ls;
+    ngx_core_conf_t      *ccf;
+    ngx_pool_cleanup_t   *cln;
+    ngx_quic_bpf_conf_t  *bcf;
+
+    if (ngx_test_config) {
+        /*
+         * during config test, SO_REUSEPORT socket option is
+         * not set, thus making further processing meaningless
+         */
+        return NGX_OK;
+    }
+
+    ccf = ngx_core_get_conf(cycle);
+    bcf = ngx_quic_bpf_get_conf(cycle);
+
+    ngx_conf_init_value(bcf->enabled, 0);
+
+    bcf->map_size = ccf->worker_processes * 4;
+
+    cln = ngx_pool_cleanup_add(cycle->pool, 0);
+    if (cln == NULL) {
+        goto failed;
+    }
+
+    cln->data = bcf;
+    cln->handler = ngx_quic_bpf_cleanup;
+
+    if (ngx_inherited && ngx_is_init_cycle(cycle->old_cycle)) {
+        if (ngx_quic_bpf_import_maps(cycle) != NGX_OK) {
+            goto failed;
+        }
+    }
+
+    ls = cycle->listening.elts;
+
+    for (i = 0; i < cycle->listening.nelts; i++) {
+        if (ls[i].quic && ls[i].reuseport) {
+            if (ngx_quic_bpf_group_add_socket(cycle, &ls[i]) != NGX_OK) {
+                goto failed;
+            }
+        }
+    }
+
+    if (ngx_quic_bpf_export_maps(cycle) != NGX_OK) {
+        goto failed;
+    }
+
+    return NGX_OK;
+
+failed:
+
+    if (ngx_is_init_cycle(cycle->old_cycle)) {
+        ngx_log_error(NGX_LOG_EMERG, cycle->log, 0,
+                      "ngx_quic_bpf_module failed to initialize, check limits");
+
+        /* refuse to start */
+        return NGX_ERROR;
+    }
+
+    /*
+     * returning error now will lead to master process exiting immediately
+     * leaving worker processes orphaned, what is really unexpected.
+     * Instead, just issue a not about failed initialization and try
+     * to cleanup a bit. Still program can be already loaded to kernel
+     * for some reuseport groups, and there is no way to revert, so
+     * behaviour may be inconsistent.
+     */
+
+    ngx_log_error(NGX_LOG_EMERG, cycle->log, 0,
+                  "ngx_quic_bpf_module failed to initialize properly, ignored."
+                  "please check limits and note that nginx state now "
+                  "can be inconsistent and restart may be required");
+
+    return NGX_OK;
+}
+
+
+static void
+ngx_quic_bpf_cleanup(void *data)
+{
+    ngx_quic_bpf_conf_t  *bcf = (ngx_quic_bpf_conf_t *) data;
+
+    ngx_queue_t            *q;
+    ngx_quic_sock_group_t  *grp;
+
+    for (q = ngx_queue_head(&bcf->groups);
+         q != ngx_queue_sentinel(&bcf->groups);
+         q = ngx_queue_next(q))
+    {
+        grp = ngx_queue_data(q, ngx_quic_sock_group_t, queue);
+
+        ngx_quic_bpf_close(ngx_cycle->log, grp->map_fd, "map");
+    }
+}
+
+
+static ngx_inline void
+ngx_quic_bpf_close(ngx_log_t *log, int fd, const char *name)
+{
+    if (close(fd) != -1) {
+        return;
+    }
+
+    ngx_log_error(NGX_LOG_EMERG, log, ngx_errno,
+                  "quic bpf close %s fd:%d failed", name, fd);
+}
+
+
+static ngx_quic_sock_group_t *
+ngx_quic_bpf_find_group(ngx_quic_bpf_conf_t *bcf, ngx_listening_t *ls)
+{
+    ngx_queue_t            *q;
+    ngx_quic_sock_group_t  *grp;
+
+    for (q = ngx_queue_head(&bcf->groups);
+         q != ngx_queue_sentinel(&bcf->groups);
+         q = ngx_queue_next(q))
+    {
+        grp = ngx_queue_data(q, ngx_quic_sock_group_t, queue);
+
+        if (ngx_cmp_sockaddr(ls->sockaddr, ls->socklen,
+                             grp->sockaddr, grp->socklen, 1)
+            == NGX_OK)
+        {
+            return grp;
+        }
+    }
+
+    return NULL;
+}
+
+
+static ngx_quic_sock_group_t *
+ngx_quic_bpf_alloc_group(ngx_cycle_t *cycle, struct sockaddr *sa,
+    socklen_t socklen)
+{
+    ngx_quic_bpf_conf_t    *bcf;
+    ngx_quic_sock_group_t  *grp;
+
+    bcf = ngx_quic_bpf_get_conf(cycle);
+
+    grp = ngx_pcalloc(cycle->pool, sizeof(ngx_quic_sock_group_t));
+    if (grp == NULL) {
+        return NULL;
+    }
+
+    grp->socklen = socklen;
+    grp->sockaddr = ngx_palloc(cycle->pool, socklen);
+    if (grp->sockaddr == NULL) {
+        return NULL;
+    }
+    ngx_memcpy(grp->sockaddr, sa, socklen);
+
+    ngx_queue_insert_tail(&bcf->groups, &grp->queue);
+
+    return grp;
+}
+
+
+static ngx_quic_sock_group_t *
+ngx_quic_bpf_create_group(ngx_cycle_t *cycle, ngx_listening_t *ls)
+{
+    int                     progfd, failed, flags, rc;
+    ngx_quic_bpf_conf_t    *bcf;
+    ngx_quic_sock_group_t  *grp;
+
+    bcf = ngx_quic_bpf_get_conf(cycle);
+
+    if (!bcf->enabled) {
+        return NULL;
+    }
+
+    grp = ngx_quic_bpf_alloc_group(cycle, ls->sockaddr, ls->socklen);
+    if (grp == NULL) {
+        return NULL;
+    }
+
+    grp->map_fd = ngx_bpf_map_create(cycle->log, BPF_MAP_TYPE_SOCKHASH,
+                                     sizeof(uint64_t), sizeof(uint64_t),
+                                     bcf->map_size, 0);
+    if (grp->map_fd == -1) {
+        goto failed;
+    }
+
+    flags = fcntl(grp->map_fd, F_GETFD);
+    if (flags == -1) {
+        ngx_log_error(NGX_LOG_EMERG, cycle->log, errno,
+                      "quic bpf getfd failed");
+        goto failed;
+    }
+
+    /* need to inherit map during binary upgrade after exec */
+    flags &= ~FD_CLOEXEC;
+
+    rc = fcntl(grp->map_fd, F_SETFD, flags);
+    if (rc == -1) {
+        ngx_log_error(NGX_LOG_EMERG, cycle->log, errno,
+                      "quic bpf setfd failed");
+        goto failed;
+    }
+
+    ngx_bpf_program_link(&ngx_quic_reuseport_helper,
+                         "ngx_quic_sockmap", grp->map_fd);
+
+    progfd = ngx_bpf_load_program(cycle->log, &ngx_quic_reuseport_helper);
+    if (progfd < 0) {
+        goto failed;
+    }
+
+    failed = 0;
+
+    if (setsockopt(ls->fd, SOL_SOCKET, SO_ATTACH_REUSEPORT_EBPF,
+                   &progfd, sizeof(int))
+        == -1)
+    {
+        ngx_log_error(NGX_LOG_EMERG, cycle->log, ngx_socket_errno,
+                      "quic bpf setsockopt(SO_ATTACH_REUSEPORT_EBPF) failed");
+        failed = 1;
+    }
+
+    ngx_quic_bpf_close(cycle->log, progfd, "program");
+
+    if (failed) {
+        goto failed;
+    }
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
+                   "quic bpf sockmap created fd:%d", grp->map_fd);
+    return grp;
+
+failed:
+
+    if (grp->map_fd != -1) {
+        ngx_quic_bpf_close(cycle->log, grp->map_fd, "map");
+    }
+
+    ngx_queue_remove(&grp->queue);
+
+    return NULL;
+}
+
+
+static ngx_quic_sock_group_t *
+ngx_quic_bpf_get_group(ngx_cycle_t *cycle, ngx_listening_t *ls)
+{
+    ngx_quic_bpf_conf_t    *bcf, *old_bcf;
+    ngx_quic_sock_group_t  *grp, *ogrp;
+
+    bcf = ngx_quic_bpf_get_conf(cycle);
+
+    grp = ngx_quic_bpf_find_group(bcf, ls);
+    if (grp) {
+        return grp;
+    }
+
+    old_bcf = ngx_quic_bpf_get_old_conf(cycle);
+
+    if (old_bcf == NULL) {
+        return ngx_quic_bpf_create_group(cycle, ls);
+    }
+
+    ogrp = ngx_quic_bpf_find_group(old_bcf, ls);
+    if (ogrp == NULL) {
+        return ngx_quic_bpf_create_group(cycle, ls);
+    }
+
+    grp = ngx_quic_bpf_alloc_group(cycle, ls->sockaddr, ls->socklen);
+    if (grp == NULL) {
+        return NULL;
+    }
+
+    grp->map_fd = dup(ogrp->map_fd);
+    if (grp->map_fd == -1) {
+        ngx_log_error(NGX_LOG_EMERG, cycle->log, ngx_errno,
+                      "quic bpf failed to duplicate bpf map descriptor");
+
+        ngx_queue_remove(&grp->queue);
+
+        return NULL;
+    }
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
+                   "quic bpf sockmap fd duplicated old:%d new:%d",
+                   ogrp->map_fd, grp->map_fd);
+
+    return grp;
+}
+
+
+static ngx_int_t
+ngx_quic_bpf_group_add_socket(ngx_cycle_t *cycle,  ngx_listening_t *ls)
+{
+    uint64_t                cookie;
+    ngx_quic_bpf_conf_t    *bcf;
+    ngx_quic_sock_group_t  *grp;
+
+    bcf = ngx_quic_bpf_get_conf(cycle);
+
+    grp = ngx_quic_bpf_get_group(cycle, ls);
+
+    if (grp == NULL) {
+        if (!bcf->enabled) {
+            return NGX_OK;
+        }
+
+        return NGX_ERROR;
+    }
+
+    grp->unused = 0;
+
+    cookie = ngx_quic_bpf_socket_key(ls->fd, cycle->log);
+    if (cookie == (uint64_t) NGX_ERROR) {
+        return NGX_ERROR;
+    }
+
+    /* map[cookie] = socket; for use in kernel helper */
+    if (ngx_bpf_map_update(grp->map_fd, &cookie, &ls->fd, BPF_ANY) == -1) {
+        ngx_log_error(NGX_LOG_EMERG, cycle->log, ngx_errno,
+                      "quic bpf failed to update socket map key=%xL", cookie);
+        return NGX_ERROR;
+    }
+
+    ngx_log_debug4(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
+                 "quic bpf sockmap fd:%d add socket:%d cookie:0x%xL worker:%ui",
+                 grp->map_fd, ls->fd, cookie, ls->worker);
+
+    /* do not inherit this socket */
+    ls->ignore = 1;
+
+    return NGX_OK;
+}
+
+
+static uint64_t
+ngx_quic_bpf_socket_key(ngx_fd_t fd, ngx_log_t *log)
+{
+    uint64_t   cookie;
+    socklen_t  optlen;
+
+    optlen = sizeof(cookie);
+
+    if (getsockopt(fd, SOL_SOCKET, SO_COOKIE, &cookie, &optlen) == -1) {
+        ngx_log_error(NGX_LOG_EMERG, log, ngx_socket_errno,
+                      "quic bpf getsockopt(SO_COOKIE) failed");
+
+        return (ngx_uint_t) NGX_ERROR;
+    }
+
+    return cookie;
+}
+
+
+static ngx_int_t
+ngx_quic_bpf_export_maps(ngx_cycle_t *cycle)
+{
+    u_char                 *p, *buf;
+    size_t                  len;
+    ngx_str_t              *var;
+    ngx_queue_t            *q;
+    ngx_core_conf_t        *ccf;
+    ngx_quic_bpf_conf_t    *bcf;
+    ngx_quic_sock_group_t  *grp;
+
+    ccf = ngx_core_get_conf(cycle);
+    bcf = ngx_quic_bpf_get_conf(cycle);
+
+    len = sizeof(NGX_QUIC_BPF_VARNAME) + 1;
+
+    q = ngx_queue_head(&bcf->groups);
+
+    while (q != ngx_queue_sentinel(&bcf->groups)) {
+
+        grp = ngx_queue_data(q, ngx_quic_sock_group_t, queue);
+
+        q = ngx_queue_next(q);
+
+        if (grp->unused) {
+            /*
+             * map was inherited, but it is not used in this configuration;
+             * do not pass such map further and drop the group to prevent
+             * interference with changes during reload
+             */
+
+            ngx_quic_bpf_close(cycle->log, grp->map_fd, "map");
+            ngx_queue_remove(&grp->queue);
+
+            continue;
+        }
+
+        len += NGX_INT32_LEN + 1 + NGX_SOCKADDR_STRLEN + 1;
+    }
+
+    len++;
+
+    buf = ngx_palloc(cycle->pool, len);
+    if (buf == NULL) {
+        return NGX_ERROR;
+    }
+
+    p = ngx_cpymem(buf, NGX_QUIC_BPF_VARNAME "=",
+                   sizeof(NGX_QUIC_BPF_VARNAME));
+
+    for (q = ngx_queue_head(&bcf->groups);
+         q != ngx_queue_sentinel(&bcf->groups);
+         q = ngx_queue_next(q))
+    {
+        grp = ngx_queue_data(q, ngx_quic_sock_group_t, queue);
+
+        p = ngx_sprintf(p, "%ud", grp->map_fd);
+
+        *p++ = NGX_QUIC_BPF_ADDRSEP;
+
+        p += ngx_sock_ntop(grp->sockaddr, grp->socklen, p,
+                           NGX_SOCKADDR_STRLEN, 1);
+
+        *p++ = NGX_QUIC_BPF_VARSEP;
+    }
+
+    *p = '\0';
+
+    var = ngx_array_push(&ccf->env);
+    if (var == NULL) {
+        return NGX_ERROR;
+    }
+
+    var->data = buf;
+    var->len = sizeof(NGX_QUIC_BPF_VARNAME) - 1;
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_bpf_import_maps(ngx_cycle_t *cycle)
+{
+    int                     s;
+    u_char                 *inherited, *p, *v;
+    ngx_uint_t              in_fd;
+    ngx_addr_t              tmp;
+    ngx_quic_bpf_conf_t    *bcf;
+    ngx_quic_sock_group_t  *grp;
+
+    inherited = (u_char *) getenv(NGX_QUIC_BPF_VARNAME);
+
+    if (inherited == NULL) {
+        return NGX_OK;
+    }
+
+    bcf = ngx_quic_bpf_get_conf(cycle);
+
+#if (NGX_SUPPRESS_WARN)
+    s = -1;
+#endif
+
+    in_fd = 1;
+
+    for (p = inherited, v = p; *p; p++) {
+
+        switch (*p) {
+
+        case NGX_QUIC_BPF_ADDRSEP:
+
+            if (!in_fd) {
+                ngx_log_error(NGX_LOG_EMERG, cycle->log, 0,
+                              "quic bpf failed to parse inherited env");
+                return NGX_ERROR;
+            }
+            in_fd = 0;
+
+            s = ngx_atoi(v, p - v);
+            if (s == NGX_ERROR) {
+                ngx_log_error(NGX_LOG_EMERG, cycle->log, 0,
+                              "quic bpf failed to parse inherited map fd");
+                return NGX_ERROR;
+            }
+
+            v = p + 1;
+            break;
+
+        case NGX_QUIC_BPF_VARSEP:
+
+            if (in_fd) {
+                ngx_log_error(NGX_LOG_EMERG, cycle->log, 0,
+                              "quic bpf failed to parse inherited env");
+                return NGX_ERROR;
+            }
+            in_fd = 1;
+
+            grp = ngx_pcalloc(cycle->pool,
+                              sizeof(ngx_quic_sock_group_t));
+            if (grp == NULL) {
+                return NGX_ERROR;
+            }
+
+            grp->map_fd = s;
+
+            if (ngx_parse_addr_port(cycle->pool, &tmp, v, p - v)
+                != NGX_OK)
+            {
+                ngx_log_error(NGX_LOG_EMERG, cycle->log, 0,
+                              "quic bpf failed to parse inherited"
+                              " address '%*s'", p - v , v);
+
+                ngx_quic_bpf_close(cycle->log, s, "inherited map");
+
+                return NGX_ERROR;
+            }
+
+            grp->sockaddr = tmp.sockaddr;
+            grp->socklen = tmp.socklen;
+
+            grp->unused = 1;
+
+            ngx_queue_insert_tail(&bcf->groups, &grp->queue);
+
+            ngx_log_debug3(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
+                           "quic bpf sockmap inherited with "
+                           "fd:%d address:%*s",
+                           grp->map_fd, p - v, v);
+            v = p + 1;
+            break;
+
+        default:
+            break;
+        }
+    }
+
+    return NGX_OK;
+}
diff --git a/src/event/quic/ngx_event_quic_bpf_code.c b/src/event/quic/ngx_event_quic_bpf_code.c
new file mode 100644
index 0000000..5c9dea1
--- /dev/null
+++ b/src/event/quic/ngx_event_quic_bpf_code.c
@@ -0,0 +1,88 @@
+/* AUTO-GENERATED, DO NOT EDIT. */
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include "ngx_bpf.h"
+
+
+static ngx_bpf_reloc_t bpf_reloc_prog_ngx_quic_reuseport_helper[] = {
+    { "ngx_quic_sockmap", 55 },
+};
+
+static struct bpf_insn bpf_insn_prog_ngx_quic_reuseport_helper[] = {
+    /* opcode dst          src         offset imm */
+    { 0x79,   BPF_REG_4,   BPF_REG_1, (int16_t)      0,        0x0 },
+    { 0x79,   BPF_REG_3,   BPF_REG_1, (int16_t)      8,        0x0 },
+    { 0xbf,   BPF_REG_2,   BPF_REG_4, (int16_t)      0,        0x0 },
+    {  0x7,   BPF_REG_2,   BPF_REG_0, (int16_t)      0,        0x8 },
+    { 0x2d,   BPF_REG_2,   BPF_REG_3, (int16_t)     54,        0x0 },
+    { 0xbf,   BPF_REG_5,   BPF_REG_4, (int16_t)      0,        0x0 },
+    {  0x7,   BPF_REG_5,   BPF_REG_0, (int16_t)      0,        0x9 },
+    { 0x2d,   BPF_REG_5,   BPF_REG_3, (int16_t)     51,        0x0 },
+    { 0xb7,   BPF_REG_5,   BPF_REG_0, (int16_t)      0,       0x14 },
+    { 0xb7,   BPF_REG_0,   BPF_REG_0, (int16_t)      0,        0x9 },
+    { 0x71,   BPF_REG_6,   BPF_REG_2, (int16_t)      0,        0x0 },
+    { 0x67,   BPF_REG_6,   BPF_REG_0, (int16_t)      0,       0x38 },
+    { 0xc7,   BPF_REG_6,   BPF_REG_0, (int16_t)      0,       0x38 },
+    { 0x65,   BPF_REG_6,   BPF_REG_0, (int16_t)     10, 0xffffffff },
+    { 0xbf,   BPF_REG_2,   BPF_REG_4, (int16_t)      0,        0x0 },
+    {  0x7,   BPF_REG_2,   BPF_REG_0, (int16_t)      0,        0xd },
+    { 0x2d,   BPF_REG_2,   BPF_REG_3, (int16_t)     42,        0x0 },
+    { 0xbf,   BPF_REG_5,   BPF_REG_4, (int16_t)      0,        0x0 },
+    {  0x7,   BPF_REG_5,   BPF_REG_0, (int16_t)      0,        0xe },
+    { 0x2d,   BPF_REG_5,   BPF_REG_3, (int16_t)     39,        0x0 },
+    { 0xb7,   BPF_REG_0,   BPF_REG_0, (int16_t)      0,        0xe },
+    { 0x71,   BPF_REG_5,   BPF_REG_2, (int16_t)      0,        0x0 },
+    { 0xb7,   BPF_REG_6,   BPF_REG_0, (int16_t)      0,        0x8 },
+    { 0x2d,   BPF_REG_6,   BPF_REG_5, (int16_t)     35,        0x0 },
+    {  0xf,   BPF_REG_5,   BPF_REG_0, (int16_t)      0,        0x0 },
+    {  0xf,   BPF_REG_4,   BPF_REG_5, (int16_t)      0,        0x0 },
+    { 0x2d,   BPF_REG_4,   BPF_REG_3, (int16_t)     32,        0x0 },
+    { 0xbf,   BPF_REG_4,   BPF_REG_2, (int16_t)      0,        0x0 },
+    {  0x7,   BPF_REG_4,   BPF_REG_0, (int16_t)      0,        0x9 },
+    { 0x2d,   BPF_REG_4,   BPF_REG_3, (int16_t)     29,        0x0 },
+    { 0x71,   BPF_REG_4,   BPF_REG_2, (int16_t)      1,        0x0 },
+    { 0x67,   BPF_REG_4,   BPF_REG_0, (int16_t)      0,       0x38 },
+    { 0x71,   BPF_REG_3,   BPF_REG_2, (int16_t)      2,        0x0 },
+    { 0x67,   BPF_REG_3,   BPF_REG_0, (int16_t)      0,       0x30 },
+    { 0x4f,   BPF_REG_3,   BPF_REG_4, (int16_t)      0,        0x0 },
+    { 0x71,   BPF_REG_4,   BPF_REG_2, (int16_t)      3,        0x0 },
+    { 0x67,   BPF_REG_4,   BPF_REG_0, (int16_t)      0,       0x28 },
+    { 0x4f,   BPF_REG_3,   BPF_REG_4, (int16_t)      0,        0x0 },
+    { 0x71,   BPF_REG_4,   BPF_REG_2, (int16_t)      4,        0x0 },
+    { 0x67,   BPF_REG_4,   BPF_REG_0, (int16_t)      0,       0x20 },
+    { 0x4f,   BPF_REG_3,   BPF_REG_4, (int16_t)      0,        0x0 },
+    { 0x71,   BPF_REG_4,   BPF_REG_2, (int16_t)      5,        0x0 },
+    { 0x67,   BPF_REG_4,   BPF_REG_0, (int16_t)      0,       0x18 },
+    { 0x4f,   BPF_REG_3,   BPF_REG_4, (int16_t)      0,        0x0 },
+    { 0x71,   BPF_REG_4,   BPF_REG_2, (int16_t)      6,        0x0 },
+    { 0x67,   BPF_REG_4,   BPF_REG_0, (int16_t)      0,       0x10 },
+    { 0x4f,   BPF_REG_3,   BPF_REG_4, (int16_t)      0,        0x0 },
+    { 0x71,   BPF_REG_4,   BPF_REG_2, (int16_t)      7,        0x0 },
+    { 0x67,   BPF_REG_4,   BPF_REG_0, (int16_t)      0,        0x8 },
+    { 0x4f,   BPF_REG_3,   BPF_REG_4, (int16_t)      0,        0x0 },
+    { 0x71,   BPF_REG_2,   BPF_REG_2, (int16_t)      8,        0x0 },
+    { 0x4f,   BPF_REG_3,   BPF_REG_2, (int16_t)      0,        0x0 },
+    { 0x7b,  BPF_REG_10,   BPF_REG_3, (int16_t)  65528,        0x0 },
+    { 0xbf,   BPF_REG_3,  BPF_REG_10, (int16_t)      0,        0x0 },
+    {  0x7,   BPF_REG_3,   BPF_REG_0, (int16_t)      0, 0xfffffff8 },
+    { 0x18,   BPF_REG_2,   BPF_REG_0, (int16_t)      0,        0x0 },
+    {  0x0,   BPF_REG_0,   BPF_REG_0, (int16_t)      0,        0x0 },
+    { 0xb7,   BPF_REG_4,   BPF_REG_0, (int16_t)      0,        0x0 },
+    { 0x85,   BPF_REG_0,   BPF_REG_0, (int16_t)      0,       0x52 },
+    { 0xb7,   BPF_REG_0,   BPF_REG_0, (int16_t)      0,        0x1 },
+    { 0x95,   BPF_REG_0,   BPF_REG_0, (int16_t)      0,        0x0 },
+};
+
+
+ngx_bpf_program_t ngx_quic_reuseport_helper = {
+    .relocs = bpf_reloc_prog_ngx_quic_reuseport_helper,
+    .nrelocs = sizeof(bpf_reloc_prog_ngx_quic_reuseport_helper)
+               / sizeof(bpf_reloc_prog_ngx_quic_reuseport_helper[0]),
+    .ins = bpf_insn_prog_ngx_quic_reuseport_helper,
+    .nins = sizeof(bpf_insn_prog_ngx_quic_reuseport_helper)
+            / sizeof(bpf_insn_prog_ngx_quic_reuseport_helper[0]),
+    .license = "BSD",
+    .type = BPF_PROG_TYPE_SK_REUSEPORT,
+};
diff --git a/src/event/quic/ngx_event_quic_connection.h b/src/event/quic/ngx_event_quic_connection.h
new file mode 100644
index 0000000..824c92b
--- /dev/null
+++ b/src/event/quic/ngx_event_quic_connection.h
@@ -0,0 +1,305 @@
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_EVENT_QUIC_CONNECTION_H_INCLUDED_
+#define _NGX_EVENT_QUIC_CONNECTION_H_INCLUDED_
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_event.h>
+
+
+/* #define NGX_QUIC_DEBUG_PACKETS */  /* dump packet contents */
+/* #define NGX_QUIC_DEBUG_FRAMES */   /* dump frames contents */
+/* #define NGX_QUIC_DEBUG_ALLOC */    /* log frames and bufs alloc */
+/* #define NGX_QUIC_DEBUG_CRYPTO */
+
+typedef struct ngx_quic_connection_s  ngx_quic_connection_t;
+typedef struct ngx_quic_server_id_s   ngx_quic_server_id_t;
+typedef struct ngx_quic_client_id_s   ngx_quic_client_id_t;
+typedef struct ngx_quic_send_ctx_s    ngx_quic_send_ctx_t;
+typedef struct ngx_quic_socket_s      ngx_quic_socket_t;
+typedef struct ngx_quic_path_s        ngx_quic_path_t;
+typedef struct ngx_quic_keys_s        ngx_quic_keys_t;
+
+#if (NGX_QUIC_OPENSSL_COMPAT)
+#include <ngx_event_quic_openssl_compat.h>
+#endif
+#include <ngx_event_quic_transport.h>
+#include <ngx_event_quic_protection.h>
+#include <ngx_event_quic_frames.h>
+#include <ngx_event_quic_migration.h>
+#include <ngx_event_quic_connid.h>
+#include <ngx_event_quic_streams.h>
+#include <ngx_event_quic_ssl.h>
+#include <ngx_event_quic_tokens.h>
+#include <ngx_event_quic_ack.h>
+#include <ngx_event_quic_output.h>
+#include <ngx_event_quic_socket.h>
+
+
+/* RFC 9002, 6.2.2.  Handshakes and New Paths: kInitialRtt */
+#define NGX_QUIC_INITIAL_RTT                 333 /* ms */
+
+#define NGX_QUIC_UNSET_PN                    (uint64_t) -1
+
+#define NGX_QUIC_SEND_CTX_LAST               (NGX_QUIC_ENCRYPTION_LAST - 1)
+
+/*  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_get_connection(c)                                            \
+    (((c)->udp) ? (((ngx_quic_socket_t *)((c)->udp))->quic) : NULL)
+
+#define ngx_quic_get_socket(c)               ((ngx_quic_socket_t *)((c)->udp))
+
+#define ngx_quic_init_rtt(qc)                                                 \
+    (qc)->avg_rtt = NGX_QUIC_INITIAL_RTT;                                     \
+    (qc)->rttvar = NGX_QUIC_INITIAL_RTT / 2;                                  \
+    (qc)->min_rtt = NGX_TIMER_INFINITE;                                       \
+    (qc)->first_rtt = NGX_TIMER_INFINITE;                                     \
+    (qc)->latest_rtt = 0;
+
+
+typedef enum {
+    NGX_QUIC_PATH_IDLE = 0,
+    NGX_QUIC_PATH_VALIDATING,
+    NGX_QUIC_PATH_WAITING,
+    NGX_QUIC_PATH_MTUD
+} ngx_quic_path_state_e;
+
+
+struct ngx_quic_client_id_s {
+    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_uint_t                        used;  /* unsigned  used:1; */
+};
+
+
+struct ngx_quic_server_id_s {
+    uint64_t                          seqnum;
+    size_t                            len;
+    u_char                            id[NGX_QUIC_CID_LEN_MAX];
+};
+
+
+struct ngx_quic_path_s {
+    ngx_queue_t                       queue;
+    struct sockaddr                  *sockaddr;
+    ngx_sockaddr_t                    sa;
+    socklen_t                         socklen;
+    ngx_quic_client_id_t             *cid;
+    ngx_quic_path_state_e             state;
+    ngx_msec_t                        expires;
+    ngx_uint_t                        tries;
+    ngx_uint_t                        tag;
+    size_t                            mtu;
+    size_t                            mtud;
+    size_t                            max_mtu;
+    off_t                             sent;
+    off_t                             received;
+    u_char                            challenge[2][8];
+    uint64_t                          seqnum;
+    uint64_t                          mtu_pnum[NGX_QUIC_PATH_RETRIES];
+    ngx_str_t                         addr_text;
+    u_char                            text[NGX_SOCKADDR_STRLEN];
+    unsigned                          validated:1;
+    unsigned                          mtu_unvalidated:1;
+};
+
+
+struct ngx_quic_socket_s {
+    ngx_udp_connection_t              udp;
+    ngx_quic_connection_t            *quic;
+    ngx_queue_t                       queue;
+    ngx_quic_server_id_t              sid;
+    ngx_sockaddr_t                    sockaddr;
+    socklen_t                         socklen;
+    ngx_uint_t                        used; /* unsigned  used:1; */
+};
+
+
+typedef struct {
+    ngx_rbtree_t                      tree;
+    ngx_rbtree_node_t                 sentinel;
+
+    ngx_queue_t                       uninitialized;
+    ngx_queue_t                       free;
+
+    uint64_t                          sent;
+    uint64_t                          recv_offset;
+    uint64_t                          recv_window;
+    uint64_t                          recv_last;
+    uint64_t                          recv_max_data;
+    uint64_t                          send_offset;
+    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_uint_t                        initialized;
+                                                 /* unsigned  initialized:1; */
+} 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;
+
+
+/*
+ * RFC 9000, 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 that
+ *  are also Initial packets.
+ */
+struct ngx_quic_send_ctx_s {
+    enum ssl_encryption_level_t       level;
+
+    ngx_quic_buffer_t                 crypto;
+    uint64_t                          crypto_sent;
+
+    uint64_t                          pnum;        /* to be sent */
+    uint64_t                          largest_ack; /* received from peer */
+    uint64_t                          largest_pn;  /* received from peer */
+
+    ngx_queue_t                       frames;      /* generated frames */
+    ngx_queue_t                       sending;     /* frames assigned to pkt */
+    ngx_queue_t                       sent;        /* frames waiting ACK */
+
+    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;
+};
+
+
+struct ngx_quic_connection_s {
+    uint32_t                          version;
+
+    ngx_quic_path_t                  *path;
+
+    ngx_queue_t                       sockets;
+    ngx_queue_t                       paths;
+    ngx_queue_t                       client_ids;
+    ngx_queue_t                       free_sockets;
+    ngx_queue_t                       free_paths;
+    ngx_queue_t                       free_client_ids;
+
+    ngx_uint_t                        nsockets;
+    ngx_uint_t                        nclient_ids;
+    uint64_t                          max_retired_seqnum;
+    uint64_t                          client_seqnum;
+    uint64_t                          server_seqnum;
+    uint64_t                          path_seqnum;
+
+    ngx_quic_tp_t                     tp;
+    ngx_quic_tp_t                     ctp;
+
+    ngx_quic_send_ctx_t               send_ctx[NGX_QUIC_SEND_CTX_LAST];
+
+    ngx_quic_keys_t                  *keys;
+
+    ngx_quic_conf_t                  *conf;
+
+    ngx_event_t                       push;
+    ngx_event_t                       pto;
+    ngx_event_t                       close;
+    ngx_event_t                       path_validation;
+    ngx_event_t                       key_update;
+
+    ngx_msec_t                        last_cc;
+
+    ngx_msec_t                        first_rtt;
+    ngx_msec_t                        latest_rtt;
+    ngx_msec_t                        avg_rtt;
+    ngx_msec_t                        min_rtt;
+    ngx_msec_t                        rttvar;
+
+    ngx_uint_t                        pto_count;
+
+    ngx_queue_t                       free_frames;
+    ngx_buf_t                        *free_bufs;
+    ngx_buf_t                        *free_shadow_bufs;
+
+    ngx_uint_t                        nframes;
+#ifdef NGX_QUIC_DEBUG_ALLOC
+    ngx_uint_t                        nbufs;
+    ngx_uint_t                        nshadowbufs;
+#endif
+
+#if (NGX_QUIC_OPENSSL_COMPAT)
+    ngx_quic_compat_t                *compat;
+#endif
+
+    ngx_quic_streams_t                streams;
+    ngx_quic_congestion_t             congestion;
+
+    uint64_t                          rst_pnum;    /* first on validated path */
+
+    off_t                             received;
+
+    ngx_uint_t                        error;
+    enum ssl_encryption_level_t       error_level;
+    ngx_uint_t                        error_ftype;
+    const char                       *error_reason;
+
+    ngx_uint_t                        shutdown_code;
+    const char                       *shutdown_reason;
+
+    unsigned                          error_app:1;
+    unsigned                          send_timer_set:1;
+    unsigned                          closing:1;
+    unsigned                          shutdown:1;
+    unsigned                          draining:1;
+    unsigned                          key_phase:1;
+    unsigned                          validated:1;
+    unsigned                          client_tp_done:1;
+};
+
+
+ngx_int_t ngx_quic_apply_transport_params(ngx_connection_t *c,
+    ngx_quic_tp_t *ctp);
+void ngx_quic_discard_ctx(ngx_connection_t *c,
+    enum ssl_encryption_level_t level);
+void ngx_quic_close_connection(ngx_connection_t *c, ngx_int_t rc);
+void ngx_quic_shutdown_quic(ngx_connection_t *c);
+
+#if (NGX_DEBUG)
+void ngx_quic_connstate_dbg(ngx_connection_t *c);
+#else
+#define ngx_quic_connstate_dbg(c)
+#endif
+
+#endif /* _NGX_EVENT_QUIC_CONNECTION_H_INCLUDED_ */
diff --git a/src/event/quic/ngx_event_quic_connid.c b/src/event/quic/ngx_event_quic_connid.c
new file mode 100644
index 0000000..f508682
--- /dev/null
+++ b/src/event/quic/ngx_event_quic_connid.c
@@ -0,0 +1,502 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_event.h>
+#include <ngx_event_quic_connection.h>
+
+#define NGX_QUIC_MAX_SERVER_IDS   8
+
+
+#if (NGX_QUIC_BPF)
+static ngx_int_t ngx_quic_bpf_attach_id(ngx_connection_t *c, u_char *id);
+#endif
+static ngx_int_t ngx_quic_retire_client_id(ngx_connection_t *c,
+    ngx_quic_client_id_t *cid);
+static ngx_quic_client_id_t *ngx_quic_alloc_client_id(ngx_connection_t *c,
+    ngx_quic_connection_t *qc);
+static ngx_int_t ngx_quic_send_server_id(ngx_connection_t *c,
+    ngx_quic_server_id_t *sid);
+
+
+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;
+    }
+
+#if (NGX_QUIC_BPF)
+    if (ngx_quic_bpf_attach_id(c, id) != NGX_OK) {
+        ngx_log_error(NGX_LOG_ERR, c->log, 0,
+                      "quic bpf failed to generate socket key");
+        /* ignore error, things still may work */
+    }
+#endif
+
+    return NGX_OK;
+}
+
+
+#if (NGX_QUIC_BPF)
+
+static ngx_int_t
+ngx_quic_bpf_attach_id(ngx_connection_t *c, u_char *id)
+{
+    int        fd;
+    uint64_t   cookie;
+    socklen_t  optlen;
+
+    fd = c->listening->fd;
+
+    optlen = sizeof(cookie);
+
+    if (getsockopt(fd, SOL_SOCKET, SO_COOKIE, &cookie, &optlen) == -1) {
+        ngx_log_error(NGX_LOG_ERR, c->log, ngx_socket_errno,
+                      "quic getsockopt(SO_COOKIE) failed");
+
+        return NGX_ERROR;
+    }
+
+    ngx_quic_dcid_encode_key(id, cookie);
+
+    return NGX_OK;
+}
+
+#endif
+
+
+ngx_int_t
+ngx_quic_handle_new_connection_id_frame(ngx_connection_t *c,
+    ngx_quic_new_conn_id_frame_t *f)
+{
+    ngx_str_t               id;
+    ngx_queue_t            *q;
+    ngx_quic_frame_t       *frame;
+    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) {
+        /*
+         * RFC 9000, 19.15.  NEW_CONNECTION_ID Frame
+         *
+         *  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.
+         */
+
+        frame = ngx_quic_alloc_frame(c);
+        if (frame == NULL) {
+            return NGX_ERROR;
+        }
+
+        frame->level = ssl_encryption_application;
+        frame->type = NGX_QUIC_FT_RETIRE_CONNECTION_ID;
+        frame->u.retire_cid.sequence_number = f->seqnum;
+
+        ngx_quic_queue_frame(qc, frame);
+
+        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)
+        {
+            /*
+             * ..if 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 {
+
+        id.data = f->cid;
+        id.len = f->len;
+
+        if (ngx_quic_create_client_id(c, &id, f->seqnum, f->srt) == NULL) {
+            return NGX_ERROR;
+        }
+    }
+
+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;
+        }
+
+        if (ngx_quic_retire_client_id(c, cid) != NGX_OK) {
+            return NGX_ERROR;
+        }
+    }
+
+done:
+
+    if (qc->nclient_ids > qc->tp.active_connection_id_limit) {
+        /*
+         * RFC 9000, 5.1.1.  Issuing Connection IDs
+         *
+         * 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_client_id(ngx_connection_t *c, ngx_quic_client_id_t *cid)
+{
+    ngx_queue_t            *q;
+    ngx_quic_path_t        *path;
+    ngx_quic_client_id_t   *new_cid;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    if (!cid->used) {
+        return ngx_quic_free_client_id(c, cid);
+    }
+
+    /* we are going to retire client id which is in use */
+
+    q = ngx_queue_head(&qc->paths);
+
+    while (q != ngx_queue_sentinel(&qc->paths)) {
+
+        path = ngx_queue_data(q, ngx_quic_path_t, queue);
+        q = ngx_queue_next(q);
+
+        if (path->cid != cid) {
+            continue;
+        }
+
+        if (path == qc->path) {
+            /* this is the active path: update it with new CID */
+            new_cid = ngx_quic_next_client_id(c);
+            if (new_cid == NULL) {
+                return NGX_ERROR;
+            }
+
+            qc->path->cid = new_cid;
+            new_cid->used = 1;
+
+            return ngx_quic_free_client_id(c, cid);
+        }
+
+        return ngx_quic_free_path(c, path);
+    }
+
+    return NGX_OK;
+}
+
+
+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;
+}
+
+
+ngx_quic_client_id_t *
+ngx_quic_create_client_id(ngx_connection_t *c, ngx_str_t *id,
+    uint64_t seqnum, u_char *token)
+{
+    ngx_quic_client_id_t   *cid;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    cid = ngx_quic_alloc_client_id(c, qc);
+    if (cid == NULL) {
+        return NULL;
+    }
+
+    cid->seqnum = seqnum;
+
+    cid->len = id->len;
+    ngx_memcpy(cid->id, id->data, id->len);
+
+    if (token) {
+        ngx_memcpy(cid->sr_token, token, NGX_QUIC_SR_TOKEN_LEN);
+    }
+
+    ngx_queue_insert_tail(&qc->client_ids, &cid->queue);
+    qc->nclient_ids++;
+
+    if (seqnum > qc->client_seqnum) {
+        qc->client_seqnum = seqnum;
+    }
+
+    ngx_log_debug5(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic cid seq:%uL received id:%uz:%xV:%*xs",
+                    cid->seqnum, id->len, id,
+                    (size_t) NGX_QUIC_SR_TOKEN_LEN, cid->sr_token);
+
+    return cid;
+}
+
+
+ngx_quic_client_id_t *
+ngx_quic_next_client_id(ngx_connection_t *c)
+{
+    ngx_queue_t            *q;
+    ngx_quic_client_id_t   *cid;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    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->used) {
+            return cid;
+        }
+    }
+
+    return NULL;
+}
+
+
+ngx_int_t
+ngx_quic_handle_retire_connection_id_frame(ngx_connection_t *c,
+    ngx_quic_retire_cid_frame_t *f)
+{
+    ngx_quic_socket_t      *qsock;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    if (f->sequence_number >= qc->server_seqnum) {
+        /*
+         * RFC 9000, 19.16.
+         *
+         *  Receipt of a RETIRE_CONNECTION_ID frame containing a sequence
+         *  number greater than any previously sent to the peer MUST be
+         *  treated as a connection error of type PROTOCOL_VIOLATION.
+         */
+        qc->error = NGX_QUIC_ERR_PROTOCOL_VIOLATION;
+        qc->error_reason = "sequence number of id to retire was never issued";
+
+        return NGX_ERROR;
+    }
+
+    qsock = ngx_quic_get_socket(c);
+
+    if (qsock->sid.seqnum == f->sequence_number) {
+
+        /*
+         * RFC 9000, 19.16.
+         *
+         * The sequence number specified in a RETIRE_CONNECTION_ID frame MUST
+         * NOT refer to the Destination Connection ID field of the packet in
+         * which the frame is contained.  The peer MAY treat this as a
+         * connection error of type PROTOCOL_VIOLATION.
+         */
+
+        qc->error = NGX_QUIC_ERR_PROTOCOL_VIOLATION;
+        qc->error_reason = "sequence number of id to retire refers DCID";
+
+        return NGX_ERROR;
+    }
+
+    qsock = ngx_quic_find_socket(c, f->sequence_number);
+    if (qsock == NULL) {
+        return NGX_OK;
+    }
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic socket seq:%uL is retired", qsock->sid.seqnum);
+
+    ngx_quic_close_socket(c, qsock);
+
+    /* restore socket count up to a limit after deletion */
+    if (ngx_quic_create_sockets(c) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_quic_create_sockets(ngx_connection_t *c)
+{
+    ngx_uint_t              n;
+    ngx_quic_socket_t      *qsock;
+    ngx_quic_connection_t  *qc;
+
+    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 create sockets has:%ui max:%ui", qc->nsockets, n);
+
+    while (qc->nsockets < n) {
+
+        qsock = ngx_quic_create_socket(c, qc);
+        if (qsock == NULL) {
+            return NGX_ERROR;
+        }
+
+        if (ngx_quic_listen(c, qc, qsock) != NGX_OK) {
+            return NGX_ERROR;
+        }
+
+        if (ngx_quic_send_server_id(c, &qsock->sid) != NGX_OK) {
+            return NGX_ERROR;
+        }
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_send_server_id(ngx_connection_t *c, ngx_quic_server_id_t *sid)
+{
+    ngx_str_t               dcid;
+    ngx_quic_frame_t       *frame;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    dcid.len = sid->len;
+    dcid.data = sid->id;
+
+    frame = ngx_quic_alloc_frame(c);
+    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, sid->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;
+}
+
+
+ngx_int_t
+ngx_quic_free_client_id(ngx_connection_t *c, ngx_quic_client_id_t *cid)
+{
+    ngx_quic_frame_t       *frame;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    frame = ngx_quic_alloc_frame(c);
+    if (frame == NULL) {
+        return NGX_ERROR;
+    }
+
+    frame->level = ssl_encryption_application;
+    frame->type = NGX_QUIC_FT_RETIRE_CONNECTION_ID;
+    frame->u.retire_cid.sequence_number = cid->seqnum;
+
+    ngx_quic_queue_frame(qc, frame);
+
+    /* we are no longer going to use this client id */
+
+    ngx_queue_remove(&cid->queue);
+    ngx_queue_insert_head(&qc->free_client_ids, &cid->queue);
+
+    qc->nclient_ids--;
+
+    return NGX_OK;
+}
diff --git a/src/event/quic/ngx_event_quic_connid.h b/src/event/quic/ngx_event_quic_connid.h
new file mode 100644
index 0000000..33e9c65
--- /dev/null
+++ b/src/event/quic/ngx_event_quic_connid.h
@@ -0,0 +1,29 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_EVENT_QUIC_CONNID_H_INCLUDED_
+#define _NGX_EVENT_QUIC_CONNID_H_INCLUDED_
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+
+ngx_int_t ngx_quic_handle_retire_connection_id_frame(ngx_connection_t *c,
+    ngx_quic_retire_cid_frame_t *f);
+ngx_int_t ngx_quic_handle_new_connection_id_frame(ngx_connection_t *c,
+    ngx_quic_new_conn_id_frame_t *f);
+
+ngx_int_t ngx_quic_create_sockets(ngx_connection_t *c);
+ngx_int_t ngx_quic_create_server_id(ngx_connection_t *c, u_char *id);
+
+ngx_quic_client_id_t *ngx_quic_create_client_id(ngx_connection_t *c,
+    ngx_str_t *id, uint64_t seqnum, u_char *token);
+ngx_quic_client_id_t *ngx_quic_next_client_id(ngx_connection_t *c);
+ngx_int_t ngx_quic_free_client_id(ngx_connection_t *c,
+    ngx_quic_client_id_t *cid);
+
+#endif /* _NGX_EVENT_QUIC_CONNID_H_INCLUDED_ */
diff --git a/src/event/quic/ngx_event_quic_frames.c b/src/event/quic/ngx_event_quic_frames.c
new file mode 100644
index 0000000..6ea908c
--- /dev/null
+++ b/src/event/quic/ngx_event_quic_frames.c
@@ -0,0 +1,895 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_event.h>
+#include <ngx_event_quic_connection.h>
+
+
+#define NGX_QUIC_BUFFER_SIZE  4096
+
+#define ngx_quic_buf_refs(b)         (b)->shadow->num
+#define ngx_quic_buf_inc_refs(b)     ngx_quic_buf_refs(b)++
+#define ngx_quic_buf_dec_refs(b)     ngx_quic_buf_refs(b)--
+#define ngx_quic_buf_set_refs(b, v)  ngx_quic_buf_refs(b) = v
+
+
+static ngx_buf_t *ngx_quic_alloc_buf(ngx_connection_t *c);
+static void ngx_quic_free_buf(ngx_connection_t *c, ngx_buf_t *b);
+static ngx_buf_t *ngx_quic_clone_buf(ngx_connection_t *c, ngx_buf_t *b);
+static ngx_int_t ngx_quic_split_chain(ngx_connection_t *c, ngx_chain_t *cl,
+    off_t offset);
+
+
+static ngx_buf_t *
+ngx_quic_alloc_buf(ngx_connection_t *c)
+{
+    u_char                 *p;
+    ngx_buf_t              *b;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    b = qc->free_bufs;
+
+    if (b) {
+        qc->free_bufs = b->shadow;
+        p = b->start;
+
+    } else {
+        b = qc->free_shadow_bufs;
+
+        if (b) {
+            qc->free_shadow_bufs = b->shadow;
+
+#ifdef NGX_QUIC_DEBUG_ALLOC
+            ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                           "quic use shadow buffer n:%ui %ui",
+                           ++qc->nbufs, --qc->nshadowbufs);
+#endif
+
+        } else {
+            b = ngx_palloc(c->pool, sizeof(ngx_buf_t));
+            if (b == NULL) {
+                return NULL;
+            }
+
+#ifdef NGX_QUIC_DEBUG_ALLOC
+            ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                           "quic new buffer n:%ui", ++qc->nbufs);
+#endif
+        }
+
+        p = ngx_pnalloc(c->pool, NGX_QUIC_BUFFER_SIZE);
+        if (p == NULL) {
+            return NULL;
+        }
+    }
+
+#ifdef NGX_QUIC_DEBUG_ALLOC
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, "quic alloc buffer %p", b);
+#endif
+
+    ngx_memzero(b, sizeof(ngx_buf_t));
+
+    b->tag = (ngx_buf_tag_t) &ngx_quic_alloc_buf;
+    b->temporary = 1;
+    b->shadow = b;
+
+    b->start = p;
+    b->pos = p;
+    b->last = p;
+    b->end = p + NGX_QUIC_BUFFER_SIZE;
+
+    ngx_quic_buf_set_refs(b, 1);
+
+    return b;
+}
+
+
+static void
+ngx_quic_free_buf(ngx_connection_t *c, ngx_buf_t *b)
+{
+    ngx_buf_t              *shadow;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    ngx_quic_buf_dec_refs(b);
+
+#ifdef NGX_QUIC_DEBUG_ALLOC
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic free buffer %p r:%ui",
+                   b, (ngx_uint_t) ngx_quic_buf_refs(b));
+#endif
+
+    shadow = b->shadow;
+
+    if (ngx_quic_buf_refs(b) == 0) {
+        shadow->shadow = qc->free_bufs;
+        qc->free_bufs = shadow;
+    }
+
+    if (b != shadow) {
+        b->shadow = qc->free_shadow_bufs;
+        qc->free_shadow_bufs = b;
+    }
+
+}
+
+
+static ngx_buf_t *
+ngx_quic_clone_buf(ngx_connection_t *c, ngx_buf_t *b)
+{
+    ngx_buf_t              *nb;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    nb = qc->free_shadow_bufs;
+
+    if (nb) {
+        qc->free_shadow_bufs = nb->shadow;
+
+    } else {
+        nb = ngx_palloc(c->pool, sizeof(ngx_buf_t));
+        if (nb == NULL) {
+            return NULL;
+        }
+
+#ifdef NGX_QUIC_DEBUG_ALLOC
+        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic new shadow buffer n:%ui", ++qc->nshadowbufs);
+#endif
+    }
+
+    *nb = *b;
+
+    ngx_quic_buf_inc_refs(b);
+
+#ifdef NGX_QUIC_DEBUG_ALLOC
+    ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic clone buffer %p %p r:%ui",
+                   b, nb, (ngx_uint_t) ngx_quic_buf_refs(b));
+#endif
+
+    return nb;
+}
+
+
+static ngx_int_t
+ngx_quic_split_chain(ngx_connection_t *c, ngx_chain_t *cl, off_t offset)
+{
+    ngx_buf_t    *b, *tb;
+    ngx_chain_t  *tail;
+
+    b = cl->buf;
+
+    tail = ngx_alloc_chain_link(c->pool);
+    if (tail == NULL) {
+        return NGX_ERROR;
+    }
+
+    tb = ngx_quic_clone_buf(c, b);
+    if (tb == NULL) {
+        return NGX_ERROR;
+    }
+
+    tail->buf = tb;
+
+    tb->pos += offset;
+
+    b->last = tb->pos;
+    b->last_buf = 0;
+
+    tail->next = cl->next;
+    cl->next = tail;
+
+    return NGX_OK;
+}
+
+
+ngx_quic_frame_t *
+ngx_quic_alloc_frame(ngx_connection_t *c)
+{
+    ngx_queue_t            *q;
+    ngx_quic_frame_t       *frame;
+    ngx_quic_connection_t  *qc;
+
+    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_ALLOC
+        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic reuse frame n:%ui", qc->nframes);
+#endif
+
+    } else if (qc->nframes < 10000) {
+        frame = ngx_palloc(c->pool, sizeof(ngx_quic_frame_t));
+        if (frame == NULL) {
+            return NULL;
+        }
+
+        ++qc->nframes;
+
+#ifdef NGX_QUIC_DEBUG_ALLOC
+        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic alloc frame n:%ui", qc->nframes);
+#endif
+
+    } else {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0, "quic flood detected");
+        return NULL;
+    }
+
+    ngx_memzero(frame, sizeof(ngx_quic_frame_t));
+
+    return frame;
+}
+
+
+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_quic_free_chain(c, frame->data);
+    }
+
+    ngx_queue_insert_head(&qc->free_frames, &frame->queue);
+
+#ifdef NGX_QUIC_DEBUG_ALLOC
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic free frame n:%ui", qc->nframes);
+#endif
+}
+
+
+void
+ngx_quic_free_chain(ngx_connection_t *c, ngx_chain_t *in)
+{
+    ngx_chain_t  *cl;
+
+    while (in) {
+        cl = in;
+        in = in->next;
+
+        ngx_quic_free_buf(c, cl->buf);
+        ngx_free_chain(c->pool, cl);
+    }
+}
+
+
+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);
+}
+
+
+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);
+}
+
+
+ngx_int_t
+ngx_quic_split_frame(ngx_connection_t *c, ngx_quic_frame_t *f, size_t len)
+{
+    size_t                     shrink;
+    ngx_chain_t               *out;
+    ngx_quic_frame_t          *nf;
+    ngx_quic_buffer_t          qb;
+    ngx_quic_ordered_frame_t  *of, *onf;
+
+    switch (f->type) {
+    case NGX_QUIC_FT_CRYPTO:
+    case NGX_QUIC_FT_STREAM:
+        break;
+
+    default:
+        return NGX_DECLINED;
+    }
+
+    if ((size_t) f->len <= len) {
+        return NGX_OK;
+    }
+
+    shrink = f->len - len;
+
+    ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic split frame now:%uz need:%uz shrink:%uz",
+                   f->len, len, shrink);
+
+    of = &f->u.ord;
+
+    if (of->length <= shrink) {
+        return NGX_DECLINED;
+    }
+
+    of->length -= shrink;
+    f->len = ngx_quic_create_frame(NULL, f);
+
+    if ((size_t) f->len > len) {
+        ngx_log_error(NGX_LOG_ERR, c->log, 0, "could not split QUIC frame");
+        return NGX_ERROR;
+    }
+
+    ngx_memzero(&qb, sizeof(ngx_quic_buffer_t));
+    qb.chain = f->data;
+
+    out = ngx_quic_read_buffer(c, &qb, of->length);
+    if (out == NGX_CHAIN_ERROR) {
+        return NGX_ERROR;
+    }
+
+    f->data = out;
+
+    nf = ngx_quic_alloc_frame(c);
+    if (nf == NULL) {
+        return NGX_ERROR;
+    }
+
+    *nf = *f;
+    onf = &nf->u.ord;
+    onf->offset += of->length;
+    onf->length = shrink;
+    nf->len = ngx_quic_create_frame(NULL, nf);
+    nf->data = qb.chain;
+
+    if (f->type == NGX_QUIC_FT_STREAM) {
+        f->u.stream.fin = 0;
+    }
+
+    ngx_queue_insert_after(&f->queue, &nf->queue);
+
+    return NGX_OK;
+}
+
+
+ngx_chain_t *
+ngx_quic_copy_buffer(ngx_connection_t *c, u_char *data, size_t len)
+{
+    ngx_buf_t          buf;
+    ngx_chain_t        cl, *out;
+    ngx_quic_buffer_t  qb;
+
+    ngx_memzero(&buf, sizeof(ngx_buf_t));
+
+    buf.pos = data;
+    buf.last = buf.pos + len;
+    buf.temporary = 1;
+
+    cl.buf = &buf;
+    cl.next = NULL;
+
+    ngx_memzero(&qb, sizeof(ngx_quic_buffer_t));
+
+    if (ngx_quic_write_buffer(c, &qb, &cl, len, 0) == NGX_CHAIN_ERROR) {
+        return NGX_CHAIN_ERROR;
+    }
+
+    out = ngx_quic_read_buffer(c, &qb, len);
+    if (out == NGX_CHAIN_ERROR) {
+        return NGX_CHAIN_ERROR;
+    }
+
+    ngx_quic_free_buffer(c, &qb);
+
+    return out;
+}
+
+
+ngx_chain_t *
+ngx_quic_read_buffer(ngx_connection_t *c, ngx_quic_buffer_t *qb, uint64_t limit)
+{
+    uint64_t      n;
+    ngx_buf_t    *b;
+    ngx_chain_t  *out, **ll;
+
+    out = qb->chain;
+
+    for (ll = &out; *ll; ll = &(*ll)->next) {
+        b = (*ll)->buf;
+
+        if (b->sync) {
+            /* hole */
+            break;
+        }
+
+        if (limit == 0) {
+            break;
+        }
+
+        n = b->last - b->pos;
+
+        if (n > limit) {
+            if (ngx_quic_split_chain(c, *ll, limit) != NGX_OK) {
+                return NGX_CHAIN_ERROR;
+            }
+
+            n = limit;
+        }
+
+        limit -= n;
+        qb->offset += n;
+    }
+
+    if (qb->offset >= qb->last_offset) {
+        qb->last_chain = NULL;
+    }
+
+    qb->chain = *ll;
+    *ll = NULL;
+
+    return out;
+}
+
+
+void
+ngx_quic_skip_buffer(ngx_connection_t *c, ngx_quic_buffer_t *qb,
+    uint64_t offset)
+{
+    size_t        n;
+    ngx_buf_t    *b;
+    ngx_chain_t  *cl;
+
+    while (qb->chain) {
+        if (qb->offset >= offset) {
+            break;
+        }
+
+        cl = qb->chain;
+        b = cl->buf;
+        n = b->last - b->pos;
+
+        if (qb->offset + n > offset) {
+            n = offset - qb->offset;
+            b->pos += n;
+            qb->offset += n;
+            break;
+        }
+
+        qb->offset += n;
+        qb->chain = cl->next;
+
+        cl->next = NULL;
+        ngx_quic_free_chain(c, cl);
+    }
+
+    if (qb->chain == NULL) {
+        qb->offset = offset;
+    }
+
+    if (qb->offset >= qb->last_offset) {
+        qb->last_chain = NULL;
+    }
+}
+
+
+ngx_chain_t *
+ngx_quic_alloc_chain(ngx_connection_t *c)
+{
+    ngx_chain_t  *cl;
+
+    cl = ngx_alloc_chain_link(c->pool);
+    if (cl == NULL) {
+        return NULL;
+    }
+
+    cl->buf = ngx_quic_alloc_buf(c);
+    if (cl->buf == NULL) {
+        return NULL;
+    }
+
+    return cl;
+}
+
+
+ngx_chain_t *
+ngx_quic_write_buffer(ngx_connection_t *c, ngx_quic_buffer_t *qb,
+    ngx_chain_t *in, uint64_t limit, uint64_t offset)
+{
+    u_char       *p;
+    uint64_t      n, base;
+    ngx_buf_t    *b;
+    ngx_chain_t  *cl, **chain;
+
+    if (qb->last_chain && offset >= qb->last_offset) {
+        base = qb->last_offset;
+        chain = &qb->last_chain;
+
+    } else {
+        base = qb->offset;
+        chain = &qb->chain;
+    }
+
+    while (in && limit) {
+
+        if (offset < base) {
+            n = ngx_min((uint64_t) (in->buf->last - in->buf->pos),
+                        ngx_min(base - offset, limit));
+
+            in->buf->pos += n;
+            offset += n;
+            limit -= n;
+
+            if (in->buf->pos == in->buf->last) {
+                in = in->next;
+            }
+
+            continue;
+        }
+
+        cl = *chain;
+
+        if (cl == NULL) {
+            cl = ngx_quic_alloc_chain(c);
+            if (cl == NULL) {
+                return NGX_CHAIN_ERROR;
+            }
+
+            cl->buf->last = cl->buf->end;
+            cl->buf->sync = 1; /* hole */
+            cl->next = NULL;
+            *chain = cl;
+        }
+
+        b = cl->buf;
+        n = b->last - b->pos;
+
+        if (base + n <= offset) {
+            base += n;
+            chain = &cl->next;
+            continue;
+        }
+
+        if (b->sync && offset > base) {
+            if (ngx_quic_split_chain(c, cl, offset - base) != NGX_OK) {
+                return NGX_CHAIN_ERROR;
+            }
+
+            continue;
+        }
+
+        p = b->pos + (offset - base);
+
+        while (in) {
+
+            if (!ngx_buf_in_memory(in->buf) || in->buf->pos == in->buf->last) {
+                in = in->next;
+                continue;
+            }
+
+            if (p == b->last || limit == 0) {
+                break;
+            }
+
+            n = ngx_min(b->last - p, in->buf->last - in->buf->pos);
+            n = ngx_min(n, limit);
+
+            if (b->sync) {
+                ngx_memcpy(p, in->buf->pos, n);
+                qb->size += n;
+            }
+
+            p += n;
+            in->buf->pos += n;
+            offset += n;
+            limit -= n;
+        }
+
+        if (b->sync && p == b->last) {
+            b->sync = 0;
+            continue;
+        }
+
+        if (b->sync && p != b->pos) {
+            if (ngx_quic_split_chain(c, cl, p - b->pos) != NGX_OK) {
+                return NGX_CHAIN_ERROR;
+            }
+
+            b->sync = 0;
+        }
+    }
+
+    qb->last_offset = base;
+    qb->last_chain = *chain;
+
+    return in;
+}
+
+
+void
+ngx_quic_free_buffer(ngx_connection_t *c, ngx_quic_buffer_t *qb)
+{
+    ngx_quic_free_chain(c, qb->chain);
+
+    qb->chain = NULL;
+    qb->last_chain = NULL;
+}
+
+
+#if (NGX_DEBUG)
+
+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);
+
+#ifdef NGX_QUIC_DEBUG_FRAMES
+        {
+            ngx_chain_t  *cl;
+
+            p = ngx_slprintf(p, last, " data:");
+
+            for (cl = f->data; cl; cl = cl->next) {
+                p = ngx_slprintf(p, last, "%*xs",
+                                 cl->buf->last - cl->buf->pos, cl->buf->pos);
+            }
+        }
+#endif
+
+        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);
+
+        if (f->data) {
+            pos = f->data->buf->pos;
+            end = f->data->buf->last;
+
+        } else {
+            pos = NULL;
+            end = NULL;
+        }
+
+        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->type == NGX_QUIC_FT_CONNECTION_CLOSE ? "" : "_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_STREAM:
+        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");
+        }
+
+#ifdef NGX_QUIC_DEBUG_FRAMES
+        {
+            ngx_chain_t  *cl;
+
+            p = ngx_slprintf(p, last, " data:");
+
+            for (cl = f->data; cl; cl = cl->next) {
+                p = ngx_slprintf(p, last, "%*xs",
+                                 cl->buf->last - cl->buf->pos, cl->buf->pos);
+            }
+        }
+#endif
+
+        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%*xs",
+                         sizeof(f->u.path_challenge.data),
+                         f->u.path_challenge.data);
+        break;
+
+    case NGX_QUIC_FT_PATH_RESPONSE:
+        p = ngx_slprintf(p, last, "PATH_RESPONSE data:0x%*xs",
+                         sizeof(f->u.path_challenge.data),
+                         f->u.path_challenge.data);
+        break;
+
+    case NGX_QUIC_FT_NEW_TOKEN:
+        p = ngx_slprintf(p, last, "NEW_TOKEN");
+
+#ifdef NGX_QUIC_DEBUG_FRAMES
+        {
+            ngx_chain_t  *cl;
+
+            p = ngx_slprintf(p, last, " token:");
+
+            for (cl = f->data; cl; cl = cl->next) {
+                p = ngx_slprintf(p, last, "%*xs",
+                                 cl->buf->last - cl->buf->pos, cl->buf->pos);
+            }
+        }
+#endif
+
+        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_debug5(NGX_LOG_DEBUG_EVENT, log, 0, "quic frame %s %s:%uL %*s",
+                   tx ? "tx" : "rx", ngx_quic_level_name(f->level), f->pnum,
+                   p - buf, buf);
+}
+
+#endif
diff --git a/src/event/quic/ngx_event_quic_frames.h b/src/event/quic/ngx_event_quic_frames.h
new file mode 100644
index 0000000..2d55af9
--- /dev/null
+++ b/src/event/quic/ngx_event_quic_frames.h
@@ -0,0 +1,45 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_EVENT_QUIC_FRAMES_H_INCLUDED_
+#define _NGX_EVENT_QUIC_FRAMES_H_INCLUDED_
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+
+typedef ngx_int_t (*ngx_quic_frame_handler_pt)(ngx_connection_t *c,
+    ngx_quic_frame_t *frame, void *data);
+
+
+ngx_quic_frame_t *ngx_quic_alloc_frame(ngx_connection_t *c);
+void ngx_quic_free_frame(ngx_connection_t *c, ngx_quic_frame_t *frame);
+void ngx_quic_free_frames(ngx_connection_t *c, ngx_queue_t *frames);
+void ngx_quic_queue_frame(ngx_quic_connection_t *qc, ngx_quic_frame_t *frame);
+ngx_int_t ngx_quic_split_frame(ngx_connection_t *c, ngx_quic_frame_t *f,
+    size_t len);
+
+ngx_chain_t *ngx_quic_alloc_chain(ngx_connection_t *c);
+void ngx_quic_free_chain(ngx_connection_t *c, ngx_chain_t *in);
+
+ngx_chain_t *ngx_quic_copy_buffer(ngx_connection_t *c, u_char *data,
+    size_t len);
+ngx_chain_t *ngx_quic_read_buffer(ngx_connection_t *c, ngx_quic_buffer_t *qb,
+    uint64_t limit);
+ngx_chain_t *ngx_quic_write_buffer(ngx_connection_t *c, ngx_quic_buffer_t *qb,
+    ngx_chain_t *in, uint64_t limit, uint64_t offset);
+void ngx_quic_skip_buffer(ngx_connection_t *c, ngx_quic_buffer_t *qb,
+    uint64_t offset);
+void ngx_quic_free_buffer(ngx_connection_t *c, ngx_quic_buffer_t *qb);
+
+#if (NGX_DEBUG)
+void ngx_quic_log_frame(ngx_log_t *log, ngx_quic_frame_t *f, ngx_uint_t tx);
+#else
+#define ngx_quic_log_frame(log, f, tx)
+#endif
+
+#endif /* _NGX_EVENT_QUIC_FRAMES_H_INCLUDED_ */
diff --git a/src/event/quic/ngx_event_quic_migration.c b/src/event/quic/ngx_event_quic_migration.c
new file mode 100644
index 0000000..2d1467e
--- /dev/null
+++ b/src/event/quic/ngx_event_quic_migration.c
@@ -0,0 +1,1003 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_event.h>
+#include <ngx_event_quic_connection.h>
+
+
+#define NGX_QUIC_PATH_MTU_DELAY       100
+#define NGX_QUIC_PATH_MTU_PRECISION   16
+
+
+static void ngx_quic_set_connection_path(ngx_connection_t *c,
+    ngx_quic_path_t *path);
+static ngx_int_t ngx_quic_validate_path(ngx_connection_t *c,
+    ngx_quic_path_t *path);
+static ngx_int_t ngx_quic_send_path_challenge(ngx_connection_t *c,
+    ngx_quic_path_t *path);
+static void ngx_quic_set_path_timer(ngx_connection_t *c);
+static ngx_int_t ngx_quic_expire_path_validation(ngx_connection_t *c,
+    ngx_quic_path_t *path);
+static ngx_int_t ngx_quic_expire_path_mtu_delay(ngx_connection_t *c,
+    ngx_quic_path_t *path);
+static ngx_int_t ngx_quic_expire_path_mtu_discovery(ngx_connection_t *c,
+    ngx_quic_path_t *path);
+static ngx_quic_path_t *ngx_quic_get_path(ngx_connection_t *c, ngx_uint_t tag);
+static ngx_int_t ngx_quic_send_path_mtu_probe(ngx_connection_t *c,
+    ngx_quic_path_t *path);
+
+
+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)
+{
+    size_t                  min;
+    ngx_quic_frame_t       *fp;
+    ngx_quic_connection_t  *qc;
+
+    if (pkt->level != ssl_encryption_application || pkt->path_challenged) {
+        ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic ignoring PATH_CHALLENGE");
+        return NGX_OK;
+    }
+
+    pkt->path_challenged = 1;
+
+    qc = ngx_quic_get_connection(c);
+
+    fp = ngx_quic_alloc_frame(c);
+    if (fp == NULL) {
+        return NGX_ERROR;
+    }
+
+    fp->level = ssl_encryption_application;
+    fp->type = NGX_QUIC_FT_PATH_RESPONSE;
+    fp->u.path_response = *f;
+
+    /*
+     * RFC 9000, 8.2.2.  Path Validation Responses
+     *
+     * A PATH_RESPONSE frame MUST be sent on the network path where the
+     * PATH_CHALLENGE frame was received.
+     */
+
+    /*
+     * An endpoint MUST expand datagrams that contain a PATH_RESPONSE frame
+     * to at least the smallest allowed maximum datagram size of 1200 bytes.
+     * ...
+     * However, an endpoint MUST NOT expand the datagram containing the
+     * PATH_RESPONSE if the resulting data exceeds the anti-amplification limit.
+     */
+
+    min = (ngx_quic_path_limit(c, pkt->path, 1200) < 1200) ? 0 : 1200;
+
+    if (ngx_quic_frame_sendto(c, fp, min, pkt->path) == NGX_ERROR) {
+        return NGX_ERROR;
+    }
+
+    if (pkt->path == qc->path) {
+        /*
+         * RFC 9000, 9.3.3.  Off-Path Packet Forwarding
+         *
+         * An endpoint that receives a PATH_CHALLENGE on an active path SHOULD
+         * send a non-probing packet in response.
+         */
+
+        fp = ngx_quic_alloc_frame(c);
+        if (fp == NULL) {
+            return NGX_ERROR;
+        }
+
+        fp->level = ssl_encryption_application;
+        fp->type = NGX_QUIC_FT_PING;
+
+        ngx_quic_queue_frame(qc, fp);
+    }
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_quic_handle_path_response_frame(ngx_connection_t *c,
+    ngx_quic_path_challenge_frame_t *f)
+{
+    ngx_uint_t              rst;
+    ngx_queue_t            *q;
+    ngx_quic_path_t        *path, *prev;
+    ngx_quic_send_ctx_t    *ctx;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    /*
+     * RFC 9000, 8.2.3.  Successful Path Validation
+     *
+     * A PATH_RESPONSE frame received on any network path validates the path
+     * on which the PATH_CHALLENGE was sent.
+     */
+
+    for (q = ngx_queue_head(&qc->paths);
+         q != ngx_queue_sentinel(&qc->paths);
+         q = ngx_queue_next(q))
+    {
+        path = ngx_queue_data(q, ngx_quic_path_t, queue);
+
+        if (path->state != NGX_QUIC_PATH_VALIDATING) {
+            continue;
+        }
+
+        if (ngx_memcmp(path->challenge[0], f->data, sizeof(f->data)) == 0
+            || ngx_memcmp(path->challenge[1], f->data, sizeof(f->data)) == 0)
+        {
+            goto valid;
+        }
+    }
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic stale PATH_RESPONSE ignored");
+
+    return NGX_OK;
+
+valid:
+
+    /*
+     * RFC 9000, 9.4.  Loss Detection and Congestion Control
+     *
+     * On confirming a peer's ownership of its new address,
+     * an endpoint MUST immediately reset the congestion controller
+     * and round-trip time estimator for the new path to initial values
+     * unless the only change in the peer's address is its port number.
+     */
+
+    rst = 1;
+
+    prev = ngx_quic_get_path(c, NGX_QUIC_PATH_BACKUP);
+
+    if (prev != NULL) {
+
+        if (ngx_cmp_sockaddr(prev->sockaddr, prev->socklen,
+                             path->sockaddr, path->socklen, 0)
+            == NGX_OK)
+        {
+            /* address did not change */
+            rst = 0;
+
+            path->mtu = prev->mtu;
+            path->max_mtu = prev->max_mtu;
+            path->mtu_unvalidated = 0;
+        }
+    }
+
+    if (rst) {
+        /* prevent old path packets contribution to congestion control */
+
+        ctx = ngx_quic_get_send_ctx(qc, ssl_encryption_application);
+        qc->rst_pnum = ctx->pnum;
+
+        ngx_memzero(&qc->congestion, sizeof(ngx_quic_congestion_t));
+
+        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;
+
+        ngx_quic_init_rtt(qc);
+    }
+
+    path->validated = 1;
+
+    if (path->mtu_unvalidated) {
+        path->mtu_unvalidated = 0;
+        return ngx_quic_validate_path(c, path);
+    }
+
+    /*
+     * RFC 9000, 9.3.  Responding to Connection Migration
+     *
+     *  After verifying a new client address, the server SHOULD
+     *  send new address validation tokens (Section 8) to the client.
+     */
+
+    if (ngx_quic_send_new_token(c, path) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                  "quic path seq:%uL addr:%V successfully validated",
+                  path->seqnum, &path->addr_text);
+
+    ngx_quic_path_dbg(c, "is validated", path);
+
+    ngx_quic_discover_path_mtu(c, path);
+
+    return NGX_OK;
+}
+
+
+ngx_quic_path_t *
+ngx_quic_new_path(ngx_connection_t *c,
+    struct sockaddr *sockaddr, socklen_t socklen, ngx_quic_client_id_t *cid)
+{
+    ngx_queue_t            *q;
+    ngx_quic_path_t        *path;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    if (!ngx_queue_empty(&qc->free_paths)) {
+
+        q = ngx_queue_head(&qc->free_paths);
+        path = ngx_queue_data(q, ngx_quic_path_t, queue);
+
+        ngx_queue_remove(&path->queue);
+
+        ngx_memzero(path, sizeof(ngx_quic_path_t));
+
+    } else {
+
+        path = ngx_pcalloc(c->pool, sizeof(ngx_quic_path_t));
+        if (path == NULL) {
+            return NULL;
+        }
+    }
+
+    ngx_queue_insert_tail(&qc->paths, &path->queue);
+
+    path->cid = cid;
+    cid->used = 1;
+
+    path->seqnum = qc->path_seqnum++;
+
+    path->sockaddr = &path->sa.sockaddr;
+    path->socklen = socklen;
+    ngx_memcpy(path->sockaddr, sockaddr, socklen);
+
+    path->addr_text.data = path->text;
+    path->addr_text.len = ngx_sock_ntop(sockaddr, socklen, path->text,
+                                        NGX_SOCKADDR_STRLEN, 1);
+
+    path->mtu = NGX_QUIC_MIN_INITIAL_SIZE;
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic path seq:%uL created addr:%V",
+                   path->seqnum, &path->addr_text);
+    return path;
+}
+
+
+static ngx_quic_path_t *
+ngx_quic_get_path(ngx_connection_t *c, ngx_uint_t tag)
+{
+    ngx_queue_t            *q;
+    ngx_quic_path_t        *path;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    for (q = ngx_queue_head(&qc->paths);
+         q != ngx_queue_sentinel(&qc->paths);
+         q = ngx_queue_next(q))
+    {
+        path = ngx_queue_data(q, ngx_quic_path_t, queue);
+
+        if (path->tag == tag) {
+            return path;
+        }
+    }
+
+    return NULL;
+}
+
+
+ngx_int_t
+ngx_quic_set_path(ngx_connection_t *c, ngx_quic_header_t *pkt)
+{
+    off_t                   len;
+    ngx_queue_t            *q;
+    ngx_quic_path_t        *path, *probe;
+    ngx_quic_socket_t      *qsock;
+    ngx_quic_send_ctx_t    *ctx;
+    ngx_quic_client_id_t   *cid;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+    qsock = ngx_quic_get_socket(c);
+
+    len = pkt->raw->last - pkt->raw->start;
+
+    if (c->udp->buffer == NULL) {
+        /* first ever packet in connection, path already exists  */
+        path = qc->path;
+        goto update;
+    }
+
+    probe = NULL;
+
+    for (q = ngx_queue_head(&qc->paths);
+         q != ngx_queue_sentinel(&qc->paths);
+         q = ngx_queue_next(q))
+    {
+        path = ngx_queue_data(q, ngx_quic_path_t, queue);
+
+        if (ngx_cmp_sockaddr(&qsock->sockaddr.sockaddr, qsock->socklen,
+                             path->sockaddr, path->socklen, 1)
+            == NGX_OK)
+        {
+            goto update;
+        }
+
+        if (path->tag == NGX_QUIC_PATH_PROBE) {
+            probe = path;
+        }
+    }
+
+    /* packet from new path, drop current probe, if any */
+
+    ctx = ngx_quic_get_send_ctx(qc, pkt->level);
+
+    /*
+     * only accept highest-numbered packets to prevent connection id
+     * exhaustion by excessive probing packets from unknown paths
+     */
+    if (pkt->pn != ctx->largest_pn) {
+        return NGX_DONE;
+    }
+
+    if (probe && ngx_quic_free_path(c, probe) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    /* new path requires new client id */
+    cid = ngx_quic_next_client_id(c);
+    if (cid == NULL) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "quic no available client ids for new path");
+        /* stop processing of this datagram */
+        return NGX_DONE;
+    }
+
+    path = ngx_quic_new_path(c, &qsock->sockaddr.sockaddr, qsock->socklen, cid);
+    if (path == NULL) {
+        return NGX_ERROR;
+    }
+
+    path->tag = NGX_QUIC_PATH_PROBE;
+
+    /*
+     * client arrived using new path and previously seen DCID,
+     * this indicates NAT rebinding (or bad client)
+     */
+    if (qsock->used) {
+        pkt->rebound = 1;
+    }
+
+update:
+
+    qsock->used = 1;
+    pkt->path = path;
+
+    /* TODO: this may be too late in some cases;
+     *       for example, if error happens during decrypt(), we cannot
+     *       send CC, if error happens in 1st packet, due to amplification
+     *       limit, because path->received = 0
+     *
+     *       should we account garbage as received or only decrypting packets?
+     */
+    path->received += len;
+
+    ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic packet len:%O via sock seq:%L path seq:%uL",
+                   len, (int64_t) qsock->sid.seqnum, path->seqnum);
+    ngx_quic_path_dbg(c, "status", path);
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_quic_free_path(ngx_connection_t *c, ngx_quic_path_t *path)
+{
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    ngx_queue_remove(&path->queue);
+    ngx_queue_insert_head(&qc->free_paths, &path->queue);
+
+    /*
+     * invalidate CID that is no longer usable for any other path;
+     * this also requests new CIDs from client
+     */
+    if (path->cid) {
+        if (ngx_quic_free_client_id(c, path->cid) != NGX_OK) {
+            return NGX_ERROR;
+        }
+    }
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic path seq:%uL addr:%V retired",
+                   path->seqnum, &path->addr_text);
+
+    return NGX_OK;
+}
+
+
+static void
+ngx_quic_set_connection_path(ngx_connection_t *c, ngx_quic_path_t *path)
+{
+    ngx_memcpy(c->sockaddr, path->sockaddr, path->socklen);
+    c->socklen = path->socklen;
+
+    if (c->addr_text.data) {
+        c->addr_text.len = ngx_sock_ntop(c->sockaddr, c->socklen,
+                                         c->addr_text.data,
+                                         c->listening->addr_text_max_len, 0);
+    }
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic send path set to seq:%uL addr:%V",
+                   path->seqnum, &path->addr_text);
+}
+
+
+ngx_int_t
+ngx_quic_handle_migration(ngx_connection_t *c, ngx_quic_header_t *pkt)
+{
+    ngx_quic_path_t        *next, *bkp;
+    ngx_quic_send_ctx_t    *ctx;
+    ngx_quic_connection_t  *qc;
+
+    /* got non-probing packet via non-active path */
+
+    qc = ngx_quic_get_connection(c);
+
+    ctx = ngx_quic_get_send_ctx(qc, pkt->level);
+
+    /*
+     * RFC 9000, 9.3.  Responding to Connection Migration
+     *
+     * An endpoint only changes the address to which it sends packets in
+     * response to the highest-numbered non-probing packet.
+     */
+    if (pkt->pn != ctx->largest_pn) {
+        return NGX_OK;
+    }
+
+    next = pkt->path;
+
+    /*
+     * RFC 9000, 9.3.3:
+     *
+     * In response to an apparent migration, endpoints MUST validate the
+     * previously active path using a PATH_CHALLENGE frame.
+     */
+    if (pkt->rebound) {
+
+        /* NAT rebinding: client uses new path with old SID */
+        if (ngx_quic_validate_path(c, qc->path) != NGX_OK) {
+            return NGX_ERROR;
+        }
+    }
+
+    if (qc->path->validated) {
+
+        if (next->tag != NGX_QUIC_PATH_BACKUP) {
+            /* can delete backup path, if any */
+            bkp = ngx_quic_get_path(c, NGX_QUIC_PATH_BACKUP);
+
+            if (bkp && ngx_quic_free_path(c, bkp) != NGX_OK) {
+                return NGX_ERROR;
+            }
+        }
+
+        qc->path->tag = NGX_QUIC_PATH_BACKUP;
+        ngx_quic_path_dbg(c, "is now backup", qc->path);
+
+    } else {
+        if (ngx_quic_free_path(c, qc->path) != NGX_OK) {
+            return NGX_ERROR;
+        }
+    }
+
+    /* switch active path to migrated */
+    qc->path = next;
+    qc->path->tag = NGX_QUIC_PATH_ACTIVE;
+
+    ngx_quic_set_connection_path(c, next);
+
+    if (!next->validated && next->state != NGX_QUIC_PATH_VALIDATING) {
+        if (ngx_quic_validate_path(c, next) != NGX_OK) {
+            return NGX_ERROR;
+        }
+    }
+
+    ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                  "quic migrated to path seq:%uL addr:%V",
+                  qc->path->seqnum, &qc->path->addr_text);
+
+    ngx_quic_path_dbg(c, "is now active", qc->path);
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_validate_path(ngx_connection_t *c, ngx_quic_path_t *path)
+{
+    ngx_msec_t              pto;
+    ngx_quic_send_ctx_t    *ctx;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic initiated validation of path seq:%uL", path->seqnum);
+
+    path->tries = 0;
+
+    if (RAND_bytes((u_char *) path->challenge, sizeof(path->challenge)) != 1) {
+        return NGX_ERROR;
+    }
+
+    (void) ngx_quic_send_path_challenge(c, path);
+
+    ctx = ngx_quic_get_send_ctx(qc, ssl_encryption_application);
+    pto = ngx_max(ngx_quic_pto(c, ctx), 1000);
+
+    path->expires = ngx_current_msec + pto;
+    path->state = NGX_QUIC_PATH_VALIDATING;
+
+    ngx_quic_set_path_timer(c);
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_send_path_challenge(ngx_connection_t *c, ngx_quic_path_t *path)
+{
+    size_t             min;
+    ngx_uint_t         n;
+    ngx_quic_frame_t  *frame;
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic path seq:%uL send path_challenge tries:%ui",
+                   path->seqnum, path->tries);
+
+    for (n = 0; n < 2; n++) {
+
+        frame = ngx_quic_alloc_frame(c);
+        if (frame == NULL) {
+            return NGX_ERROR;
+        }
+
+        frame->level = ssl_encryption_application;
+        frame->type = NGX_QUIC_FT_PATH_CHALLENGE;
+
+        ngx_memcpy(frame->u.path_challenge.data, path->challenge[n], 8);
+
+        /*
+         * RFC 9000, 8.2.1.  Initiating Path Validation
+         *
+         * An endpoint MUST expand datagrams that contain a PATH_CHALLENGE frame
+         * to at least the smallest allowed maximum datagram size of 1200 bytes,
+         * unless the anti-amplification limit for the path does not permit
+         * sending a datagram of this size.
+         */
+
+        if (path->mtu_unvalidated
+            || ngx_quic_path_limit(c, path, 1200) < 1200)
+        {
+            min = 0;
+            path->mtu_unvalidated = 1;
+
+        } else {
+            min = 1200;
+        }
+
+        if (ngx_quic_frame_sendto(c, frame, min, path) == NGX_ERROR) {
+            return NGX_ERROR;
+        }
+    }
+
+    return NGX_OK;
+}
+
+
+void
+ngx_quic_discover_path_mtu(ngx_connection_t *c, ngx_quic_path_t *path)
+{
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    if (path->max_mtu) {
+        if (path->max_mtu - path->mtu <= NGX_QUIC_PATH_MTU_PRECISION) {
+            path->state = NGX_QUIC_PATH_IDLE;
+            ngx_quic_set_path_timer(c);
+            return;
+        }
+
+        path->mtud = (path->mtu + path->max_mtu) / 2;
+
+    } else {
+        path->mtud = path->mtu * 2;
+
+        if (path->mtud >= qc->ctp.max_udp_payload_size) {
+            path->mtud = qc->ctp.max_udp_payload_size;
+            path->max_mtu = qc->ctp.max_udp_payload_size;
+        }
+    }
+
+    path->state = NGX_QUIC_PATH_WAITING;
+    path->expires = ngx_current_msec + NGX_QUIC_PATH_MTU_DELAY;
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic path seq:%uL schedule mtu:%uz",
+                   path->seqnum, path->mtud);
+
+    ngx_quic_set_path_timer(c);
+}
+
+
+static void
+ngx_quic_set_path_timer(ngx_connection_t *c)
+{
+    ngx_msec_t              now;
+    ngx_queue_t            *q;
+    ngx_msec_int_t          left, next;
+    ngx_quic_path_t        *path;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    now = ngx_current_msec;
+    next = -1;
+
+    for (q = ngx_queue_head(&qc->paths);
+         q != ngx_queue_sentinel(&qc->paths);
+         q = ngx_queue_next(q))
+    {
+        path = ngx_queue_data(q, ngx_quic_path_t, queue);
+
+        if (path->state == NGX_QUIC_PATH_IDLE) {
+            continue;
+        }
+
+        left = path->expires - now;
+        left = ngx_max(left, 1);
+
+        if (next == -1 || left < next) {
+            next = left;
+        }
+    }
+
+    if (next != -1) {
+        ngx_add_timer(&qc->path_validation, next);
+
+    } else if (qc->path_validation.timer_set) {
+        ngx_del_timer(&qc->path_validation);
+    }
+}
+
+
+void
+ngx_quic_path_handler(ngx_event_t *ev)
+{
+    ngx_msec_t              now;
+    ngx_queue_t            *q;
+    ngx_msec_int_t          left;
+    ngx_quic_path_t        *path;
+    ngx_connection_t       *c;
+    ngx_quic_connection_t  *qc;
+
+    c = ev->data;
+    qc = ngx_quic_get_connection(c);
+
+    now = ngx_current_msec;
+
+    q = ngx_queue_head(&qc->paths);
+
+    while (q != ngx_queue_sentinel(&qc->paths)) {
+
+        path = ngx_queue_data(q, ngx_quic_path_t, queue);
+        q = ngx_queue_next(q);
+
+        if (path->state == NGX_QUIC_PATH_IDLE) {
+            continue;
+        }
+
+        left = path->expires - now;
+
+        if (left > 0) {
+            continue;
+        }
+
+        switch (path->state) {
+        case NGX_QUIC_PATH_VALIDATING:
+            if (ngx_quic_expire_path_validation(c, path) != NGX_OK) {
+                goto failed;
+            }
+
+            break;
+
+        case NGX_QUIC_PATH_WAITING:
+            if (ngx_quic_expire_path_mtu_delay(c, path) != NGX_OK) {
+                goto failed;
+            }
+
+            break;
+
+        case NGX_QUIC_PATH_MTUD:
+            if (ngx_quic_expire_path_mtu_discovery(c, path) != NGX_OK) {
+                goto failed;
+            }
+
+            break;
+
+        default:
+            break;
+        }
+    }
+
+    ngx_quic_set_path_timer(c);
+
+    return;
+
+failed:
+
+    ngx_quic_close_connection(c, NGX_ERROR);
+}
+
+
+static ngx_int_t
+ngx_quic_expire_path_validation(ngx_connection_t *c, ngx_quic_path_t *path)
+{
+    ngx_msec_int_t          pto;
+    ngx_quic_path_t        *bkp;
+    ngx_quic_send_ctx_t    *ctx;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+    ctx = ngx_quic_get_send_ctx(qc, ssl_encryption_application);
+
+    if (++path->tries < NGX_QUIC_PATH_RETRIES) {
+        pto = ngx_max(ngx_quic_pto(c, ctx), 1000) << path->tries;
+        path->expires = ngx_current_msec + pto;
+
+        (void) ngx_quic_send_path_challenge(c, path);
+
+        return NGX_OK;
+    }
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic path seq:%uL validation failed", path->seqnum);
+
+    /* found expired path */
+
+    path->validated = 0;
+
+
+    /* RFC 9000, 9.3.2.  On-Path Address Spoofing
+     *
+     * To protect the connection from failing due to such a spurious
+     * migration, an endpoint MUST revert to using the last validated
+     * peer address when validation of a new peer address fails.
+     */
+
+    if (qc->path == path) {
+        /* active path validation failed */
+
+        bkp = ngx_quic_get_path(c, NGX_QUIC_PATH_BACKUP);
+
+        if (bkp == NULL) {
+            qc->error = NGX_QUIC_ERR_NO_VIABLE_PATH;
+            qc->error_reason = "no viable path";
+            return NGX_ERROR;
+        }
+
+        qc->path = bkp;
+        qc->path->tag = NGX_QUIC_PATH_ACTIVE;
+
+        ngx_quic_set_connection_path(c, qc->path);
+
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "quic path seq:%uL addr:%V is restored from backup",
+                      qc->path->seqnum, &qc->path->addr_text);
+
+        ngx_quic_path_dbg(c, "is active", qc->path);
+    }
+
+    return ngx_quic_free_path(c, path);
+}
+
+
+static ngx_int_t
+ngx_quic_expire_path_mtu_delay(ngx_connection_t *c, ngx_quic_path_t *path)
+{
+    ngx_int_t               rc;
+    ngx_uint_t              i;
+    ngx_msec_t              pto;
+    ngx_quic_send_ctx_t    *ctx;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+    ctx = ngx_quic_get_send_ctx(qc, ssl_encryption_application);
+
+    path->tries = 0;
+
+    for ( ;; ) {
+
+        for (i = 0; i < NGX_QUIC_PATH_RETRIES; i++) {
+            path->mtu_pnum[i] = NGX_QUIC_UNSET_PN;
+        }
+
+        rc = ngx_quic_send_path_mtu_probe(c, path);
+
+        if (rc == NGX_ERROR) {
+            return NGX_ERROR;
+        }
+
+        if (rc == NGX_OK) {
+            pto = ngx_quic_pto(c, ctx);
+            path->expires = ngx_current_msec + pto;
+            path->state = NGX_QUIC_PATH_MTUD;
+            return NGX_OK;
+        }
+
+        /* rc == NGX_DECLINED */
+
+        path->max_mtu = path->mtud;
+
+        if (path->max_mtu - path->mtu <= NGX_QUIC_PATH_MTU_PRECISION) {
+            path->state = NGX_QUIC_PATH_IDLE;
+            return NGX_OK;
+        }
+
+        path->mtud = (path->mtu + path->max_mtu) / 2;
+    }
+}
+
+
+static ngx_int_t
+ngx_quic_expire_path_mtu_discovery(ngx_connection_t *c, ngx_quic_path_t *path)
+{
+    ngx_int_t               rc;
+    ngx_msec_int_t          pto;
+    ngx_quic_send_ctx_t    *ctx;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+    ctx = ngx_quic_get_send_ctx(qc, ssl_encryption_application);
+
+    if (++path->tries < NGX_QUIC_PATH_RETRIES) {
+        rc = ngx_quic_send_path_mtu_probe(c, path);
+
+        if (rc == NGX_ERROR) {
+            return NGX_ERROR;
+        }
+
+        if (rc == NGX_OK) {
+            pto = ngx_quic_pto(c, ctx) << path->tries;
+            path->expires = ngx_current_msec + pto;
+            return NGX_OK;
+        }
+
+        /* rc == NGX_DECLINED */
+    }
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic path seq:%uL expired mtu:%uz",
+                   path->seqnum, path->mtud);
+
+    path->max_mtu = path->mtud;
+
+    ngx_quic_discover_path_mtu(c, path);
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_send_path_mtu_probe(ngx_connection_t *c, ngx_quic_path_t *path)
+{
+    size_t                  mtu;
+    uint64_t                pnum;
+    ngx_int_t               rc;
+    ngx_uint_t              log_error;
+    ngx_quic_frame_t       *frame;
+    ngx_quic_send_ctx_t    *ctx;
+    ngx_quic_connection_t  *qc;
+
+    frame = ngx_quic_alloc_frame(c);
+    if (frame == NULL) {
+        return NGX_ERROR;
+    }
+
+    frame->level = ssl_encryption_application;
+    frame->type = NGX_QUIC_FT_PING;
+
+    qc = ngx_quic_get_connection(c);
+    ctx = ngx_quic_get_send_ctx(qc, ssl_encryption_application);
+    pnum = ctx->pnum;
+
+    ngx_log_debug4(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic path seq:%uL send probe "
+                   "mtu:%uz pnum:%uL tries:%ui",
+                   path->seqnum, path->mtud, ctx->pnum, path->tries);
+
+    log_error = c->log_error;
+    c->log_error = NGX_ERROR_IGNORE_EMSGSIZE;
+
+    mtu = path->mtu;
+    path->mtu = path->mtud;
+
+    rc = ngx_quic_frame_sendto(c, frame, path->mtud, path);
+
+    path->mtu = mtu;
+    c->log_error = log_error;
+
+    if (rc == NGX_OK) {
+        path->mtu_pnum[path->tries] = pnum;
+        return NGX_OK;
+    }
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic path seq:%uL rejected mtu:%uz",
+                   path->seqnum, path->mtud);
+
+    if (rc == NGX_ERROR) {
+        if (c->write->error) {
+            c->write->error = 0;
+            return NGX_DECLINED;
+        }
+
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_quic_handle_path_mtu(ngx_connection_t *c, ngx_quic_path_t *path,
+    uint64_t min, uint64_t max)
+{
+    uint64_t    pnum;
+    ngx_uint_t  i;
+
+    if (path->state != NGX_QUIC_PATH_MTUD) {
+        return NGX_OK;
+    }
+
+    for (i = 0; i < NGX_QUIC_PATH_RETRIES; i++) {
+        pnum = path->mtu_pnum[i];
+
+        if (pnum == NGX_QUIC_UNSET_PN) {
+            continue;
+        }
+
+        if (pnum < min || pnum > max) {
+            continue;
+        }
+
+        path->mtu = path->mtud;
+
+        ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic path seq:%uL ack mtu:%uz",
+                       path->seqnum, path->mtu);
+
+        ngx_quic_discover_path_mtu(c, path);
+
+        break;
+    }
+
+    return NGX_OK;
+}
diff --git a/src/event/quic/ngx_event_quic_migration.h b/src/event/quic/ngx_event_quic_migration.h
new file mode 100644
index 0000000..270c572
--- /dev/null
+++ b/src/event/quic/ngx_event_quic_migration.h
@@ -0,0 +1,45 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_EVENT_QUIC_MIGRATION_H_INCLUDED_
+#define _NGX_EVENT_QUIC_MIGRATION_H_INCLUDED_
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+#define NGX_QUIC_PATH_RETRIES   3
+
+#define NGX_QUIC_PATH_PROBE     0
+#define NGX_QUIC_PATH_ACTIVE    1
+#define NGX_QUIC_PATH_BACKUP    2
+
+#define ngx_quic_path_dbg(c, msg, path)                                       \
+    ngx_log_debug7(NGX_LOG_DEBUG_EVENT, c->log, 0,                            \
+                   "quic path seq:%uL %s tx:%O rx:%O valid:%d st:%d mtu:%uz", \
+                   path->seqnum, msg, path->sent, path->received,             \
+                   path->validated, path->state, path->mtu);
+
+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_int_t ngx_quic_handle_path_response_frame(ngx_connection_t *c,
+    ngx_quic_path_challenge_frame_t *f);
+
+ngx_quic_path_t *ngx_quic_new_path(ngx_connection_t *c,
+    struct sockaddr *sockaddr, socklen_t socklen, ngx_quic_client_id_t *cid);
+ngx_int_t ngx_quic_free_path(ngx_connection_t *c, ngx_quic_path_t *path);
+
+ngx_int_t ngx_quic_set_path(ngx_connection_t *c, ngx_quic_header_t *pkt);
+ngx_int_t ngx_quic_handle_migration(ngx_connection_t *c,
+    ngx_quic_header_t *pkt);
+
+void ngx_quic_path_handler(ngx_event_t *ev);
+
+void ngx_quic_discover_path_mtu(ngx_connection_t *c, ngx_quic_path_t *path);
+ngx_int_t ngx_quic_handle_path_mtu(ngx_connection_t *c,
+    ngx_quic_path_t *path, uint64_t min, uint64_t max);
+
+#endif /* _NGX_EVENT_QUIC_MIGRATION_H_INCLUDED_ */
diff --git a/src/event/quic/ngx_event_quic_openssl_compat.c b/src/event/quic/ngx_event_quic_openssl_compat.c
new file mode 100644
index 0000000..6052bc6
--- /dev/null
+++ b/src/event/quic/ngx_event_quic_openssl_compat.c
@@ -0,0 +1,652 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_event.h>
+#include <ngx_event_quic_connection.h>
+
+
+#if (NGX_QUIC_OPENSSL_COMPAT)
+
+#define NGX_QUIC_COMPAT_RECORD_SIZE          1024
+
+#define NGX_QUIC_COMPAT_SSL_TP_EXT           0x39
+
+#define NGX_QUIC_COMPAT_CLIENT_HANDSHAKE     "CLIENT_HANDSHAKE_TRAFFIC_SECRET"
+#define NGX_QUIC_COMPAT_SERVER_HANDSHAKE     "SERVER_HANDSHAKE_TRAFFIC_SECRET"
+#define NGX_QUIC_COMPAT_CLIENT_APPLICATION   "CLIENT_TRAFFIC_SECRET_0"
+#define NGX_QUIC_COMPAT_SERVER_APPLICATION   "SERVER_TRAFFIC_SECRET_0"
+
+
+typedef struct {
+    ngx_quic_secret_t             secret;
+    ngx_uint_t                    cipher;
+} ngx_quic_compat_keys_t;
+
+
+typedef struct {
+    ngx_log_t                    *log;
+
+    u_char                        type;
+    ngx_str_t                     payload;
+    uint64_t                      number;
+    ngx_quic_compat_keys_t       *keys;
+
+    enum ssl_encryption_level_t   level;
+} ngx_quic_compat_record_t;
+
+
+struct ngx_quic_compat_s {
+    const SSL_QUIC_METHOD        *method;
+
+    enum ssl_encryption_level_t   write_level;
+
+    uint64_t                      read_record;
+    ngx_quic_compat_keys_t        keys;
+
+    ngx_str_t                     tp;
+    ngx_str_t                     ctp;
+};
+
+
+static void ngx_quic_compat_keylog_callback(const SSL *ssl, const char *line);
+static ngx_int_t ngx_quic_compat_set_encryption_secret(ngx_connection_t *c,
+    ngx_quic_compat_keys_t *keys, enum ssl_encryption_level_t level,
+    const SSL_CIPHER *cipher, const uint8_t *secret, size_t secret_len);
+static void ngx_quic_compat_cleanup_encryption_secret(void *data);
+static int ngx_quic_compat_add_transport_params_callback(SSL *ssl,
+    unsigned int ext_type, unsigned int context, const unsigned char **out,
+    size_t *outlen, X509 *x, size_t chainidx, int *al, void *add_arg);
+static int ngx_quic_compat_parse_transport_params_callback(SSL *ssl,
+    unsigned int ext_type, unsigned int context, const unsigned char *in,
+    size_t inlen, X509 *x, size_t chainidx, int *al, void *parse_arg);
+static void ngx_quic_compat_message_callback(int write_p, int version,
+    int content_type, const void *buf, size_t len, SSL *ssl, void *arg);
+static size_t ngx_quic_compat_create_header(ngx_quic_compat_record_t *rec,
+    u_char *out, ngx_uint_t plain);
+static ngx_int_t ngx_quic_compat_create_record(ngx_quic_compat_record_t *rec,
+    ngx_str_t *res);
+
+
+ngx_int_t
+ngx_quic_compat_init(ngx_conf_t *cf, SSL_CTX *ctx)
+{
+    SSL_CTX_set_keylog_callback(ctx, ngx_quic_compat_keylog_callback);
+
+    if (SSL_CTX_has_client_custom_ext(ctx, NGX_QUIC_COMPAT_SSL_TP_EXT)) {
+        return NGX_OK;
+    }
+
+    if (SSL_CTX_add_custom_ext(ctx, NGX_QUIC_COMPAT_SSL_TP_EXT,
+                               SSL_EXT_CLIENT_HELLO
+                               |SSL_EXT_TLS1_3_ENCRYPTED_EXTENSIONS,
+                               ngx_quic_compat_add_transport_params_callback,
+                               NULL,
+                               NULL,
+                               ngx_quic_compat_parse_transport_params_callback,
+                               NULL)
+        == 0)
+    {
+        ngx_log_error(NGX_LOG_EMERG, cf->log, 0,
+                      "SSL_CTX_add_custom_ext() failed");
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}
+
+
+static void
+ngx_quic_compat_keylog_callback(const SSL *ssl, const char *line)
+{
+    u_char                        ch, *p, *start, value;
+    size_t                        n;
+    ngx_uint_t                    write;
+    const SSL_CIPHER             *cipher;
+    ngx_quic_compat_t            *com;
+    ngx_connection_t             *c;
+    ngx_quic_connection_t        *qc;
+    enum ssl_encryption_level_t   level;
+    u_char                        secret[EVP_MAX_MD_SIZE];
+
+    c = ngx_ssl_get_connection(ssl);
+    if (c->type != SOCK_DGRAM) {
+        return;
+    }
+
+    p = (u_char *) line;
+
+    for (start = p; *p && *p != ' '; p++);
+
+    n = p - start;
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic compat secret %*s", n, start);
+
+    if (n == sizeof(NGX_QUIC_COMPAT_CLIENT_HANDSHAKE) - 1
+        && ngx_strncmp(start, NGX_QUIC_COMPAT_CLIENT_HANDSHAKE, n) == 0)
+    {
+        level = ssl_encryption_handshake;
+        write = 0;
+
+    } else if (n == sizeof(NGX_QUIC_COMPAT_SERVER_HANDSHAKE) - 1
+               && ngx_strncmp(start, NGX_QUIC_COMPAT_SERVER_HANDSHAKE, n) == 0)
+    {
+        level = ssl_encryption_handshake;
+        write = 1;
+
+    } else if (n == sizeof(NGX_QUIC_COMPAT_CLIENT_APPLICATION) - 1
+               && ngx_strncmp(start, NGX_QUIC_COMPAT_CLIENT_APPLICATION, n)
+                  == 0)
+    {
+        level = ssl_encryption_application;
+        write = 0;
+
+    } else if (n == sizeof(NGX_QUIC_COMPAT_SERVER_APPLICATION) - 1
+               && ngx_strncmp(start, NGX_QUIC_COMPAT_SERVER_APPLICATION, n)
+                   == 0)
+    {
+        level = ssl_encryption_application;
+        write = 1;
+
+    } else {
+        return;
+    }
+
+    if (*p++ == '\0') {
+        return;
+    }
+
+    for ( /* void */ ; *p && *p != ' '; p++);
+
+    if (*p++ == '\0') {
+        return;
+    }
+
+    for (n = 0, start = p; *p; p++) {
+        ch = *p;
+
+        if (ch >= '0' && ch <= '9') {
+            value = ch - '0';
+            goto next;
+        }
+
+        ch = (u_char) (ch | 0x20);
+
+        if (ch >= 'a' && ch <= 'f') {
+            value = ch - 'a' + 10;
+            goto next;
+        }
+
+        ngx_log_error(NGX_LOG_EMERG, c->log, 0,
+                      "invalid OpenSSL QUIC secret format");
+
+        return;
+
+    next:
+
+        if ((p - start) % 2) {
+            secret[n++] += value;
+
+        } else {
+            if (n >= EVP_MAX_MD_SIZE) {
+                ngx_log_error(NGX_LOG_EMERG, c->log, 0,
+                              "too big OpenSSL QUIC secret");
+                return;
+            }
+
+            secret[n] = (value << 4);
+        }
+    }
+
+    qc = ngx_quic_get_connection(c);
+    com = qc->compat;
+    cipher = SSL_get_current_cipher(ssl);
+
+    if (write) {
+        com->method->set_write_secret((SSL *) ssl, level, cipher, secret, n);
+        com->write_level = level;
+
+    } else {
+        com->method->set_read_secret((SSL *) ssl, level, cipher, secret, n);
+        com->read_record = 0;
+
+        (void) ngx_quic_compat_set_encryption_secret(c, &com->keys, level,
+                                                     cipher, secret, n);
+    }
+
+    ngx_explicit_memzero(secret, n);
+}
+
+
+static ngx_int_t
+ngx_quic_compat_set_encryption_secret(ngx_connection_t *c,
+    ngx_quic_compat_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_str_t            secret_str;
+    ngx_uint_t           i;
+    ngx_quic_md_t        key;
+    ngx_quic_hkdf_t      seq[2];
+    ngx_quic_secret_t   *peer_secret;
+    ngx_quic_ciphers_t   ciphers;
+    ngx_pool_cleanup_t  *cln;
+
+    peer_secret = &keys->secret;
+
+    keys->cipher = SSL_CIPHER_get_id(cipher);
+
+    key_len = ngx_quic_ciphers(keys->cipher, &ciphers);
+
+    if (key_len == NGX_ERROR) {
+        ngx_ssl_error(NGX_LOG_INFO, c->log, 0, "unexpected cipher");
+        return NGX_ERROR;
+    }
+
+    key.len = key_len;
+
+    peer_secret->iv.len = NGX_QUIC_IV_LEN;
+
+    secret_str.len = secret_len;
+    secret_str.data = (u_char *) secret;
+
+    ngx_quic_hkdf_set(&seq[0], "tls13 key", &key, &secret_str);
+    ngx_quic_hkdf_set(&seq[1], "tls13 iv", &peer_secret->iv, &secret_str);
+
+    for (i = 0; i < (sizeof(seq) / sizeof(seq[0])); i++) {
+        if (ngx_quic_hkdf_expand(&seq[i], ciphers.d, c->log) != NGX_OK) {
+            return NGX_ERROR;
+        }
+    }
+
+    /* register cleanup handler once */
+
+    if (peer_secret->ctx) {
+        ngx_quic_crypto_cleanup(peer_secret);
+
+    } else {
+        cln = ngx_pool_cleanup_add(c->pool, 0);
+        if (cln == NULL) {
+            return NGX_ERROR;
+        }
+
+        cln->handler = ngx_quic_compat_cleanup_encryption_secret;
+        cln->data = peer_secret;
+    }
+
+    if (ngx_quic_crypto_init(ciphers.c, peer_secret, &key, 1, c->log)
+        == NGX_ERROR)
+    {
+        return NGX_ERROR;
+    }
+
+    ngx_explicit_memzero(key.data, key.len);
+
+    return NGX_OK;
+}
+
+
+static void
+ngx_quic_compat_cleanup_encryption_secret(void *data)
+{
+    ngx_quic_secret_t *secret = data;
+
+    ngx_quic_crypto_cleanup(secret);
+}
+
+
+static int
+ngx_quic_compat_add_transport_params_callback(SSL *ssl, unsigned int ext_type,
+    unsigned int context, const unsigned char **out, size_t *outlen, X509 *x,
+    size_t chainidx, int *al, void *add_arg)
+{
+    ngx_connection_t       *c;
+    ngx_quic_compat_t      *com;
+    ngx_quic_connection_t  *qc;
+
+    c = ngx_ssl_get_connection(ssl);
+    if (c->type != SOCK_DGRAM) {
+        return 0;
+    }
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic compat add transport params");
+
+    qc = ngx_quic_get_connection(c);
+    com = qc->compat;
+
+    *out = com->tp.data;
+    *outlen = com->tp.len;
+
+    return 1;
+}
+
+
+static int
+ngx_quic_compat_parse_transport_params_callback(SSL *ssl, unsigned int ext_type,
+    unsigned int context, const unsigned char *in, size_t inlen, X509 *x,
+    size_t chainidx, int *al, void *parse_arg)
+{
+    u_char                 *p;
+    ngx_connection_t       *c;
+    ngx_quic_compat_t      *com;
+    ngx_quic_connection_t  *qc;
+
+    c = ngx_ssl_get_connection(ssl);
+    if (c->type != SOCK_DGRAM) {
+        return 0;
+    }
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic compat parse transport params");
+
+    qc = ngx_quic_get_connection(c);
+    com = qc->compat;
+
+    p = ngx_pnalloc(c->pool, inlen);
+    if (p == NULL) {
+        return 0;
+    }
+
+    ngx_memcpy(p, in, inlen);
+
+    com->ctp.data = p;
+    com->ctp.len = inlen;
+
+    return 1;
+}
+
+
+int
+SSL_set_quic_method(SSL *ssl, const SSL_QUIC_METHOD *quic_method)
+{
+    BIO                    *rbio, *wbio;
+    ngx_connection_t       *c;
+    ngx_quic_compat_t      *com;
+    ngx_quic_connection_t  *qc;
+
+    c = ngx_ssl_get_connection(ssl);
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, "quic compat set method");
+
+    qc = ngx_quic_get_connection(c);
+
+    qc->compat = ngx_pcalloc(c->pool, sizeof(ngx_quic_compat_t));
+    if (qc->compat == NULL) {
+        return 0;
+    }
+
+    com = qc->compat;
+    com->method = quic_method;
+
+    rbio = BIO_new(BIO_s_mem());
+    if (rbio == NULL) {
+        return 0;
+    }
+
+    wbio = BIO_new(BIO_s_null());
+    if (wbio == NULL) {
+        BIO_free(rbio);
+        return 0;
+    }
+
+    SSL_set_bio(ssl, rbio, wbio);
+
+    SSL_set_msg_callback(ssl, ngx_quic_compat_message_callback);
+
+    /* early data is not supported */
+    SSL_set_max_early_data(ssl, 0);
+
+    return 1;
+}
+
+
+static void
+ngx_quic_compat_message_callback(int write_p, int version, int content_type,
+    const void *buf, size_t len, SSL *ssl, void *arg)
+{
+    ngx_uint_t                    alert;
+    ngx_connection_t             *c;
+    ngx_quic_compat_t            *com;
+    ngx_quic_connection_t        *qc;
+    enum ssl_encryption_level_t   level;
+
+    if (!write_p) {
+        return;
+    }
+
+    c = ngx_ssl_get_connection(ssl);
+    qc = ngx_quic_get_connection(c);
+
+    if (qc == NULL) {
+        /* closing */
+        return;
+    }
+
+    com = qc->compat;
+    level = com->write_level;
+
+    switch (content_type) {
+
+    case SSL3_RT_HANDSHAKE:
+        ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic compat tx %s len:%uz ",
+                       ngx_quic_level_name(level), len);
+
+        if (com->method->add_handshake_data(ssl, level, buf, len) != 1) {
+            goto failed;
+        }
+
+        break;
+
+    case SSL3_RT_ALERT:
+        if (len >= 2) {
+            alert = ((u_char *) buf)[1];
+
+            ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                           "quic compat %s alert:%ui len:%uz ",
+                           ngx_quic_level_name(level), alert, len);
+
+            if (com->method->send_alert(ssl, level, alert) != 1) {
+                goto failed;
+            }
+        }
+
+        break;
+    }
+
+    return;
+
+failed:
+
+    ngx_post_event(&qc->close, &ngx_posted_events);
+}
+
+
+int
+SSL_provide_quic_data(SSL *ssl, enum ssl_encryption_level_t level,
+    const uint8_t *data, size_t len)
+{
+    BIO                       *rbio;
+    size_t                     n;
+    u_char                    *p;
+    ngx_str_t                  res;
+    ngx_connection_t          *c;
+    ngx_quic_compat_t         *com;
+    ngx_quic_connection_t     *qc;
+    ngx_quic_compat_record_t   rec;
+    u_char                     in[NGX_QUIC_COMPAT_RECORD_SIZE + 1];
+    u_char                     out[NGX_QUIC_COMPAT_RECORD_SIZE + 1
+                                   + SSL3_RT_HEADER_LENGTH
+                                   + NGX_QUIC_TAG_LEN];
+
+    c = ngx_ssl_get_connection(ssl);
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0, "quic compat rx %s len:%uz",
+                   ngx_quic_level_name(level), len);
+
+    qc = ngx_quic_get_connection(c);
+    com = qc->compat;
+    rbio = SSL_get_rbio(ssl);
+
+    while (len) {
+        ngx_memzero(&rec, sizeof(ngx_quic_compat_record_t));
+
+        rec.type = SSL3_RT_HANDSHAKE;
+        rec.log = c->log;
+        rec.number = com->read_record++;
+        rec.keys = &com->keys;
+        rec.level = level;
+
+        if (level == ssl_encryption_initial) {
+            n = ngx_min(len, 65535);
+
+            rec.payload.len = n;
+            rec.payload.data = (u_char *) data;
+
+            ngx_quic_compat_create_header(&rec, out, 1);
+
+            BIO_write(rbio, out, SSL3_RT_HEADER_LENGTH);
+            BIO_write(rbio, data, n);
+
+#if defined(NGX_QUIC_DEBUG_CRYPTO) && defined(NGX_QUIC_DEBUG_PACKETS)
+            ngx_log_debug5(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                           "quic compat record len:%uz %*xs%*xs",
+                           n + SSL3_RT_HEADER_LENGTH,
+                           (size_t) SSL3_RT_HEADER_LENGTH, out, n, data);
+#endif
+
+        } else {
+            n = ngx_min(len, NGX_QUIC_COMPAT_RECORD_SIZE);
+
+            p = ngx_cpymem(in, data, n);
+            *p++ = SSL3_RT_HANDSHAKE;
+
+            rec.payload.len = p - in;
+            rec.payload.data = in;
+
+            res.data = out;
+
+            if (ngx_quic_compat_create_record(&rec, &res) != NGX_OK) {
+                return 0;
+            }
+
+#if defined(NGX_QUIC_DEBUG_CRYPTO) && defined(NGX_QUIC_DEBUG_PACKETS)
+            ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                           "quic compat record len:%uz %xV", res.len, &res);
+#endif
+
+            BIO_write(rbio, res.data, res.len);
+        }
+
+        data += n;
+        len -= n;
+    }
+
+    return 1;
+}
+
+
+static size_t
+ngx_quic_compat_create_header(ngx_quic_compat_record_t *rec, u_char *out,
+    ngx_uint_t plain)
+{
+    u_char  type;
+    size_t  len;
+
+    len = rec->payload.len;
+
+    if (plain) {
+        type = rec->type;
+
+    } else {
+        type = SSL3_RT_APPLICATION_DATA;
+        len += NGX_QUIC_TAG_LEN;
+    }
+
+    out[0] = type;
+    out[1] = 0x03;
+    out[2] = 0x03;
+    out[3] = (len >> 8);
+    out[4] = len;
+
+    return 5;
+}
+
+
+static ngx_int_t
+ngx_quic_compat_create_record(ngx_quic_compat_record_t *rec, ngx_str_t *res)
+{
+    ngx_str_t           ad, out;
+    ngx_quic_secret_t  *secret;
+    u_char              nonce[NGX_QUIC_IV_LEN];
+
+    ad.data = res->data;
+    ad.len = ngx_quic_compat_create_header(rec, ad.data, 0);
+
+    out.len = rec->payload.len + NGX_QUIC_TAG_LEN;
+    out.data = res->data + ad.len;
+
+#ifdef NGX_QUIC_DEBUG_CRYPTO
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, rec->log, 0,
+                   "quic compat ad len:%uz %xV", ad.len, &ad);
+#endif
+
+    secret = &rec->keys->secret;
+
+    ngx_memcpy(nonce, secret->iv.data, secret->iv.len);
+    ngx_quic_compute_nonce(nonce, sizeof(nonce), rec->number);
+
+    if (ngx_quic_crypto_seal(secret, &out, nonce, &rec->payload, &ad, rec->log)
+        != NGX_OK)
+    {
+        return NGX_ERROR;
+    }
+
+    res->len = ad.len + out.len;
+
+    return NGX_OK;
+}
+
+
+int
+SSL_set_quic_transport_params(SSL *ssl, const uint8_t *params,
+    size_t params_len)
+{
+    ngx_connection_t       *c;
+    ngx_quic_compat_t      *com;
+    ngx_quic_connection_t  *qc;
+
+    c = ngx_ssl_get_connection(ssl);
+    qc = ngx_quic_get_connection(c);
+    com = qc->compat;
+
+    com->tp.len = params_len;
+    com->tp.data = (u_char *) params;
+
+    return 1;
+}
+
+
+void
+SSL_get_peer_quic_transport_params(const SSL *ssl, const uint8_t **out_params,
+    size_t *out_params_len)
+{
+    ngx_connection_t       *c;
+    ngx_quic_compat_t      *com;
+    ngx_quic_connection_t  *qc;
+
+    c = ngx_ssl_get_connection(ssl);
+    qc = ngx_quic_get_connection(c);
+    com = qc->compat;
+
+    *out_params = com->ctp.data;
+    *out_params_len = com->ctp.len;
+}
+
+#endif /* NGX_QUIC_OPENSSL_COMPAT */
diff --git a/src/event/quic/ngx_event_quic_openssl_compat.h b/src/event/quic/ngx_event_quic_openssl_compat.h
new file mode 100644
index 0000000..77cc3cb
--- /dev/null
+++ b/src/event/quic/ngx_event_quic_openssl_compat.h
@@ -0,0 +1,59 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_EVENT_QUIC_OPENSSL_COMPAT_H_INCLUDED_
+#define _NGX_EVENT_QUIC_OPENSSL_COMPAT_H_INCLUDED_
+
+#if defined SSL_R_MISSING_QUIC_TRANSPORT_PARAMETERS_EXTENSION                 \
+    || defined LIBRESSL_VERSION_NUMBER
+#undef NGX_QUIC_OPENSSL_COMPAT
+#else
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+
+typedef struct ngx_quic_compat_s  ngx_quic_compat_t;
+
+
+enum ssl_encryption_level_t {
+    ssl_encryption_initial = 0,
+    ssl_encryption_early_data,
+    ssl_encryption_handshake,
+    ssl_encryption_application
+};
+
+
+typedef struct ssl_quic_method_st {
+    int (*set_read_secret)(SSL *ssl, enum ssl_encryption_level_t level,
+                           const SSL_CIPHER *cipher,
+                           const uint8_t *rsecret, size_t secret_len);
+    int (*set_write_secret)(SSL *ssl, enum ssl_encryption_level_t level,
+                            const SSL_CIPHER *cipher,
+                            const uint8_t *wsecret, size_t secret_len);
+    int (*add_handshake_data)(SSL *ssl, enum ssl_encryption_level_t level,
+                              const uint8_t *data, size_t len);
+    int (*flush_flight)(SSL *ssl);
+    int (*send_alert)(SSL *ssl, enum ssl_encryption_level_t level,
+                      uint8_t alert);
+} SSL_QUIC_METHOD;
+
+
+ngx_int_t ngx_quic_compat_init(ngx_conf_t *cf, SSL_CTX *ctx);
+
+int SSL_set_quic_method(SSL *ssl, const SSL_QUIC_METHOD *quic_method);
+int SSL_provide_quic_data(SSL *ssl, enum ssl_encryption_level_t level,
+    const uint8_t *data, size_t len);
+int SSL_set_quic_transport_params(SSL *ssl, const uint8_t *params,
+    size_t params_len);
+void SSL_get_peer_quic_transport_params(const SSL *ssl,
+    const uint8_t **out_params, size_t *out_params_len);
+
+
+#endif /* TLSEXT_TYPE_quic_transport_parameters */
+
+#endif /* _NGX_EVENT_QUIC_OPENSSL_COMPAT_H_INCLUDED_ */
diff --git a/src/event/quic/ngx_event_quic_output.c b/src/event/quic/ngx_event_quic_output.c
new file mode 100644
index 0000000..f087e2b
--- /dev/null
+++ b/src/event/quic/ngx_event_quic_output.c
@@ -0,0 +1,1319 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_event.h>
+#include <ngx_event_quic_connection.h>
+
+
+#define NGX_QUIC_MAX_UDP_SEGMENT_BUF  65487 /* 65K - IPv6 header */
+#define NGX_QUIC_MAX_SEGMENTS            64 /* UDP_MAX_SEGMENTS */
+
+#define NGX_QUIC_RETRY_TOKEN_LIFETIME     3 /* seconds */
+#define NGX_QUIC_NEW_TOKEN_LIFETIME     600 /* seconds */
+#define NGX_QUIC_RETRY_BUFFER_SIZE      256
+    /* 1 flags + 4 version + 3 x (1 + 20) s/o/dcid + itag + token(64) */
+
+/*
+ * RFC 9000, 10.3.  Stateless Reset
+ *
+ * Endpoints MUST discard packets that are too small to be valid QUIC
+ * packets.  With the set of AEAD functions defined in [QUIC-TLS],
+ * short header packets that are smaller than 21 bytes are never valid.
+ */
+#define NGX_QUIC_MIN_PKT_LEN             21
+
+#define NGX_QUIC_MIN_SR_PACKET           43 /* 5 rand + 16 srt + 22 padding */
+#define NGX_QUIC_MAX_SR_PACKET         1200
+
+#define NGX_QUIC_CC_MIN_INTERVAL       1000 /* 1s */
+
+#define NGX_QUIC_SOCKET_RETRY_DELAY      10 /* ms, for NGX_AGAIN on write */
+
+
+#define ngx_quic_log_packet(log, pkt)                                         \
+    ngx_log_debug6(NGX_LOG_DEBUG_EVENT, log, 0,                               \
+                   "quic packet tx %s bytes:%ui need_ack:%d"                  \
+                   " number:%L encoded nl:%d trunc:0x%xD",                    \
+                   ngx_quic_level_name((pkt)->level), (pkt)->payload.len,     \
+                   (pkt)->need_ack, (pkt)->number, (pkt)->num_len,            \
+                    (pkt)->trunc);
+
+
+static ngx_int_t ngx_quic_create_datagrams(ngx_connection_t *c);
+static void ngx_quic_commit_send(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx);
+static void ngx_quic_revert_send(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx,
+    uint64_t pnum);
+#if ((NGX_HAVE_UDP_SEGMENT) && (NGX_HAVE_MSGHDR_MSG_CONTROL))
+static ngx_uint_t ngx_quic_allow_segmentation(ngx_connection_t *c);
+static ngx_int_t ngx_quic_create_segments(ngx_connection_t *c);
+static ssize_t ngx_quic_send_segments(ngx_connection_t *c, u_char *buf,
+    size_t len, struct sockaddr *sockaddr, socklen_t socklen, size_t segment);
+#endif
+static ssize_t ngx_quic_output_packet(ngx_connection_t *c,
+    ngx_quic_send_ctx_t *ctx, u_char *data, size_t max, size_t min);
+static void ngx_quic_init_packet(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx,
+    ngx_quic_header_t *pkt, ngx_quic_path_t *path);
+static ngx_uint_t ngx_quic_get_padding_level(ngx_connection_t *c);
+static ssize_t ngx_quic_send(ngx_connection_t *c, u_char *buf, size_t len,
+    struct sockaddr *sockaddr, socklen_t socklen);
+static void ngx_quic_set_packet_number(ngx_quic_header_t *pkt,
+    ngx_quic_send_ctx_t *ctx);
+
+
+ngx_int_t
+ngx_quic_output(ngx_connection_t *c)
+{
+    size_t                  in_flight;
+    ngx_int_t               rc;
+    ngx_quic_congestion_t  *cg;
+    ngx_quic_connection_t  *qc;
+
+    c->log->action = "sending frames";
+
+    qc = ngx_quic_get_connection(c);
+    cg = &qc->congestion;
+
+    in_flight = cg->in_flight;
+
+#if ((NGX_HAVE_UDP_SEGMENT) && (NGX_HAVE_MSGHDR_MSG_CONTROL))
+    if (ngx_quic_allow_segmentation(c)) {
+        rc = ngx_quic_create_segments(c);
+    } else
+#endif
+    {
+        rc = ngx_quic_create_datagrams(c);
+    }
+
+    if (rc != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    if (in_flight == cg->in_flight || qc->closing) {
+        /* no ack-eliciting data was sent or we are done */
+        return NGX_OK;
+    }
+
+    if (!qc->send_timer_set) {
+        qc->send_timer_set = 1;
+        ngx_add_timer(c->read, qc->tp.max_idle_timeout);
+    }
+
+    ngx_quic_set_lost_timer(c);
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_create_datagrams(ngx_connection_t *c)
+{
+    size_t                  len, min;
+    ssize_t                 n;
+    u_char                 *p;
+    uint64_t                preserved_pnum[NGX_QUIC_SEND_CTX_LAST];
+    ngx_uint_t              i, pad;
+    ngx_quic_path_t        *path;
+    ngx_quic_send_ctx_t    *ctx;
+    ngx_quic_congestion_t  *cg;
+    ngx_quic_connection_t  *qc;
+    static u_char           dst[NGX_QUIC_MAX_UDP_PAYLOAD_SIZE];
+
+    qc = ngx_quic_get_connection(c);
+    cg = &qc->congestion;
+    path = qc->path;
+
+    while (cg->in_flight < cg->window) {
+
+        p = dst;
+
+        len = ngx_quic_path_limit(c, path, path->mtu);
+
+        pad = ngx_quic_get_padding_level(c);
+
+        for (i = 0; i < NGX_QUIC_SEND_CTX_LAST; i++) {
+
+            ctx = &qc->send_ctx[i];
+
+            preserved_pnum[i] = ctx->pnum;
+
+            if (ngx_quic_generate_ack(c, ctx) != NGX_OK) {
+                return NGX_ERROR;
+            }
+
+            min = (i == pad && p - dst < NGX_QUIC_MIN_INITIAL_SIZE)
+                  ? NGX_QUIC_MIN_INITIAL_SIZE - (p - dst) : 0;
+
+            if (min > len) {
+                /* padding can't be applied - avoid sending the packet */
+
+                while (i-- > 0) {
+                    ctx = &qc->send_ctx[i];
+                    ngx_quic_revert_send(c, ctx, preserved_pnum[i]);
+                }
+
+                return NGX_OK;
+            }
+
+            n = ngx_quic_output_packet(c, ctx, p, len, min);
+            if (n == NGX_ERROR) {
+                return NGX_ERROR;
+            }
+
+            p += n;
+            len -= n;
+        }
+
+        len = p - dst;
+        if (len == 0) {
+            break;
+        }
+
+        n = ngx_quic_send(c, dst, len, path->sockaddr, path->socklen);
+
+        if (n == NGX_ERROR) {
+            return NGX_ERROR;
+        }
+
+        if (n == NGX_AGAIN) {
+            for (i = 0; i < NGX_QUIC_SEND_CTX_LAST; i++) {
+                ngx_quic_revert_send(c, &qc->send_ctx[i], preserved_pnum[i]);
+            }
+
+            ngx_add_timer(&qc->push, NGX_QUIC_SOCKET_RETRY_DELAY);
+            break;
+        }
+
+        for (i = 0; i < NGX_QUIC_SEND_CTX_LAST; i++) {
+            ngx_quic_commit_send(c, &qc->send_ctx[i]);
+        }
+
+        path->sent += len;
+    }
+
+    return NGX_OK;
+}
+
+
+static void
+ngx_quic_commit_send(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx)
+{
+    ngx_queue_t            *q;
+    ngx_quic_frame_t       *f;
+    ngx_quic_congestion_t  *cg;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    cg = &qc->congestion;
+
+    while (!ngx_queue_empty(&ctx->sending)) {
+
+        q = ngx_queue_head(&ctx->sending);
+        f = ngx_queue_data(q, ngx_quic_frame_t, queue);
+
+        ngx_queue_remove(q);
+
+        if (f->pkt_need_ack && !qc->closing) {
+            ngx_queue_insert_tail(&ctx->sent, q);
+
+            cg->in_flight += f->plen;
+
+        } else {
+            ngx_quic_free_frame(c, f);
+        }
+    }
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic congestion send if:%uz", cg->in_flight);
+}
+
+
+static void
+ngx_quic_revert_send(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx,
+    uint64_t pnum)
+{
+    ngx_queue_t  *q;
+
+    while (!ngx_queue_empty(&ctx->sending)) {
+
+        q = ngx_queue_last(&ctx->sending);
+        ngx_queue_remove(q);
+        ngx_queue_insert_head(&ctx->frames, q);
+    }
+
+    ctx->pnum = pnum;
+}
+
+
+#if ((NGX_HAVE_UDP_SEGMENT) && (NGX_HAVE_MSGHDR_MSG_CONTROL))
+
+static ngx_uint_t
+ngx_quic_allow_segmentation(ngx_connection_t *c)
+{
+    size_t                  bytes, len;
+    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 (!qc->conf->gso_enabled) {
+        return 0;
+    }
+
+    if (!qc->path->validated) {
+        /* don't even try to be faster on non-validated paths */
+        return 0;
+    }
+
+    ctx = ngx_quic_get_send_ctx(qc, ssl_encryption_initial);
+    if (!ngx_queue_empty(&ctx->frames)) {
+        return 0;
+    }
+
+    ctx = ngx_quic_get_send_ctx(qc, ssl_encryption_handshake);
+    if (!ngx_queue_empty(&ctx->frames)) {
+        return 0;
+    }
+
+    ctx = ngx_quic_get_send_ctx(qc, ssl_encryption_application);
+
+    bytes = 0;
+    len = ngx_min(qc->path->mtu, NGX_QUIC_MAX_UDP_SEGMENT_BUF);
+
+    for (q = ngx_queue_head(&ctx->frames);
+         q != ngx_queue_sentinel(&ctx->frames);
+         q = ngx_queue_next(q))
+    {
+        f = ngx_queue_data(q, ngx_quic_frame_t, queue);
+
+        bytes += f->len;
+
+        if (bytes > len * 3) {
+            /* require at least ~3 full packets to batch */
+            return 1;
+        }
+    }
+
+    return 0;
+}
+
+
+static ngx_int_t
+ngx_quic_create_segments(ngx_connection_t *c)
+{
+    size_t                  len, segsize;
+    ssize_t                 n;
+    u_char                 *p, *end;
+    uint64_t                preserved_pnum;
+    ngx_uint_t              nseg;
+    ngx_quic_path_t        *path;
+    ngx_quic_send_ctx_t    *ctx;
+    ngx_quic_congestion_t  *cg;
+    ngx_quic_connection_t  *qc;
+    static u_char           dst[NGX_QUIC_MAX_UDP_SEGMENT_BUF];
+
+    qc = ngx_quic_get_connection(c);
+    cg = &qc->congestion;
+    path = qc->path;
+
+    ctx = ngx_quic_get_send_ctx(qc, ssl_encryption_application);
+
+    if (ngx_quic_generate_ack(c, ctx) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    segsize = ngx_min(path->mtu, NGX_QUIC_MAX_UDP_SEGMENT_BUF);
+    p = dst;
+    end = dst + sizeof(dst);
+
+    nseg = 0;
+
+    preserved_pnum = ctx->pnum;
+
+    for ( ;; ) {
+
+        len = ngx_min(segsize, (size_t) (end - p));
+
+        if (len && cg->in_flight + (p - dst) < cg->window) {
+
+            n = ngx_quic_output_packet(c, ctx, p, len, len);
+            if (n == NGX_ERROR) {
+                return NGX_ERROR;
+            }
+
+            if (n) {
+                p += n;
+                nseg++;
+            }
+
+        } else {
+            n = 0;
+        }
+
+        if (p == dst) {
+            break;
+        }
+
+        if (n == 0 || nseg == NGX_QUIC_MAX_SEGMENTS) {
+            n = ngx_quic_send_segments(c, dst, p - dst, path->sockaddr,
+                                       path->socklen, segsize);
+            if (n == NGX_ERROR) {
+                return NGX_ERROR;
+            }
+
+            if (n == NGX_AGAIN) {
+                ngx_quic_revert_send(c, ctx, preserved_pnum);
+
+                ngx_add_timer(&qc->push, NGX_QUIC_SOCKET_RETRY_DELAY);
+                break;
+            }
+
+            ngx_quic_commit_send(c, ctx);
+
+            path->sent += n;
+
+            p = dst;
+            nseg = 0;
+            preserved_pnum = ctx->pnum;
+        }
+    }
+
+    return NGX_OK;
+}
+
+
+static ssize_t
+ngx_quic_send_segments(ngx_connection_t *c, u_char *buf, size_t len,
+    struct sockaddr *sockaddr, socklen_t socklen, size_t segment)
+{
+    size_t           clen;
+    ssize_t          n;
+    uint16_t        *valp;
+    struct iovec     iov;
+    struct msghdr    msg;
+    struct cmsghdr  *cmsg;
+
+#if (NGX_HAVE_ADDRINFO_CMSG)
+    char             msg_control[CMSG_SPACE(sizeof(uint16_t))
+                             + CMSG_SPACE(sizeof(ngx_addrinfo_t))];
+#else
+    char             msg_control[CMSG_SPACE(sizeof(uint16_t))];
+#endif
+
+    ngx_memzero(&msg, sizeof(struct msghdr));
+    ngx_memzero(msg_control, sizeof(msg_control));
+
+    iov.iov_len = len;
+    iov.iov_base = (void *) buf;
+
+    msg.msg_iov = &iov;
+    msg.msg_iovlen = 1;
+
+    msg.msg_name = sockaddr;
+    msg.msg_namelen = socklen;
+
+    msg.msg_control = msg_control;
+    msg.msg_controllen = sizeof(msg_control);
+
+    cmsg = CMSG_FIRSTHDR(&msg);
+
+    cmsg->cmsg_level = SOL_UDP;
+    cmsg->cmsg_type = UDP_SEGMENT;
+    cmsg->cmsg_len = CMSG_LEN(sizeof(uint16_t));
+
+    clen = CMSG_SPACE(sizeof(uint16_t));
+
+    valp = (void *) CMSG_DATA(cmsg);
+    *valp = segment;
+
+#if (NGX_HAVE_ADDRINFO_CMSG)
+    if (c->listening && c->listening->wildcard && c->local_sockaddr) {
+        cmsg = CMSG_NXTHDR(&msg, cmsg);
+        clen += ngx_set_srcaddr_cmsg(cmsg, c->local_sockaddr);
+    }
+#endif
+
+    msg.msg_controllen = clen;
+
+    n = ngx_sendmsg(c, &msg, 0);
+    if (n < 0) {
+        return n;
+    }
+
+    c->sent += n;
+
+    return n;
+}
+
+#endif
+
+
+
+static ngx_uint_t
+ngx_quic_get_padding_level(ngx_connection_t *c)
+{
+    ngx_uint_t              i;
+    ngx_queue_t            *q;
+    ngx_quic_frame_t       *f;
+    ngx_quic_send_ctx_t    *ctx;
+    ngx_quic_connection_t  *qc;
+
+    /*
+     * RFC 9000, 14.1.  Initial Datagram Size
+     *
+     * Similarly, a server MUST expand the payload of all UDP datagrams
+     * carrying ack-eliciting Initial packets to at least the smallest
+     * allowed maximum datagram size of 1200 bytes.
+     */
+
+    qc = ngx_quic_get_connection(c);
+    ctx = ngx_quic_get_send_ctx(qc, ssl_encryption_initial);
+
+    for (q = ngx_queue_head(&ctx->frames);
+         q != ngx_queue_sentinel(&ctx->frames);
+         q = ngx_queue_next(q))
+    {
+        f = ngx_queue_data(q, ngx_quic_frame_t, queue);
+
+        if (f->need_ack) {
+            for (i = 0; i + 1 < NGX_QUIC_SEND_CTX_LAST; i++) {
+                ctx = &qc->send_ctx[i + 1];
+
+                if (ngx_queue_empty(&ctx->frames)) {
+                    break;
+                }
+            }
+
+            return i;
+        }
+    }
+
+    return NGX_QUIC_SEND_CTX_LAST;
+}
+
+
+static ssize_t
+ngx_quic_output_packet(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx,
+    u_char *data, size_t max, size_t min)
+{
+    size_t                  len, pad, min_payload, max_payload;
+    u_char                 *p;
+    ssize_t                 flen;
+    ngx_str_t               res;
+    ngx_int_t               rc;
+    ngx_uint_t              nframes;
+    ngx_msec_t              now;
+    ngx_queue_t            *q;
+    ngx_quic_frame_t       *f;
+    ngx_quic_header_t       pkt;
+    ngx_quic_connection_t  *qc;
+    static u_char           src[NGX_QUIC_MAX_UDP_PAYLOAD_SIZE];
+
+    if (ngx_queue_empty(&ctx->frames)) {
+        return 0;
+    }
+
+    ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic output %s packet max:%uz min:%uz",
+                   ngx_quic_level_name(ctx->level), max, min);
+
+    qc = ngx_quic_get_connection(c);
+
+    if (!ngx_quic_keys_available(qc->keys, ctx->level, 1)) {
+        ngx_log_error(NGX_LOG_ALERT, c->log, 0, "quic %s write keys discarded",
+                      ngx_quic_level_name(ctx->level));
+
+        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_free_frame(c, f);
+        }
+
+        return 0;
+    }
+
+    ngx_quic_init_packet(c, ctx, &pkt, qc->path);
+
+    min_payload = ngx_quic_payload_size(&pkt, min);
+    max_payload = ngx_quic_payload_size(&pkt, max);
+
+    /* RFC 9001, 5.4.2.  Header Protection Sample */
+    pad = 4 - pkt.num_len;
+    min_payload = ngx_max(min_payload, pad);
+
+    if (min_payload > max_payload) {
+        return 0;
+    }
+
+    now = ngx_current_msec;
+    nframes = 0;
+    p = src;
+    len = 0;
+
+    for (q = ngx_queue_head(&ctx->frames);
+         q != ngx_queue_sentinel(&ctx->frames);
+         q = ngx_queue_next(q))
+    {
+        f = ngx_queue_data(q, ngx_quic_frame_t, queue);
+
+        if (len >= max_payload) {
+            break;
+        }
+
+        if (len + f->len > max_payload) {
+            rc = ngx_quic_split_frame(c, f, max_payload - len);
+
+            if (rc == NGX_ERROR) {
+                return NGX_ERROR;
+            }
+
+            if (rc == NGX_DECLINED) {
+                break;
+            }
+        }
+
+        if (f->need_ack) {
+            pkt.need_ack = 1;
+        }
+
+        f->pnum = ctx->pnum;
+        f->send_time = now;
+        f->plen = 0;
+
+        ngx_quic_log_frame(c->log, f, 1);
+
+        flen = ngx_quic_create_frame(p, f);
+        if (flen == -1) {
+            return NGX_ERROR;
+        }
+
+        len += flen;
+        p += flen;
+
+        nframes++;
+    }
+
+    if (nframes == 0) {
+        return 0;
+    }
+
+    if (len < min_payload) {
+        ngx_memset(p, NGX_QUIC_FT_PADDING, min_payload - len);
+        len = min_payload;
+    }
+
+    pkt.payload.data = src;
+    pkt.payload.len = len;
+
+    res.data = data;
+
+    ngx_quic_log_packet(c->log, &pkt);
+
+    if (ngx_quic_encrypt(&pkt, &res) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    ctx->pnum++;
+
+    if (pkt.need_ack) {
+        q = ngx_queue_head(&ctx->frames);
+        f = ngx_queue_data(q, ngx_quic_frame_t, queue);
+
+        f->plen = res.len;
+    }
+
+    while (nframes--) {
+        q = ngx_queue_head(&ctx->frames);
+        f = ngx_queue_data(q, ngx_quic_frame_t, queue);
+
+        f->pkt_need_ack = pkt.need_ack;
+
+        ngx_queue_remove(q);
+        ngx_queue_insert_tail(&ctx->sending, q);
+    }
+
+    return res.len;
+}
+
+
+static void
+ngx_quic_init_packet(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx,
+    ngx_quic_header_t *pkt, ngx_quic_path_t *path)
+{
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    ngx_memzero(pkt, sizeof(ngx_quic_header_t));
+
+    pkt->flags = NGX_QUIC_PKT_FIXED_BIT;
+
+    if (ctx->level == ssl_encryption_initial) {
+        pkt->flags |= NGX_QUIC_PKT_LONG | NGX_QUIC_PKT_INITIAL;
+
+    } else if (ctx->level == ssl_encryption_handshake) {
+        pkt->flags |= NGX_QUIC_PKT_LONG | NGX_QUIC_PKT_HANDSHAKE;
+
+    } else {
+        if (qc->key_phase) {
+            pkt->flags |= NGX_QUIC_PKT_KPHASE;
+        }
+    }
+
+    pkt->dcid.data = path->cid->id;
+    pkt->dcid.len = path->cid->len;
+
+    pkt->scid = qc->tp.initial_scid;
+
+    pkt->version = qc->version;
+    pkt->log = c->log;
+    pkt->level = ctx->level;
+
+    pkt->keys = qc->keys;
+
+    ngx_quic_set_packet_number(pkt, ctx);
+}
+
+
+static ssize_t
+ngx_quic_send(ngx_connection_t *c, u_char *buf, size_t len,
+    struct sockaddr *sockaddr, socklen_t socklen)
+{
+    ssize_t          n;
+    struct iovec     iov;
+    struct msghdr    msg;
+#if (NGX_HAVE_ADDRINFO_CMSG)
+    struct cmsghdr  *cmsg;
+    char             msg_control[CMSG_SPACE(sizeof(ngx_addrinfo_t))];
+#endif
+
+    ngx_memzero(&msg, sizeof(struct msghdr));
+
+    iov.iov_len = len;
+    iov.iov_base = (void *) buf;
+
+    msg.msg_iov = &iov;
+    msg.msg_iovlen = 1;
+
+    msg.msg_name = sockaddr;
+    msg.msg_namelen = socklen;
+
+#if (NGX_HAVE_ADDRINFO_CMSG)
+    if (c->listening && c->listening->wildcard && c->local_sockaddr) {
+
+        msg.msg_control = msg_control;
+        msg.msg_controllen = sizeof(msg_control);
+        ngx_memzero(msg_control, sizeof(msg_control));
+
+        cmsg = CMSG_FIRSTHDR(&msg);
+
+        msg.msg_controllen = ngx_set_srcaddr_cmsg(cmsg, c->local_sockaddr);
+    }
+#endif
+
+    n = ngx_sendmsg(c, &msg, 0);
+    if (n < 0) {
+        return n;
+    }
+
+    c->sent += n;
+
+    return n;
+}
+
+
+static void
+ngx_quic_set_packet_number(ngx_quic_header_t *pkt, ngx_quic_send_ctx_t *ctx)
+{
+    uint64_t  delta;
+
+    delta = ctx->pnum - ctx->largest_ack;
+    pkt->number = ctx->pnum;
+
+    if (delta <= 0x7F) {
+        pkt->num_len = 1;
+        pkt->trunc = ctx->pnum & 0xff;
+
+    } else if (delta <= 0x7FFF) {
+        pkt->num_len = 2;
+        pkt->flags |= 0x1;
+        pkt->trunc = ctx->pnum & 0xffff;
+
+    } else if (delta <= 0x7FFFFF) {
+        pkt->num_len = 3;
+        pkt->flags |= 0x2;
+        pkt->trunc = ctx->pnum & 0xffffff;
+
+    } else {
+        pkt->num_len = 4;
+        pkt->flags |= 0x3;
+        pkt->trunc = ctx->pnum & 0xffffffff;
+    }
+}
+
+
+ngx_int_t
+ngx_quic_negotiate_version(ngx_connection_t *c, ngx_quic_header_t *inpkt)
+{
+    size_t             len;
+    ngx_quic_header_t  pkt;
+    static u_char      buf[NGX_QUIC_MAX_UDP_PAYLOAD_SIZE];
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "sending version negotiation packet");
+
+    pkt.log = c->log;
+    pkt.flags = NGX_QUIC_PKT_LONG | NGX_QUIC_PKT_FIXED_BIT;
+    pkt.dcid = inpkt->scid;
+    pkt.scid = inpkt->dcid;
+
+    len = ngx_quic_create_version_negotiation(&pkt, buf);
+
+#ifdef NGX_QUIC_DEBUG_PACKETS
+    ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic vnego packet to send len:%uz %*xs", len, len, buf);
+#endif
+
+    (void) ngx_quic_send(c, buf, len, c->sockaddr, c->socklen);
+
+    return NGX_DONE;
+}
+
+
+ngx_int_t
+ngx_quic_send_stateless_reset(ngx_connection_t *c, ngx_quic_conf_t *conf,
+    ngx_quic_header_t *pkt)
+{
+    u_char    *token;
+    size_t     len, max;
+    uint16_t   rndbytes;
+    u_char     buf[NGX_QUIC_MAX_SR_PACKET];
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic handle stateless reset output");
+
+    if (pkt->len <= NGX_QUIC_MIN_PKT_LEN) {
+        return NGX_DECLINED;
+    }
+
+    if (pkt->len <= NGX_QUIC_MIN_SR_PACKET) {
+        len = pkt->len - 1;
+
+    } else {
+        max = ngx_min(NGX_QUIC_MAX_SR_PACKET, pkt->len * 3);
+
+        if (RAND_bytes((u_char *) &rndbytes, sizeof(rndbytes)) != 1) {
+            return NGX_ERROR;
+        }
+
+        len = (rndbytes % (max - NGX_QUIC_MIN_SR_PACKET + 1))
+              + NGX_QUIC_MIN_SR_PACKET;
+    }
+
+    if (RAND_bytes(buf, len - NGX_QUIC_SR_TOKEN_LEN) != 1) {
+        return NGX_ERROR;
+    }
+
+    buf[0] &= ~NGX_QUIC_PKT_LONG;
+    buf[0] |= NGX_QUIC_PKT_FIXED_BIT;
+
+    token = &buf[len - NGX_QUIC_SR_TOKEN_LEN];
+
+    if (ngx_quic_new_sr_token(c, &pkt->dcid, conf->sr_token_key, token)
+        != NGX_OK)
+    {
+        return NGX_ERROR;
+    }
+
+    (void) ngx_quic_send(c, buf, len, c->sockaddr, c->socklen);
+
+    return NGX_DECLINED;
+}
+
+
+ngx_int_t
+ngx_quic_send_cc(ngx_connection_t *c)
+{
+    ngx_quic_frame_t       *frame;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    if (qc->draining) {
+        return NGX_OK;
+    }
+
+    if (qc->closing
+        && ngx_current_msec - qc->last_cc < NGX_QUIC_CC_MIN_INTERVAL)
+    {
+        /* dot not send CC too often */
+        return NGX_OK;
+    }
+
+    frame = ngx_quic_alloc_frame(c);
+    if (frame == NULL) {
+        return NGX_ERROR;
+    }
+
+    frame->level = qc->error_level;
+    frame->type = qc->error_app ? NGX_QUIC_FT_CONNECTION_CLOSE_APP
+                                : NGX_QUIC_FT_CONNECTION_CLOSE;
+    frame->u.close.error_code = qc->error;
+    frame->u.close.frame_type = qc->error_ftype;
+
+    if (qc->error_reason) {
+        frame->u.close.reason.len = ngx_strlen(qc->error_reason);
+        frame->u.close.reason.data = (u_char *) qc->error_reason;
+    }
+
+    frame->ignore_congestion = 1;
+
+    qc->last_cc = ngx_current_msec;
+
+    return ngx_quic_frame_sendto(c, frame, 0, qc->path);
+}
+
+
+ngx_int_t
+ngx_quic_send_early_cc(ngx_connection_t *c, ngx_quic_header_t *inpkt,
+    ngx_uint_t err, const char *reason)
+{
+    ssize_t            len;
+    ngx_str_t          res;
+    ngx_quic_keys_t    keys;
+    ngx_quic_frame_t   frame;
+    ngx_quic_header_t  pkt;
+
+    static u_char       src[NGX_QUIC_MAX_UDP_PAYLOAD_SIZE];
+    static u_char       dst[NGX_QUIC_MAX_UDP_PAYLOAD_SIZE];
+
+    ngx_memzero(&frame, sizeof(ngx_quic_frame_t));
+    ngx_memzero(&pkt, sizeof(ngx_quic_header_t));
+
+    frame.level = inpkt->level;
+    frame.type = NGX_QUIC_FT_CONNECTION_CLOSE;
+    frame.u.close.error_code = err;
+
+    frame.u.close.reason.data = (u_char *) reason;
+    frame.u.close.reason.len = ngx_strlen(reason);
+
+    ngx_quic_log_frame(c->log, &frame, 1);
+
+    len = ngx_quic_create_frame(NULL, &frame);
+    if (len > NGX_QUIC_MAX_UDP_PAYLOAD_SIZE) {
+        return NGX_ERROR;
+    }
+
+    len = ngx_quic_create_frame(src, &frame);
+    if (len == -1) {
+        return NGX_ERROR;
+    }
+
+    ngx_memzero(&keys, sizeof(ngx_quic_keys_t));
+
+    pkt.keys = &keys;
+
+    if (ngx_quic_keys_set_initial_secret(pkt.keys, &inpkt->dcid, c->log)
+        != NGX_OK)
+    {
+        return NGX_ERROR;
+    }
+
+    pkt.flags = NGX_QUIC_PKT_FIXED_BIT | NGX_QUIC_PKT_LONG
+                | NGX_QUIC_PKT_INITIAL;
+
+    pkt.num_len = 1;
+    /*
+     * pkt.num = 0;
+     * pkt.trunc = 0;
+     */
+
+    pkt.version = inpkt->version;
+    pkt.log = c->log;
+    pkt.level = inpkt->level;
+    pkt.dcid = inpkt->scid;
+    pkt.scid = inpkt->dcid;
+    pkt.payload.data = src;
+    pkt.payload.len = len;
+
+    res.data = dst;
+
+    ngx_quic_log_packet(c->log, &pkt);
+
+    if (ngx_quic_encrypt(&pkt, &res) != NGX_OK) {
+        ngx_quic_keys_cleanup(pkt.keys);
+        return NGX_ERROR;
+    }
+
+    if (ngx_quic_send(c, res.data, res.len, c->sockaddr, c->socklen) < 0) {
+        ngx_quic_keys_cleanup(pkt.keys);
+        return NGX_ERROR;
+    }
+
+    ngx_quic_keys_cleanup(pkt.keys);
+
+    return NGX_DONE;
+}
+
+
+ngx_int_t
+ngx_quic_send_retry(ngx_connection_t *c, ngx_quic_conf_t *conf,
+    ngx_quic_header_t *inpkt)
+{
+    time_t             expires;
+    ssize_t            len;
+    ngx_str_t          res, token;
+    ngx_quic_header_t  pkt;
+
+    u_char             buf[NGX_QUIC_RETRY_BUFFER_SIZE];
+    u_char             dcid[NGX_QUIC_SERVER_CID_LEN];
+    u_char             tbuf[NGX_QUIC_TOKEN_BUF_SIZE];
+
+    expires = ngx_time() + NGX_QUIC_RETRY_TOKEN_LIFETIME;
+
+    token.data = tbuf;
+    token.len = NGX_QUIC_TOKEN_BUF_SIZE;
+
+    if (ngx_quic_new_token(c->log, c->sockaddr, c->socklen, conf->av_token_key,
+                           &token, &inpkt->dcid, expires, 1)
+        != NGX_OK)
+    {
+        return NGX_ERROR;
+    }
+
+    ngx_memzero(&pkt, sizeof(ngx_quic_header_t));
+    pkt.flags = NGX_QUIC_PKT_FIXED_BIT | NGX_QUIC_PKT_LONG | NGX_QUIC_PKT_RETRY;
+    pkt.version = inpkt->version;
+    pkt.log = c->log;
+
+    pkt.odcid = inpkt->dcid;
+    pkt.dcid = inpkt->scid;
+
+    /* TODO: generate routable dcid */
+    if (RAND_bytes(dcid, NGX_QUIC_SERVER_CID_LEN) != 1) {
+        return NGX_ERROR;
+    }
+
+    pkt.scid.len = NGX_QUIC_SERVER_CID_LEN;
+    pkt.scid.data = dcid;
+
+    pkt.token = token;
+
+    res.data = buf;
+
+    if (ngx_quic_encrypt(&pkt, &res) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+#ifdef NGX_QUIC_DEBUG_PACKETS
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic packet to send len:%uz %xV", res.len, &res);
+#endif
+
+    len = ngx_quic_send(c, res.data, res.len, c->sockaddr, c->socklen);
+    if (len < 0) {
+        return NGX_ERROR;
+    }
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic retry packet sent to %xV", &pkt.dcid);
+
+    /*
+     * RFC 9000, 17.2.5.1.  Sending a Retry Packet
+     *
+     * A server MUST NOT send more than one Retry
+     * packet in response to a single UDP datagram.
+     * NGX_DONE will stop quic_input() from processing further
+     */
+    return NGX_DONE;
+}
+
+
+ngx_int_t
+ngx_quic_send_new_token(ngx_connection_t *c, ngx_quic_path_t *path)
+{
+    time_t                  expires;
+    ngx_str_t               token;
+    ngx_chain_t            *out;
+    ngx_quic_frame_t       *frame;
+    ngx_quic_connection_t  *qc;
+
+    u_char                  tbuf[NGX_QUIC_TOKEN_BUF_SIZE];
+
+    qc = ngx_quic_get_connection(c);
+
+    expires = ngx_time() + NGX_QUIC_NEW_TOKEN_LIFETIME;
+
+    token.data = tbuf;
+    token.len = NGX_QUIC_TOKEN_BUF_SIZE;
+
+    if (ngx_quic_new_token(c->log, path->sockaddr, path->socklen,
+                           qc->conf->av_token_key, &token, NULL, expires, 0)
+        != NGX_OK)
+    {
+        return NGX_ERROR;
+    }
+
+    out = ngx_quic_copy_buffer(c, token.data, token.len);
+    if (out == NGX_CHAIN_ERROR) {
+        return NGX_ERROR;
+    }
+
+    frame = ngx_quic_alloc_frame(c);
+    if (frame == NULL) {
+        return NGX_ERROR;
+    }
+
+    frame->level = ssl_encryption_application;
+    frame->type = NGX_QUIC_FT_NEW_TOKEN;
+    frame->data = out;
+    frame->u.token.length = token.len;
+
+    ngx_quic_queue_frame(qc, frame);
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_quic_send_ack(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx)
+{
+    size_t                  len, left;
+    uint64_t                ack_delay;
+    ngx_buf_t              *b;
+    ngx_uint_t              i;
+    ngx_chain_t            *cl, **ll;
+    ngx_quic_frame_t       *frame;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    ack_delay = ngx_current_msec - ctx->largest_received;
+    ack_delay *= 1000;
+    ack_delay >>= qc->tp.ack_delay_exponent;
+
+    frame = ngx_quic_alloc_frame(c);
+    if (frame == NULL) {
+        return NGX_ERROR;
+    }
+
+    ll = &frame->data;
+    b = NULL;
+
+    for (i = 0; i < ctx->nranges; i++) {
+        len = ngx_quic_create_ack_range(NULL, ctx->ranges[i].gap,
+                                        ctx->ranges[i].range);
+
+        left = b ? b->end - b->last : 0;
+
+        if (left < len) {
+            cl = ngx_quic_alloc_chain(c);
+            if (cl == NULL) {
+                return NGX_ERROR;
+            }
+
+            *ll = cl;
+            ll = &cl->next;
+
+            b = cl->buf;
+            left = b->end - b->last;
+
+            if (left < len) {
+                return NGX_ERROR;
+            }
+        }
+
+        b->last += ngx_quic_create_ack_range(b->last, ctx->ranges[i].gap,
+                                             ctx->ranges[i].range);
+
+        frame->u.ack.ranges_length += len;
+    }
+
+    *ll = NULL;
+
+    frame->level = ctx->level;
+    frame->type = NGX_QUIC_FT_ACK;
+    frame->u.ack.largest = ctx->largest_range;
+    frame->u.ack.delay = ack_delay;
+    frame->u.ack.range_count = ctx->nranges;
+    frame->u.ack.first_range = ctx->first_range;
+    frame->len = ngx_quic_create_frame(NULL, frame);
+
+    ngx_queue_insert_head(&ctx->frames, &frame->queue);
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_quic_send_ack_range(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx,
+    uint64_t smallest, uint64_t largest)
+{
+    ngx_quic_frame_t       *frame;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    frame = ngx_quic_alloc_frame(c);
+    if (frame == NULL) {
+        return NGX_ERROR;
+    }
+
+    frame->level = ctx->level;
+    frame->type = NGX_QUIC_FT_ACK;
+    frame->u.ack.largest = largest;
+    frame->u.ack.delay = 0;
+    frame->u.ack.range_count = 0;
+    frame->u.ack.first_range = largest - smallest;
+
+    ngx_quic_queue_frame(qc, frame);
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_quic_frame_sendto(ngx_connection_t *c, ngx_quic_frame_t *frame,
+    size_t min, ngx_quic_path_t *path)
+{
+    size_t                  max, max_payload, min_payload, pad;
+    ssize_t                 len, sent;
+    ngx_str_t               res;
+    ngx_msec_t              now;
+    ngx_quic_header_t       pkt;
+    ngx_quic_send_ctx_t    *ctx;
+    ngx_quic_congestion_t  *cg;
+    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];
+
+    qc = ngx_quic_get_connection(c);
+    cg = &qc->congestion;
+    ctx = ngx_quic_get_send_ctx(qc, frame->level);
+
+    now = ngx_current_msec;
+
+    max = ngx_quic_path_limit(c, path, path->mtu);
+
+    ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic sendto %s packet max:%uz min:%uz",
+                   ngx_quic_level_name(ctx->level), max, min);
+
+    if (cg->in_flight >= cg->window && !frame->ignore_congestion) {
+        ngx_quic_free_frame(c, frame);
+        return NGX_AGAIN;
+    }
+
+    ngx_quic_init_packet(c, ctx, &pkt, path);
+
+    min_payload = ngx_quic_payload_size(&pkt, min);
+    max_payload = ngx_quic_payload_size(&pkt, max);
+
+    /* RFC 9001, 5.4.2.  Header Protection Sample */
+    pad = 4 - pkt.num_len;
+    min_payload = ngx_max(min_payload, pad);
+
+    if (min_payload > max_payload) {
+        ngx_quic_free_frame(c, frame);
+        return NGX_AGAIN;
+    }
+
+#if (NGX_DEBUG)
+    frame->pnum = pkt.number;
+#endif
+
+    ngx_quic_log_frame(c->log, frame, 1);
+
+    len = ngx_quic_create_frame(NULL, frame);
+    if ((size_t) len > max_payload) {
+        ngx_quic_free_frame(c, frame);
+        return NGX_AGAIN;
+    }
+
+    len = ngx_quic_create_frame(src, frame);
+    if (len == -1) {
+        ngx_quic_free_frame(c, frame);
+        return NGX_ERROR;
+    }
+
+    if (len < (ssize_t) min_payload) {
+        ngx_memset(src + len, NGX_QUIC_FT_PADDING, min_payload - len);
+        len = min_payload;
+    }
+
+    pkt.payload.data = src;
+    pkt.payload.len = len;
+
+    res.data = dst;
+
+    ngx_quic_log_packet(c->log, &pkt);
+
+    if (ngx_quic_encrypt(&pkt, &res) != NGX_OK) {
+        ngx_quic_free_frame(c, frame);
+        return NGX_ERROR;
+    }
+
+    frame->pnum = ctx->pnum;
+    frame->send_time = now;
+    frame->plen = res.len;
+
+    ctx->pnum++;
+
+    sent = ngx_quic_send(c, res.data, res.len, path->sockaddr, path->socklen);
+    if (sent < 0) {
+        ngx_quic_free_frame(c, frame);
+        return sent;
+    }
+
+    path->sent += sent;
+
+    if (frame->need_ack && !qc->closing) {
+        ngx_queue_insert_tail(&ctx->sent, &frame->queue);
+
+        cg->in_flight += frame->plen;
+
+    } else {
+        ngx_quic_free_frame(c, frame);
+        return NGX_OK;
+    }
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic congestion send if:%uz", cg->in_flight);
+
+    if (!qc->send_timer_set) {
+        qc->send_timer_set = 1;
+        ngx_add_timer(c->read, qc->tp.max_idle_timeout);
+    }
+
+    ngx_quic_set_lost_timer(c);
+
+    return NGX_OK;
+}
+
+
+size_t
+ngx_quic_path_limit(ngx_connection_t *c, ngx_quic_path_t *path, size_t size)
+{
+    off_t  max;
+
+    if (!path->validated) {
+        max = path->received * 3;
+        max = (path->sent >= max) ? 0 : max - path->sent;
+
+        if ((off_t) size > max) {
+            return max;
+        }
+    }
+
+    return size;
+}
diff --git a/src/event/quic/ngx_event_quic_output.h b/src/event/quic/ngx_event_quic_output.h
new file mode 100644
index 0000000..52d8a37
--- /dev/null
+++ b/src/event/quic/ngx_event_quic_output.h
@@ -0,0 +1,40 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_EVENT_QUIC_OUTPUT_H_INCLUDED_
+#define _NGX_EVENT_QUIC_OUTPUT_H_INCLUDED_
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+
+ngx_int_t ngx_quic_output(ngx_connection_t *c);
+
+ngx_int_t ngx_quic_negotiate_version(ngx_connection_t *c,
+    ngx_quic_header_t *inpkt);
+
+ngx_int_t ngx_quic_send_stateless_reset(ngx_connection_t *c,
+    ngx_quic_conf_t *conf, ngx_quic_header_t *pkt);
+ngx_int_t ngx_quic_send_cc(ngx_connection_t *c);
+ngx_int_t ngx_quic_send_early_cc(ngx_connection_t *c,
+    ngx_quic_header_t *inpkt, ngx_uint_t err, const char *reason);
+
+ngx_int_t ngx_quic_send_retry(ngx_connection_t *c,
+    ngx_quic_conf_t *conf, ngx_quic_header_t *pkt);
+ngx_int_t ngx_quic_send_new_token(ngx_connection_t *c, ngx_quic_path_t *path);
+
+ngx_int_t ngx_quic_send_ack(ngx_connection_t *c,
+    ngx_quic_send_ctx_t *ctx);
+ngx_int_t ngx_quic_send_ack_range(ngx_connection_t *c,
+    ngx_quic_send_ctx_t *ctx, uint64_t smallest, uint64_t largest);
+
+ngx_int_t ngx_quic_frame_sendto(ngx_connection_t *c, ngx_quic_frame_t *frame,
+    size_t min, ngx_quic_path_t *path);
+size_t ngx_quic_path_limit(ngx_connection_t *c, ngx_quic_path_t *path,
+    size_t size);
+
+#endif /* _NGX_EVENT_QUIC_OUTPUT_H_INCLUDED_ */
diff --git a/src/event/quic/ngx_event_quic_protection.c b/src/event/quic/ngx_event_quic_protection.c
new file mode 100644
index 0000000..8223626
--- /dev/null
+++ b/src/event/quic/ngx_event_quic_protection.c
@@ -0,0 +1,1243 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_event.h>
+#include <ngx_event_quic_connection.h>
+
+
+/* RFC 9001, 5.4.1.  Header Protection Application: 5-byte mask */
+#define NGX_QUIC_HP_LEN               5
+
+#define NGX_QUIC_AES_128_KEY_LEN      16
+
+#define NGX_QUIC_INITIAL_CIPHER       TLS1_3_CK_AES_128_GCM_SHA256
+
+
+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 ngx_int_t ngx_quic_crypto_open(ngx_quic_secret_t *s, ngx_str_t *out,
+    u_char *nonce, ngx_str_t *in, ngx_str_t *ad, ngx_log_t *log);
+#ifndef OPENSSL_IS_BORINGSSL
+static ngx_int_t ngx_quic_crypto_common(ngx_quic_secret_t *s, ngx_str_t *out,
+    u_char *nonce, ngx_str_t *in, ngx_str_t *ad, ngx_log_t *log);
+#endif
+
+static ngx_int_t ngx_quic_crypto_hp_init(const EVP_CIPHER *cipher,
+    ngx_quic_secret_t *s, ngx_log_t *log);
+static ngx_int_t ngx_quic_crypto_hp(ngx_quic_secret_t *s,
+    u_char *out, u_char *in, ngx_log_t *log);
+static void ngx_quic_crypto_hp_cleanup(ngx_quic_secret_t *s);
+
+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);
+
+
+ngx_int_t
+ngx_quic_ciphers(ngx_uint_t id, ngx_quic_ciphers_t *ciphers)
+{
+    ngx_int_t  len;
+
+    switch (id) {
+
+    case TLS1_3_CK_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 TLS1_3_CK_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 TLS1_3_CK_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;
+
+#ifndef OPENSSL_IS_BORINGSSL
+    case TLS1_3_CK_AES_128_CCM_SHA256:
+        ciphers->c = EVP_aes_128_ccm();
+        ciphers->hp = EVP_aes_128_ctr();
+        ciphers->d = EVP_sha256();
+        len = 16;
+        break;
+#endif
+
+    default:
+        return NGX_ERROR;
+    }
+
+    return len;
+}
+
+
+ngx_int_t
+ngx_quic_keys_set_initial_secret(ngx_quic_keys_t *keys, ngx_str_t *secret,
+    ngx_log_t *log)
+{
+    size_t               is_len;
+    uint8_t              is[SHA256_DIGEST_LENGTH];
+    ngx_str_t            iss;
+    ngx_uint_t           i;
+    const EVP_MD        *digest;
+    ngx_quic_md_t        client_key, server_key;
+    ngx_quic_hkdf_t      seq[8];
+    ngx_quic_secret_t   *client, *server;
+    ngx_quic_ciphers_t   ciphers;
+
+    static const uint8_t salt[20] =
+        "\x38\x76\x2c\xf7\xf5\x59\x34\xb3\x4d\x17"
+        "\x9a\xe6\xa4\xc8\x0c\xad\xcc\xbb\x7f\x0a";
+
+    client = &keys->secrets[ssl_encryption_initial].client;
+    server = &keys->secrets[ssl_encryption_initial].server;
+
+    /*
+     * RFC 9001, section 5.  Packet Protection
+     *
+     * Initial packets use AEAD_AES_128_GCM.  The hash function
+     * for HKDF when deriving initial secrets and keys is SHA-256.
+     */
+
+    digest = EVP_sha256();
+    is_len = SHA256_DIGEST_LENGTH;
+
+    if (ngx_hkdf_extract(is, &is_len, digest, secret->data, secret->len,
+                         salt, sizeof(salt))
+        != NGX_OK)
+    {
+        return NGX_ERROR;
+    }
+
+    iss.len = is_len;
+    iss.data = is;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, log, 0,
+                   "quic ngx_quic_set_initial_secret");
+#ifdef NGX_QUIC_DEBUG_CRYPTO
+    ngx_log_debug3(NGX_LOG_DEBUG_EVENT, log, 0,
+                   "quic salt len:%uz %*xs", sizeof(salt), sizeof(salt), salt);
+    ngx_log_debug3(NGX_LOG_DEBUG_EVENT, log, 0,
+                   "quic initial secret len:%uz %*xs", is_len, is_len, is);
+#endif
+
+    client->secret.len = SHA256_DIGEST_LENGTH;
+    server->secret.len = SHA256_DIGEST_LENGTH;
+
+    client_key.len = NGX_QUIC_AES_128_KEY_LEN;
+    server_key.len = NGX_QUIC_AES_128_KEY_LEN;
+
+    client->hp.len = NGX_QUIC_AES_128_KEY_LEN;
+    server->hp.len = NGX_QUIC_AES_128_KEY_LEN;
+
+    client->iv.len = NGX_QUIC_IV_LEN;
+    server->iv.len = NGX_QUIC_IV_LEN;
+
+    /* labels per RFC 9001, 5.1. Packet Protection Keys */
+    ngx_quic_hkdf_set(&seq[0], "tls13 client in", &client->secret, &iss);
+    ngx_quic_hkdf_set(&seq[1], "tls13 quic key", &client_key, &client->secret);
+    ngx_quic_hkdf_set(&seq[2], "tls13 quic iv", &client->iv, &client->secret);
+    ngx_quic_hkdf_set(&seq[3], "tls13 quic hp", &client->hp, &client->secret);
+    ngx_quic_hkdf_set(&seq[4], "tls13 server in", &server->secret, &iss);
+    ngx_quic_hkdf_set(&seq[5], "tls13 quic key", &server_key, &server->secret);
+    ngx_quic_hkdf_set(&seq[6], "tls13 quic iv", &server->iv, &server->secret);
+    ngx_quic_hkdf_set(&seq[7], "tls13 quic hp", &server->hp, &server->secret);
+
+    for (i = 0; i < (sizeof(seq) / sizeof(seq[0])); i++) {
+        if (ngx_quic_hkdf_expand(&seq[i], digest, log) != NGX_OK) {
+            return NGX_ERROR;
+        }
+    }
+
+    if (ngx_quic_ciphers(NGX_QUIC_INITIAL_CIPHER, &ciphers) == NGX_ERROR) {
+        return NGX_ERROR;
+    }
+
+    if (ngx_quic_crypto_init(ciphers.c, client, &client_key, 0, log)
+        == NGX_ERROR)
+    {
+        return NGX_ERROR;
+    }
+
+    if (ngx_quic_crypto_init(ciphers.c, server, &server_key, 1, log)
+        == NGX_ERROR)
+    {
+        goto failed;
+    }
+
+    if (ngx_quic_crypto_hp_init(ciphers.hp, client, log) == NGX_ERROR) {
+        goto failed;
+    }
+
+    if (ngx_quic_crypto_hp_init(ciphers.hp, server, log) == NGX_ERROR) {
+        goto failed;
+    }
+
+    return NGX_OK;
+
+failed:
+
+    ngx_quic_keys_cleanup(keys);
+
+    return NGX_ERROR;
+}
+
+
+ngx_int_t
+ngx_quic_hkdf_expand(ngx_quic_hkdf_t *h, const EVP_MD *digest, ngx_log_t *log)
+{
+    size_t    info_len;
+    uint8_t  *p;
+    uint8_t   info[20];
+
+    info_len = 2 + 1 + h->label_len + 1;
+
+    info[0] = 0;
+    info[1] = h->out_len;
+    info[2] = h->label_len;
+
+    p = ngx_cpymem(&info[3], h->label, h->label_len);
+    *p = '\0';
+
+    if (ngx_hkdf_expand(h->out, h->out_len, digest,
+                        h->prk, h->prk_len, info, info_len)
+        != NGX_OK)
+    {
+        ngx_ssl_error(NGX_LOG_INFO, log, 0,
+                      "ngx_hkdf_expand(%*s) failed", h->label_len, h->label);
+        return NGX_ERROR;
+    }
+
+#ifdef NGX_QUIC_DEBUG_CRYPTO
+    ngx_log_debug5(NGX_LOG_DEBUG_EVENT, log, 0,
+                   "quic expand \"%*s\" len:%uz %*xs",
+                   h->label_len, h->label, h->out_len, h->out_len, h->out);
+#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;
+    }
+
+    return NGX_OK;
+
+#else
+
+    EVP_PKEY_CTX  *pctx;
+
+    pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, NULL);
+    if (pctx == NULL) {
+        return NGX_ERROR;
+    }
+
+    if (EVP_PKEY_derive_init(pctx) <= 0) {
+        goto failed;
+    }
+
+    if (EVP_PKEY_CTX_hkdf_mode(pctx, EVP_PKEY_HKDEF_MODE_EXPAND_ONLY) <= 0) {
+        goto failed;
+    }
+
+    if (EVP_PKEY_CTX_set_hkdf_md(pctx, digest) <= 0) {
+        goto failed;
+    }
+
+    if (EVP_PKEY_CTX_set1_hkdf_key(pctx, prk, prk_len) <= 0) {
+        goto failed;
+    }
+
+    if (EVP_PKEY_CTX_add1_hkdf_info(pctx, info, info_len) <= 0) {
+        goto failed;
+    }
+
+    if (EVP_PKEY_derive(pctx, out_key, &out_len) <= 0) {
+        goto failed;
+    }
+
+    EVP_PKEY_CTX_free(pctx);
+
+    return NGX_OK;
+
+failed:
+
+    EVP_PKEY_CTX_free(pctx);
+
+    return NGX_ERROR;
+
+#endif
+}
+
+
+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;
+    }
+
+    return NGX_OK;
+
+#else
+
+    EVP_PKEY_CTX  *pctx;
+
+    pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, NULL);
+    if (pctx == NULL) {
+        return NGX_ERROR;
+    }
+
+    if (EVP_PKEY_derive_init(pctx) <= 0) {
+        goto failed;
+    }
+
+    if (EVP_PKEY_CTX_hkdf_mode(pctx, EVP_PKEY_HKDEF_MODE_EXTRACT_ONLY) <= 0) {
+        goto failed;
+    }
+
+    if (EVP_PKEY_CTX_set_hkdf_md(pctx, digest) <= 0) {
+        goto failed;
+    }
+
+    if (EVP_PKEY_CTX_set1_hkdf_key(pctx, secret, secret_len) <= 0) {
+        goto failed;
+    }
+
+    if (EVP_PKEY_CTX_set1_hkdf_salt(pctx, salt, salt_len) <= 0) {
+        goto failed;
+    }
+
+    if (EVP_PKEY_derive(pctx, out_key, out_len) <= 0) {
+        goto failed;
+    }
+
+    EVP_PKEY_CTX_free(pctx);
+
+    return NGX_OK;
+
+failed:
+
+    EVP_PKEY_CTX_free(pctx);
+
+    return NGX_ERROR;
+
+#endif
+}
+
+
+ngx_int_t
+ngx_quic_crypto_init(const ngx_quic_cipher_t *cipher, ngx_quic_secret_t *s,
+    ngx_quic_md_t *key, ngx_int_t enc, ngx_log_t *log)
+{
+
+#ifdef OPENSSL_IS_BORINGSSL
+    EVP_AEAD_CTX  *ctx;
+
+    ctx = EVP_AEAD_CTX_new(cipher, key->data, 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;
+    }
+#else
+    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_CipherInit_ex(ctx, cipher, NULL, NULL, NULL, enc) != 1) {
+        EVP_CIPHER_CTX_free(ctx);
+        ngx_ssl_error(NGX_LOG_INFO, log, 0, "EVP_CipherInit_ex() failed");
+        return NGX_ERROR;
+    }
+
+    if (EVP_CIPHER_mode(cipher) == EVP_CIPH_CCM_MODE
+        && EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG, NGX_QUIC_TAG_LEN,
+                               NULL)
+           == 0)
+    {
+        EVP_CIPHER_CTX_free(ctx);
+        ngx_ssl_error(NGX_LOG_INFO, log, 0,
+                      "EVP_CIPHER_CTX_ctrl(EVP_CTRL_AEAD_SET_TAG) failed");
+        return NGX_ERROR;
+    }
+
+    if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_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_AEAD_SET_IVLEN) failed");
+        return NGX_ERROR;
+    }
+
+    if (EVP_CipherInit_ex(ctx, NULL, NULL, key->data, NULL, enc) != 1) {
+        EVP_CIPHER_CTX_free(ctx);
+        ngx_ssl_error(NGX_LOG_INFO, log, 0, "EVP_CipherInit_ex() failed");
+        return NGX_ERROR;
+    }
+#endif
+
+    s->ctx = ctx;
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_crypto_open(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
+    if (EVP_AEAD_CTX_open(s->ctx, out->data, &out->len, out->len, nonce,
+                          s->iv.len, in->data, in->len, ad->data, ad->len)
+        != 1)
+    {
+        ngx_ssl_error(NGX_LOG_INFO, log, 0, "EVP_AEAD_CTX_open() failed");
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+#else
+    return ngx_quic_crypto_common(s, out, nonce, in, ad, log);
+#endif
+}
+
+
+ngx_int_t
+ngx_quic_crypto_seal(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
+    if (EVP_AEAD_CTX_seal(s->ctx, out->data, &out->len, out->len, nonce,
+                          s->iv.len, in->data, in->len, ad->data, ad->len)
+        != 1)
+    {
+        ngx_ssl_error(NGX_LOG_INFO, log, 0, "EVP_AEAD_CTX_seal() failed");
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+#else
+    return ngx_quic_crypto_common(s, out, nonce, in, ad, log);
+#endif
+}
+
+
+#ifndef OPENSSL_IS_BORINGSSL
+
+static ngx_int_t
+ngx_quic_crypto_common(ngx_quic_secret_t *s, ngx_str_t *out, u_char *nonce,
+    ngx_str_t *in, ngx_str_t *ad, ngx_log_t *log)
+{
+    int                     len, enc;
+    ngx_quic_crypto_ctx_t  *ctx;
+
+    ctx = s->ctx;
+    enc = EVP_CIPHER_CTX_encrypting(ctx);
+
+    if (EVP_CipherInit_ex(ctx, NULL, NULL, NULL, nonce, enc) != 1) {
+        ngx_ssl_error(NGX_LOG_INFO, log, 0, "EVP_CipherInit_ex() failed");
+        return NGX_ERROR;
+    }
+
+    if (enc == 0) {
+        in->len -= NGX_QUIC_TAG_LEN;
+
+        if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG, NGX_QUIC_TAG_LEN,
+                                in->data + in->len)
+            == 0)
+        {
+            ngx_ssl_error(NGX_LOG_INFO, log, 0,
+                          "EVP_CIPHER_CTX_ctrl(EVP_CTRL_AEAD_SET_TAG) failed");
+            return NGX_ERROR;
+        }
+    }
+
+    if (EVP_CIPHER_mode(EVP_CIPHER_CTX_cipher(ctx)) == EVP_CIPH_CCM_MODE
+        && EVP_CipherUpdate(ctx, NULL, &len, NULL, in->len) != 1)
+    {
+        ngx_ssl_error(NGX_LOG_INFO, log, 0, "EVP_CipherUpdate() failed");
+        return NGX_ERROR;
+    }
+
+    if (EVP_CipherUpdate(ctx, NULL, &len, ad->data, ad->len) != 1) {
+        ngx_ssl_error(NGX_LOG_INFO, log, 0, "EVP_CipherUpdate() failed");
+        return NGX_ERROR;
+    }
+
+    if (EVP_CipherUpdate(ctx, out->data, &len, in->data, in->len) != 1) {
+        ngx_ssl_error(NGX_LOG_INFO, log, 0, "EVP_CipherUpdate() failed");
+        return NGX_ERROR;
+    }
+
+    out->len = len;
+
+    if (EVP_CipherFinal_ex(ctx, out->data + out->len, &len) <= 0) {
+        ngx_ssl_error(NGX_LOG_INFO, log, 0, "EVP_CipherFinal_ex failed");
+        return NGX_ERROR;
+    }
+
+    out->len += len;
+
+    if (enc == 1) {
+        if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG, NGX_QUIC_TAG_LEN,
+                                out->data + out->len)
+            == 0)
+        {
+            ngx_ssl_error(NGX_LOG_INFO, log, 0,
+                          "EVP_CIPHER_CTX_ctrl(EVP_CTRL_AEAD_GET_TAG) failed");
+            return NGX_ERROR;
+        }
+
+        out->len += NGX_QUIC_TAG_LEN;
+    }
+
+    return NGX_OK;
+}
+
+#endif
+
+
+void
+ngx_quic_crypto_cleanup(ngx_quic_secret_t *s)
+{
+    if (s->ctx) {
+#ifdef OPENSSL_IS_BORINGSSL
+        EVP_AEAD_CTX_free(s->ctx);
+#else
+        EVP_CIPHER_CTX_free(s->ctx);
+#endif
+        s->ctx = NULL;
+    }
+}
+
+
+static ngx_int_t
+ngx_quic_crypto_hp_init(const EVP_CIPHER *cipher, ngx_quic_secret_t *s,
+    ngx_log_t *log)
+{
+    EVP_CIPHER_CTX  *ctx;
+
+#ifdef OPENSSL_IS_BORINGSSL
+    if (cipher == (EVP_CIPHER *) EVP_aead_chacha20_poly1305()) {
+        /* no EVP interface */
+        s->hp_ctx = NULL;
+        return NGX_OK;
+    }
+#endif
+
+    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, s->hp.data, NULL) != 1) {
+        EVP_CIPHER_CTX_free(ctx);
+        ngx_ssl_error(NGX_LOG_INFO, log, 0, "EVP_EncryptInit_ex() failed");
+        return NGX_ERROR;
+    }
+
+    s->hp_ctx = ctx;
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_crypto_hp(ngx_quic_secret_t *s, u_char *out, u_char *in,
+    ngx_log_t *log)
+{
+    int              outlen;
+    EVP_CIPHER_CTX  *ctx;
+    u_char           zero[NGX_QUIC_HP_LEN] = {0};
+
+    ctx = s->hp_ctx;
+
+#ifdef OPENSSL_IS_BORINGSSL
+    uint32_t         cnt;
+
+    if (ctx == NULL) {
+        ngx_memcpy(&cnt, in, sizeof(uint32_t));
+        CRYPTO_chacha_20(out, zero, NGX_QUIC_HP_LEN, s->hp.data, &in[4], cnt);
+        return NGX_OK;
+    }
+#endif
+
+    if (EVP_EncryptInit_ex(ctx, NULL, NULL, NULL, in) != 1) {
+        ngx_ssl_error(NGX_LOG_INFO, log, 0, "EVP_EncryptInit_ex() failed");
+        return NGX_ERROR;
+    }
+
+    if (!EVP_EncryptUpdate(ctx, out, &outlen, zero, NGX_QUIC_HP_LEN)) {
+        ngx_ssl_error(NGX_LOG_INFO, log, 0, "EVP_EncryptUpdate() failed");
+        return NGX_ERROR;
+    }
+
+    if (!EVP_EncryptFinal_ex(ctx, out + NGX_QUIC_HP_LEN, &outlen)) {
+        ngx_ssl_error(NGX_LOG_INFO, log, 0, "EVP_EncryptFinal_Ex() failed");
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}
+
+
+static void
+ngx_quic_crypto_hp_cleanup(ngx_quic_secret_t *s)
+{
+    if (s->hp_ctx) {
+        EVP_CIPHER_CTX_free(s->hp_ctx);
+        s->hp_ctx = NULL;
+    }
+}
+
+
+ngx_int_t
+ngx_quic_keys_set_encryption_secret(ngx_log_t *log, 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_str_t            secret_str;
+    ngx_uint_t           i;
+    ngx_quic_md_t        key;
+    ngx_quic_hkdf_t      seq[3];
+    ngx_quic_secret_t   *peer_secret;
+    ngx_quic_ciphers_t   ciphers;
+
+    peer_secret = is_write ? &keys->secrets[level].server
+                           : &keys->secrets[level].client;
+
+    keys->cipher = SSL_CIPHER_get_id(cipher);
+
+    key_len = ngx_quic_ciphers(keys->cipher, &ciphers);
+
+    if (key_len == NGX_ERROR) {
+        ngx_ssl_error(NGX_LOG_INFO, log, 0, "unexpected cipher");
+        return NGX_ERROR;
+    }
+
+    if (sizeof(peer_secret->secret.data) < secret_len) {
+        ngx_log_error(NGX_LOG_ALERT, log, 0,
+                      "unexpected secret len: %uz", secret_len);
+        return NGX_ERROR;
+    }
+
+    peer_secret->secret.len = secret_len;
+    ngx_memcpy(peer_secret->secret.data, secret, secret_len);
+
+    key.len = key_len;
+    peer_secret->iv.len = NGX_QUIC_IV_LEN;
+    peer_secret->hp.len = key_len;
+
+    secret_str.len = secret_len;
+    secret_str.data = (u_char *) secret;
+
+    ngx_quic_hkdf_set(&seq[0], "tls13 quic key", &key, &secret_str);
+    ngx_quic_hkdf_set(&seq[1], "tls13 quic iv", &peer_secret->iv, &secret_str);
+    ngx_quic_hkdf_set(&seq[2], "tls13 quic hp", &peer_secret->hp, &secret_str);
+
+    for (i = 0; i < (sizeof(seq) / sizeof(seq[0])); i++) {
+        if (ngx_quic_hkdf_expand(&seq[i], ciphers.d, log) != NGX_OK) {
+            return NGX_ERROR;
+        }
+    }
+
+    if (ngx_quic_crypto_init(ciphers.c, peer_secret, &key, is_write, log)
+        == NGX_ERROR)
+    {
+        return NGX_ERROR;
+    }
+
+    if (ngx_quic_crypto_hp_init(ciphers.hp, peer_secret, log) == NGX_ERROR) {
+        return NGX_ERROR;
+    }
+
+    ngx_explicit_memzero(key.data, key.len);
+
+    return NGX_OK;
+}
+
+
+ngx_uint_t
+ngx_quic_keys_available(ngx_quic_keys_t *keys,
+    enum ssl_encryption_level_t level, ngx_uint_t is_write)
+{
+    if (is_write == 0) {
+        return keys->secrets[level].client.ctx != NULL;
+    }
+
+    return keys->secrets[level].server.ctx != NULL;
+}
+
+
+void
+ngx_quic_keys_discard(ngx_quic_keys_t *keys,
+    enum ssl_encryption_level_t level)
+{
+    ngx_quic_secret_t  *client, *server;
+
+    client = &keys->secrets[level].client;
+    server = &keys->secrets[level].server;
+
+    ngx_quic_crypto_cleanup(client);
+    ngx_quic_crypto_cleanup(server);
+
+    ngx_quic_crypto_hp_cleanup(client);
+    ngx_quic_crypto_hp_cleanup(server);
+
+    ngx_explicit_memzero(client->secret.data, client->secret.len);
+    ngx_explicit_memzero(server->secret.data, server->secret.len);
+}
+
+
+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;
+
+    ngx_quic_crypto_cleanup(&current->client);
+    ngx_quic_crypto_cleanup(&current->server);
+
+    tmp = *current;
+    *current = *next;
+    *next = tmp;
+}
+
+
+void
+ngx_quic_keys_update(ngx_event_t *ev)
+{
+    ngx_int_t               key_len;
+    ngx_uint_t              i;
+    ngx_quic_md_t           client_key, server_key;
+    ngx_quic_hkdf_t         seq[6];
+    ngx_quic_keys_t        *keys;
+    ngx_connection_t       *c;
+    ngx_quic_ciphers_t      ciphers;
+    ngx_quic_secrets_t     *current, *next;
+    ngx_quic_connection_t  *qc;
+
+    c = ev->data;
+    qc = ngx_quic_get_connection(c);
+    keys = qc->keys;
+
+    current = &keys->secrets[ssl_encryption_application];
+    next = &keys->next_key;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, "quic key update");
+
+    c->log->action = "updating keys";
+
+    key_len = ngx_quic_ciphers(keys->cipher, &ciphers);
+
+    if (key_len == NGX_ERROR) {
+        goto failed;
+    }
+
+    client_key.len = key_len;
+    server_key.len = key_len;
+
+    next->client.secret.len = current->client.secret.len;
+    next->client.iv.len = NGX_QUIC_IV_LEN;
+    next->client.hp = current->client.hp;
+    next->client.hp_ctx = current->client.hp_ctx;
+
+    next->server.secret.len = current->server.secret.len;
+    next->server.iv.len = NGX_QUIC_IV_LEN;
+    next->server.hp = current->server.hp;
+    next->server.hp_ctx = current->server.hp_ctx;
+
+    ngx_quic_hkdf_set(&seq[0], "tls13 quic ku",
+                      &next->client.secret, &current->client.secret);
+    ngx_quic_hkdf_set(&seq[1], "tls13 quic key",
+                      &client_key, &next->client.secret);
+    ngx_quic_hkdf_set(&seq[2], "tls13 quic iv",
+                      &next->client.iv, &next->client.secret);
+    ngx_quic_hkdf_set(&seq[3], "tls13 quic ku",
+                      &next->server.secret, &current->server.secret);
+    ngx_quic_hkdf_set(&seq[4], "tls13 quic key",
+                      &server_key, &next->server.secret);
+    ngx_quic_hkdf_set(&seq[5], "tls13 quic iv",
+                      &next->server.iv, &next->server.secret);
+
+    for (i = 0; i < (sizeof(seq) / sizeof(seq[0])); i++) {
+        if (ngx_quic_hkdf_expand(&seq[i], ciphers.d, c->log) != NGX_OK) {
+            goto failed;
+        }
+    }
+
+    if (ngx_quic_crypto_init(ciphers.c, &next->client, &client_key, 0, c->log)
+        == NGX_ERROR)
+    {
+        goto failed;
+    }
+
+    if (ngx_quic_crypto_init(ciphers.c, &next->server, &server_key, 1, c->log)
+        == NGX_ERROR)
+    {
+        goto failed;
+    }
+
+    ngx_explicit_memzero(current->client.secret.data,
+                         current->client.secret.len);
+    ngx_explicit_memzero(current->server.secret.data,
+                         current->server.secret.len);
+
+    ngx_explicit_memzero(client_key.data, client_key.len);
+    ngx_explicit_memzero(server_key.data, server_key.len);
+
+    return;
+
+failed:
+
+    ngx_quic_close_connection(c, NGX_ERROR);
+}
+
+
+void
+ngx_quic_keys_cleanup(ngx_quic_keys_t *keys)
+{
+    ngx_uint_t           i;
+    ngx_quic_secrets_t  *next;
+
+    for (i = 0; i < NGX_QUIC_ENCRYPTION_LAST; i++) {
+        ngx_quic_keys_discard(keys, i);
+    }
+
+    next = &keys->next_key;
+
+    ngx_quic_crypto_cleanup(&next->client);
+    ngx_quic_crypto_cleanup(&next->server);
+
+    ngx_explicit_memzero(next->client.secret.data,
+                         next->client.secret.len);
+    ngx_explicit_memzero(next->server.secret.data,
+                         next->server.secret.len);
+}
+
+
+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;
+    u_char              nonce[NGX_QUIC_IV_LEN], mask[NGX_QUIC_HP_LEN];
+
+    ad.data = res->data;
+    ad.len = ngx_quic_create_header(pkt, ad.data, &pnp);
+
+    out.len = pkt->payload.len + NGX_QUIC_TAG_LEN;
+    out.data = res->data + ad.len;
+
+#ifdef NGX_QUIC_DEBUG_CRYPTO
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, pkt->log, 0,
+                   "quic ad len:%uz %xV", ad.len, &ad);
+#endif
+
+    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_crypto_seal(secret, &out, nonce, &pkt->payload, &ad, pkt->log)
+        != NGX_OK)
+    {
+        return NGX_ERROR;
+    }
+
+    sample = &out.data[4 - pkt->num_len];
+    if (ngx_quic_crypto_hp(secret, mask, sample, pkt->log) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    /* RFC 9001, 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_md_t        key;
+    ngx_quic_secret_t    secret;
+    ngx_quic_ciphers_t   ciphers;
+
+    /* 5.8.  Retry Packet Integrity */
+    static u_char     key_data[16] =
+        "\xbe\x0c\x69\x0b\x9f\x66\x57\x5a\x1d\x76\x6b\x54\xe3\x68\xc8\x4e";
+    static u_char     nonce[NGX_QUIC_IV_LEN] =
+        "\x46\x15\x99\xd3\x5d\x63\x2b\xf2\x23\x98\x25\xbb";
+    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 = NGX_QUIC_TAG_LEN;
+
+#ifdef NGX_QUIC_DEBUG_CRYPTO
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, pkt->log, 0,
+                   "quic retry itag len:%uz %xV", ad.len, &ad);
+#endif
+
+    if (ngx_quic_ciphers(NGX_QUIC_INITIAL_CIPHER, &ciphers) == NGX_ERROR) {
+        return NGX_ERROR;
+    }
+
+    key.len = sizeof(key_data);
+    ngx_memcpy(key.data, key_data, sizeof(key_data));
+    secret.iv.len = NGX_QUIC_IV_LEN;
+
+    if (ngx_quic_crypto_init(ciphers.c, &secret, &key, 1, pkt->log)
+        == NGX_ERROR)
+    {
+        return NGX_ERROR;
+    }
+
+    if (ngx_quic_crypto_seal(&secret, &itag, nonce, &in, &ad, pkt->log)
+        != NGX_OK)
+    {
+        ngx_quic_crypto_cleanup(&secret);
+        return NGX_ERROR;
+    }
+
+    ngx_quic_crypto_cleanup(&secret);
+
+    res->len = itag.data + itag.len - start;
+    res->data = start;
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_quic_derive_key(ngx_log_t *log, const char *label, ngx_str_t *secret,
+    ngx_str_t *salt, u_char *out, size_t len)
+{
+    size_t         is_len, info_len;
+    uint8_t       *p;
+    const EVP_MD  *digest;
+
+    uint8_t        is[SHA256_DIGEST_LENGTH];
+    uint8_t        info[20];
+
+    digest = EVP_sha256();
+    is_len = SHA256_DIGEST_LENGTH;
+
+    if (ngx_hkdf_extract(is, &is_len, digest, secret->data, secret->len,
+                         salt->data, salt->len)
+        != NGX_OK)
+    {
+        ngx_ssl_error(NGX_LOG_INFO, log, 0,
+                      "ngx_hkdf_extract(%s) failed", label);
+        return NGX_ERROR;
+    }
+
+    info[0] = 0;
+    info[1] = len;
+    info[2] = ngx_strlen(label);
+
+    info_len = 2 + 1 + info[2] + 1;
+
+    if (info_len >= 20) {
+        ngx_log_error(NGX_LOG_INFO, log, 0,
+                      "ngx_quic_create_key label \"%s\" too long", label);
+        return NGX_ERROR;
+    }
+
+    p = ngx_cpymem(&info[3], label, info[2]);
+    *p = '\0';
+
+    if (ngx_hkdf_expand(out, len, digest, is, is_len, info, info_len) != NGX_OK)
+    {
+        ngx_ssl_error(NGX_LOG_INFO, log, 0,
+                      "ngx_hkdf_expand(%s) failed", label);
+        return NGX_ERROR;
+    }
+
+    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;
+}
+
+
+void
+ngx_quic_compute_nonce(u_char *nonce, size_t len, uint64_t pn)
+{
+    nonce[len - 8] ^= (pn >> 56) & 0x3f;
+    nonce[len - 7] ^= (pn >> 48) & 0xff;
+    nonce[len - 6] ^= (pn >> 40) & 0xff;
+    nonce[len - 5] ^= (pn >> 32) & 0xff;
+    nonce[len - 4] ^= (pn >> 24) & 0xff;
+    nonce[len - 3] ^= (pn >> 16) & 0xff;
+    nonce[len - 2] ^= (pn >> 8) & 0xff;
+    nonce[len - 1] ^= pn & 0xff;
+}
+
+
+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;
+    ngx_str_t           in, ad;
+    ngx_uint_t          key_phase;
+    ngx_quic_secret_t  *secret;
+    uint8_t             nonce[NGX_QUIC_IV_LEN], mask[NGX_QUIC_HP_LEN];
+
+    secret = &pkt->keys->secrets[pkt->level].client;
+
+    p = pkt->raw->pos;
+    len = pkt->data + pkt->len - p;
+
+    /*
+     * RFC 9001, 5.4.2. Header Protection Sample
+     *           5.4.3. AES-Based Header Protection
+     *           5.4.4. ChaCha20-Based Header Protection
+     *
+     * the Packet Number field is assumed to be 4 bytes long
+     * AES and ChaCha20 algorithms sample 16 bytes
+     */
+
+    if (len < NGX_QUIC_TAG_LEN + 4) {
+        return NGX_DECLINED;
+    }
+
+    sample = p + 4;
+
+    /* header protection */
+
+    if (ngx_quic_crypto_hp(secret, mask, sample, pkt->log) != 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) {
+            if (pkt->keys->next_key.client.ctx != NULL) {
+                secret = &pkt->keys->next_key.client;
+                pkt->key_update = 1;
+
+            } else {
+                /*
+                 * RFC 9001,  6.3. Timing of Receive Key Generation.
+                 *
+                 * Trial decryption to avoid timing side-channel.
+                 */
+                ngx_log_debug0(NGX_LOG_DEBUG_EVENT, pkt->log, 0,
+                               "quic next key missing");
+            }
+        }
+    }
+
+    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_log_debug2(NGX_LOG_DEBUG_EVENT, pkt->log, 0,
+                   "quic ad len:%uz %xV", ad.len, &ad);
+#endif
+
+    pkt->payload.len = in.len - NGX_QUIC_TAG_LEN;
+    pkt->payload.data = pkt->plaintext + ad.len;
+
+    if (ngx_quic_crypto_open(secret, &pkt->payload, nonce, &in, &ad, pkt->log)
+        != NGX_OK)
+    {
+        return NGX_DECLINED;
+    }
+
+    if (pkt->payload.len == 0) {
+        /*
+         * RFC 9000, 12.4.  Frames and Frame Types
+         *
+         * 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)) {
+        /*
+         * RFC 9000, Reserved Bits
+         *
+         * 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_log_debug2(NGX_LOG_DEBUG_EVENT, pkt->log, 0,
+                   "quic packet payload len:%uz %xV",
+                   pkt->payload.len, &pkt->payload);
+#endif
+
+    *largest_pn = lpn;
+
+    return NGX_OK;
+}
diff --git a/src/event/quic/ngx_event_quic_protection.h b/src/event/quic/ngx_event_quic_protection.h
new file mode 100644
index 0000000..34cfee6
--- /dev/null
+++ b/src/event/quic/ngx_event_quic_protection.h
@@ -0,0 +1,120 @@
+
+/*
+ * 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>
+
+#include <ngx_event_quic_transport.h>
+
+
+#define NGX_QUIC_ENCRYPTION_LAST  ((ssl_encryption_application) + 1)
+
+/* RFC 5116, 5.1/5.3 and RFC 8439, 2.3/2.5 for all supported ciphers */
+#define NGX_QUIC_IV_LEN               12
+#define NGX_QUIC_TAG_LEN              16
+
+/* largest hash used in TLS is SHA-384 */
+#define NGX_QUIC_MAX_MD_SIZE          48
+
+
+#ifdef OPENSSL_IS_BORINGSSL
+#define ngx_quic_cipher_t             EVP_AEAD
+#define ngx_quic_crypto_ctx_t         EVP_AEAD_CTX
+#else
+#define ngx_quic_cipher_t             EVP_CIPHER
+#define ngx_quic_crypto_ctx_t         EVP_CIPHER_CTX
+#endif
+
+
+typedef struct {
+    size_t                    len;
+    u_char                    data[NGX_QUIC_MAX_MD_SIZE];
+} ngx_quic_md_t;
+
+
+typedef struct {
+    size_t                    len;
+    u_char                    data[NGX_QUIC_IV_LEN];
+} ngx_quic_iv_t;
+
+
+typedef struct {
+    ngx_quic_md_t             secret;
+    ngx_quic_iv_t             iv;
+    ngx_quic_md_t             hp;
+    ngx_quic_crypto_ctx_t    *ctx;
+    EVP_CIPHER_CTX           *hp_ctx;
+} 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;
+};
+
+
+typedef struct {
+    const ngx_quic_cipher_t  *c;
+    const EVP_CIPHER         *hp;
+    const EVP_MD             *d;
+} ngx_quic_ciphers_t;
+
+
+typedef struct {
+    size_t                    out_len;
+    u_char                   *out;
+
+    size_t                    prk_len;
+    const uint8_t            *prk;
+
+    size_t                    label_len;
+    const u_char             *label;
+} ngx_quic_hkdf_t;
+
+#define ngx_quic_hkdf_set(seq, _label, _out, _prk)                            \
+    (seq)->out_len = (_out)->len; (seq)->out = (_out)->data;                  \
+    (seq)->prk_len = (_prk)->len, (seq)->prk = (_prk)->data,                  \
+    (seq)->label_len = (sizeof(_label) - 1); (seq)->label = (u_char *)(_label);
+
+
+ngx_int_t ngx_quic_keys_set_initial_secret(ngx_quic_keys_t *keys,
+    ngx_str_t *secret, ngx_log_t *log);
+ngx_int_t ngx_quic_keys_set_encryption_secret(ngx_log_t *log,
+    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, ngx_uint_t is_write);
+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);
+void ngx_quic_keys_update(ngx_event_t *ev);
+void ngx_quic_keys_cleanup(ngx_quic_keys_t *keys);
+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);
+void ngx_quic_compute_nonce(u_char *nonce, size_t len, uint64_t pn);
+ngx_int_t ngx_quic_ciphers(ngx_uint_t id, ngx_quic_ciphers_t *ciphers);
+ngx_int_t ngx_quic_crypto_init(const ngx_quic_cipher_t *cipher,
+    ngx_quic_secret_t *s, ngx_quic_md_t *key, ngx_int_t enc, ngx_log_t *log);
+ngx_int_t ngx_quic_crypto_seal(ngx_quic_secret_t *s, ngx_str_t *out,
+    u_char *nonce, ngx_str_t *in, ngx_str_t *ad, ngx_log_t *log);
+void ngx_quic_crypto_cleanup(ngx_quic_secret_t *s);
+ngx_int_t ngx_quic_hkdf_expand(ngx_quic_hkdf_t *hkdf, const EVP_MD *digest,
+    ngx_log_t *log);
+
+
+#endif /* _NGX_EVENT_QUIC_PROTECTION_H_INCLUDED_ */
diff --git a/src/event/quic/ngx_event_quic_socket.c b/src/event/quic/ngx_event_quic_socket.c
new file mode 100644
index 0000000..c2bc822
--- /dev/null
+++ b/src/event/quic/ngx_event_quic_socket.c
@@ -0,0 +1,237 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_event.h>
+#include <ngx_event_quic_connection.h>
+
+
+ngx_int_t
+ngx_quic_open_sockets(ngx_connection_t *c, ngx_quic_connection_t *qc,
+    ngx_quic_header_t *pkt)
+{
+    ngx_quic_socket_t     *qsock, *tmp;
+    ngx_quic_client_id_t  *cid;
+
+    /*
+     * qc->path = NULL
+     *
+     * qc->nclient_ids = 0
+     * qc->nsockets = 0
+     * qc->max_retired_seqnum = 0
+     * qc->client_seqnum = 0
+     */
+
+    ngx_queue_init(&qc->sockets);
+    ngx_queue_init(&qc->free_sockets);
+
+    ngx_queue_init(&qc->paths);
+    ngx_queue_init(&qc->free_paths);
+
+    ngx_queue_init(&qc->client_ids);
+    ngx_queue_init(&qc->free_client_ids);
+
+    qc->tp.original_dcid.len = pkt->odcid.len;
+    qc->tp.original_dcid.data = ngx_pstrdup(c->pool, &pkt->odcid);
+    if (qc->tp.original_dcid.data == NULL) {
+        return NGX_ERROR;
+    }
+
+    /* socket to use for further processing (id auto-generated) */
+    qsock = ngx_quic_create_socket(c, qc);
+    if (qsock == NULL) {
+        return NGX_ERROR;
+    }
+
+    /* socket is listening at new server id */
+    if (ngx_quic_listen(c, qc, qsock) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    qsock->used = 1;
+
+    qc->tp.initial_scid.len = qsock->sid.len;
+    qc->tp.initial_scid.data = ngx_pnalloc(c->pool, qsock->sid.len);
+    if (qc->tp.initial_scid.data == NULL) {
+        goto failed;
+    }
+    ngx_memcpy(qc->tp.initial_scid.data, qsock->sid.id, qsock->sid.len);
+
+    /* for all packets except first, this is set at udp layer */
+    c->udp = &qsock->udp;
+
+    /* ngx_quic_get_connection(c) macro is now usable */
+
+    /* we have a client identified by scid */
+    cid = ngx_quic_create_client_id(c, &pkt->scid, 0, NULL);
+    if (cid == NULL) {
+        goto failed;
+    }
+
+    /* path of the first packet is our initial active path */
+    qc->path = ngx_quic_new_path(c, c->sockaddr, c->socklen, cid);
+    if (qc->path == NULL) {
+        goto failed;
+    }
+
+    qc->path->tag = NGX_QUIC_PATH_ACTIVE;
+
+    if (pkt->validated) {
+        qc->path->validated = 1;
+    }
+
+    ngx_quic_path_dbg(c, "set active", qc->path);
+
+    tmp = ngx_pcalloc(c->pool, sizeof(ngx_quic_socket_t));
+    if (tmp == NULL) {
+        goto failed;
+    }
+
+    tmp->sid.seqnum = NGX_QUIC_UNSET_PN; /* temporary socket */
+
+    ngx_memcpy(tmp->sid.id, pkt->dcid.data, pkt->dcid.len);
+    tmp->sid.len = pkt->dcid.len;
+
+    if (ngx_quic_listen(c, qc, tmp) != NGX_OK) {
+        goto failed;
+    }
+
+    return NGX_OK;
+
+failed:
+
+    ngx_rbtree_delete(&c->listening->rbtree, &qsock->udp.node);
+    c->udp = NULL;
+
+    return NGX_ERROR;
+}
+
+
+ngx_quic_socket_t *
+ngx_quic_create_socket(ngx_connection_t *c, ngx_quic_connection_t *qc)
+{
+    ngx_queue_t        *q;
+    ngx_quic_socket_t  *sock;
+
+    if (!ngx_queue_empty(&qc->free_sockets)) {
+
+        q = ngx_queue_head(&qc->free_sockets);
+        sock = ngx_queue_data(q, ngx_quic_socket_t, queue);
+
+        ngx_queue_remove(&sock->queue);
+
+        ngx_memzero(sock, sizeof(ngx_quic_socket_t));
+
+    } else {
+
+        sock = ngx_pcalloc(c->pool, sizeof(ngx_quic_socket_t));
+        if (sock == NULL) {
+            return NULL;
+        }
+    }
+
+    sock->sid.len = NGX_QUIC_SERVER_CID_LEN;
+    if (ngx_quic_create_server_id(c, sock->sid.id) != NGX_OK) {
+        return NULL;
+    }
+
+    sock->sid.seqnum = qc->server_seqnum++;
+
+    return sock;
+}
+
+
+void
+ngx_quic_close_socket(ngx_connection_t *c, ngx_quic_socket_t *qsock)
+{
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    ngx_queue_remove(&qsock->queue);
+    ngx_queue_insert_head(&qc->free_sockets, &qsock->queue);
+
+    ngx_rbtree_delete(&c->listening->rbtree, &qsock->udp.node);
+    qc->nsockets--;
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic socket seq:%L closed nsock:%ui",
+                   (int64_t) qsock->sid.seqnum, qc->nsockets);
+}
+
+
+ngx_int_t
+ngx_quic_listen(ngx_connection_t *c, ngx_quic_connection_t *qc,
+    ngx_quic_socket_t *qsock)
+{
+    ngx_str_t              id;
+    ngx_quic_server_id_t  *sid;
+
+    sid = &qsock->sid;
+
+    id.data = sid->id;
+    id.len = sid->len;
+
+    qsock->udp.connection = c;
+    qsock->udp.node.key = ngx_crc32_long(id.data, id.len);
+    qsock->udp.key = id;
+
+    ngx_rbtree_insert(&c->listening->rbtree, &qsock->udp.node);
+
+    ngx_queue_insert_tail(&qc->sockets, &qsock->queue);
+
+    qc->nsockets++;
+    qsock->quic = qc;
+
+    ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic socket seq:%L listening at sid:%xV nsock:%ui",
+                   (int64_t) sid->seqnum, &id, qc->nsockets);
+
+    return NGX_OK;
+}
+
+
+void
+ngx_quic_close_sockets(ngx_connection_t *c)
+{
+    ngx_queue_t            *q;
+    ngx_quic_socket_t      *qsock;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    while (!ngx_queue_empty(&qc->sockets)) {
+        q = ngx_queue_head(&qc->sockets);
+        qsock = ngx_queue_data(q, ngx_quic_socket_t, queue);
+
+        ngx_quic_close_socket(c, qsock);
+    }
+}
+
+
+ngx_quic_socket_t *
+ngx_quic_find_socket(ngx_connection_t *c, uint64_t seqnum)
+{
+    ngx_queue_t            *q;
+    ngx_quic_socket_t      *qsock;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    for (q = ngx_queue_head(&qc->sockets);
+         q != ngx_queue_sentinel(&qc->sockets);
+         q = ngx_queue_next(q))
+    {
+        qsock = ngx_queue_data(q, ngx_quic_socket_t, queue);
+
+        if (qsock->sid.seqnum == seqnum) {
+            return qsock;
+        }
+    }
+
+    return NULL;
+}
diff --git a/src/event/quic/ngx_event_quic_socket.h b/src/event/quic/ngx_event_quic_socket.h
new file mode 100644
index 0000000..68ecc06
--- /dev/null
+++ b/src/event/quic/ngx_event_quic_socket.h
@@ -0,0 +1,28 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_EVENT_QUIC_SOCKET_H_INCLUDED_
+#define _NGX_EVENT_QUIC_SOCKET_H_INCLUDED_
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+
+ngx_int_t ngx_quic_open_sockets(ngx_connection_t *c,
+    ngx_quic_connection_t *qc, ngx_quic_header_t *pkt);
+void ngx_quic_close_sockets(ngx_connection_t *c);
+
+ngx_quic_socket_t *ngx_quic_create_socket(ngx_connection_t *c,
+    ngx_quic_connection_t *qc);
+ngx_int_t ngx_quic_listen(ngx_connection_t *c, ngx_quic_connection_t *qc,
+    ngx_quic_socket_t *qsock);
+void ngx_quic_close_socket(ngx_connection_t *c, ngx_quic_socket_t *qsock);
+
+ngx_quic_socket_t *ngx_quic_find_socket(ngx_connection_t *c, uint64_t seqnum);
+
+
+#endif /* _NGX_EVENT_QUIC_SOCKET_H_INCLUDED_ */
diff --git a/src/event/quic/ngx_event_quic_ssl.c b/src/event/quic/ngx_event_quic_ssl.c
new file mode 100644
index 0000000..ba0b592
--- /dev/null
+++ b/src/event/quic/ngx_event_quic_ssl.c
@@ -0,0 +1,595 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_event.h>
+#include <ngx_event_quic_connection.h>
+
+
+#if defined OPENSSL_IS_BORINGSSL                                              \
+    || defined LIBRESSL_VERSION_NUMBER                                        \
+    || NGX_QUIC_OPENSSL_COMPAT
+#define NGX_QUIC_BORINGSSL_API   1
+#endif
+
+
+/*
+ * RFC 9000, 7.5.  Cryptographic Message Buffering
+ *
+ * Implementations MUST support buffering at least 4096 bytes of data
+ */
+#define NGX_QUIC_MAX_BUFFERED    65535
+
+
+#if (NGX_QUIC_BORINGSSL_API)
+static int ngx_quic_set_read_secret(ngx_ssl_conn_t *ssl_conn,
+    enum ssl_encryption_level_t level, const SSL_CIPHER *cipher,
+    const uint8_t *secret, size_t secret_len);
+static int ngx_quic_set_write_secret(ngx_ssl_conn_t *ssl_conn,
+    enum ssl_encryption_level_t level, const SSL_CIPHER *cipher,
+    const uint8_t *secret, size_t secret_len);
+#else
+static int ngx_quic_set_encryption_secrets(ngx_ssl_conn_t *ssl_conn,
+    enum ssl_encryption_level_t level, const uint8_t *read_secret,
+    const uint8_t *write_secret, size_t secret_len);
+#endif
+
+static int ngx_quic_add_handshake_data(ngx_ssl_conn_t *ssl_conn,
+    enum ssl_encryption_level_t level, const uint8_t *data, size_t len);
+static int ngx_quic_flush_flight(ngx_ssl_conn_t *ssl_conn);
+static int ngx_quic_send_alert(ngx_ssl_conn_t *ssl_conn,
+    enum ssl_encryption_level_t level, uint8_t alert);
+static ngx_int_t ngx_quic_crypto_input(ngx_connection_t *c, ngx_chain_t *data,
+    enum ssl_encryption_level_t level);
+
+
+#if (NGX_QUIC_BORINGSSL_API)
+
+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_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic read secret len:%uz %*xs", secret_len,
+                   secret_len, rsecret);
+#endif
+
+    if (ngx_quic_keys_set_encryption_secret(c->log, 0, qc->keys, level,
+                                            cipher, rsecret, secret_len)
+        != NGX_OK)
+    {
+        return 0;
+    }
+
+    return 1;
+}
+
+
+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_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic write secret len:%uz %*xs", secret_len,
+                   secret_len, wsecret);
+#endif
+
+    if (ngx_quic_keys_set_encryption_secret(c->log, 1, qc->keys, level,
+                                            cipher, wsecret, secret_len)
+        != NGX_OK)
+    {
+        return 0;
+    }
+
+    return 1;
+}
+
+#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_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic read secret len:%uz %*xs", secret_len,
+                   secret_len, rsecret);
+#endif
+
+    cipher = SSL_get_current_cipher(ssl_conn);
+
+    if (ngx_quic_keys_set_encryption_secret(c->log, 0, qc->keys, level,
+                                            cipher, rsecret, secret_len)
+        != NGX_OK)
+    {
+        return 0;
+    }
+
+    if (level == ssl_encryption_early_data) {
+        return 1;
+    }
+
+#ifdef NGX_QUIC_DEBUG_CRYPTO
+    ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic write secret len:%uz %*xs", secret_len,
+                   secret_len, wsecret);
+#endif
+
+    if (ngx_quic_keys_set_encryption_secret(c->log, 1, qc->keys, level,
+                                            cipher, wsecret, secret_len)
+        != NGX_OK)
+    {
+        return 0;
+    }
+
+    return 1;
+}
+
+#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;
+    ngx_chain_t            *out;
+    const uint8_t          *client_params;
+    ngx_quic_tp_t           ctp;
+    ngx_quic_frame_t       *frame;
+    ngx_connection_t       *c;
+    ngx_quic_send_ctx_t    *ctx;
+    ngx_quic_connection_t  *qc;
+#if defined(TLSEXT_TYPE_application_layer_protocol_negotiation)
+    unsigned int            alpn_len;
+    const unsigned char    *alpn_data;
+#endif
+
+    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)
+
+        SSL_get0_alpn_selected(ssl_conn, &alpn_data, &alpn_len);
+
+        if (alpn_len == 0) {
+            qc->error = NGX_QUIC_ERR_CRYPTO(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) {
+            /* RFC 9001, 8.2.  QUIC Transport Parameters Extension */
+            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;
+
+        /* defaults for parameters not sent by client */
+        ngx_memcpy(&ctp, &qc->ctp, sizeof(ngx_quic_tp_t));
+
+        if (ngx_quic_parse_transport_params(p, end, &ctp, c->log)
+            != NGX_OK)
+        {
+            qc->error = NGX_QUIC_ERR_TRANSPORT_PARAMETER_ERROR;
+            qc->error_reason = "failed to process transport parameters";
+
+            return 0;
+        }
+
+        if (ngx_quic_apply_transport_params(c, &ctp) != NGX_OK) {
+            return 0;
+        }
+
+        qc->client_tp_done = 1;
+    }
+
+    ctx = ngx_quic_get_send_ctx(qc, level);
+
+    out = ngx_quic_copy_buffer(c, (u_char *) data, len);
+    if (out == NGX_CHAIN_ERROR) {
+        return 0;
+    }
+
+    frame = ngx_quic_alloc_frame(c);
+    if (frame == NULL) {
+        return 0;
+    }
+
+    frame->data = out;
+    frame->level = level;
+    frame->type = NGX_QUIC_FT_CRYPTO;
+    frame->u.crypto.offset = ctx->crypto_sent;
+    frame->u.crypto.length = len;
+
+    ctx->crypto_sent += len;
+
+    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() level:%s alert:%d",
+                   ngx_quic_level_name(level), (int) alert);
+
+    /* already closed on regular shutdown */
+
+    qc = ngx_quic_get_connection(c);
+    if (qc == NULL) {
+        return 1;
+    }
+
+    qc->error = NGX_QUIC_ERR_CRYPTO(alert);
+    qc->error_reason = "handshake failed";
+
+    return 1;
+}
+
+
+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_chain_t              *cl;
+    ngx_quic_send_ctx_t      *ctx;
+    ngx_quic_connection_t    *qc;
+    ngx_quic_crypto_frame_t  *f;
+
+    qc = ngx_quic_get_connection(c);
+
+    if (!ngx_quic_keys_available(qc->keys, pkt->level, 0)) {
+        return NGX_OK;
+    }
+
+    ctx = ngx_quic_get_send_ctx(qc, pkt->level);
+    f = &frame->u.crypto;
+
+    /* no overflow since both values are 62-bit */
+    last = f->offset + f->length;
+
+    if (last > ctx->crypto.offset + NGX_QUIC_MAX_BUFFERED) {
+        qc->error = NGX_QUIC_ERR_CRYPTO_BUFFER_EXCEEDED;
+        return NGX_ERROR;
+    }
+
+    if (last <= ctx->crypto.offset) {
+        if (pkt->level == ssl_encryption_initial) {
+            /* speeding up handshake completion */
+
+            if (!ngx_queue_empty(&ctx->sent)) {
+                ngx_quic_resend_frames(c, ctx);
+
+                ctx = ngx_quic_get_send_ctx(qc, ssl_encryption_handshake);
+                while (!ngx_queue_empty(&ctx->sent)) {
+                    ngx_quic_resend_frames(c, ctx);
+                }
+            }
+        }
+
+        return NGX_OK;
+    }
+
+    if (f->offset == ctx->crypto.offset) {
+        if (ngx_quic_crypto_input(c, frame->data, pkt->level) != NGX_OK) {
+            return NGX_ERROR;
+        }
+
+        ngx_quic_skip_buffer(c, &ctx->crypto, last);
+
+    } else {
+        if (ngx_quic_write_buffer(c, &ctx->crypto, frame->data, f->length,
+                                  f->offset)
+            == NGX_CHAIN_ERROR)
+        {
+            return NGX_ERROR;
+        }
+    }
+
+    cl = ngx_quic_read_buffer(c, &ctx->crypto, (uint64_t) -1);
+
+    if (cl) {
+        if (ngx_quic_crypto_input(c, cl, pkt->level) != NGX_OK) {
+            return NGX_ERROR;
+        }
+
+        ngx_quic_free_chain(c, cl);
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_crypto_input(ngx_connection_t *c, ngx_chain_t *data,
+    enum ssl_encryption_level_t level)
+{
+    int                     n, sslerr;
+    ngx_buf_t              *b;
+    ngx_chain_t            *cl;
+    ngx_ssl_conn_t         *ssl_conn;
+    ngx_quic_frame_t       *frame;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    ssl_conn = c->ssl->connection;
+
+    for (cl = data; cl; cl = cl->next) {
+        b = cl->buf;
+
+        if (!SSL_provide_quic_data(ssl_conn, level, b->pos, b->last - b->pos)) {
+            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_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) {
+
+            if (c->ssl->handshake_rejected) {
+                ngx_connection_error(c, 0, "handshake rejected");
+                ERR_clear_error();
+
+                return NGX_ERROR;
+            }
+
+            ngx_ssl_error(NGX_LOG_ERR, c->log, 0, "SSL_do_handshake() failed");
+            return NGX_ERROR;
+        }
+    }
+
+    if (n <= 0 || SSL_in_init(ssl_conn)) {
+        if (ngx_quic_keys_available(qc->keys, ssl_encryption_early_data, 0)
+            && qc->client_tp_done)
+        {
+            if (ngx_quic_init_streams(c) != NGX_OK) {
+                return NGX_ERROR;
+            }
+        }
+
+        return NGX_OK;
+    }
+
+#if (NGX_DEBUG)
+    ngx_ssl_handshake_log(c);
+#endif
+
+    c->ssl->handshaked = 1;
+
+    frame = ngx_quic_alloc_frame(c);
+    if (frame == NULL) {
+        return NGX_ERROR;
+    }
+
+    frame->level = ssl_encryption_application;
+    frame->type = NGX_QUIC_FT_HANDSHAKE_DONE;
+    ngx_quic_queue_frame(qc, frame);
+
+    if (qc->conf->retry) {
+        if (ngx_quic_send_new_token(c, qc->path) != NGX_OK) {
+            return NGX_ERROR;
+        }
+    }
+
+    /*
+     * RFC 9001, 9.5.  Header Protection Timing Side Channels
+     *
+     * Generating next keys before a key update is received.
+     */
+
+    ngx_post_event(&qc->key_update, &ngx_posted_events);
+
+    /*
+     * RFC 9001, 4.9.2.  Discarding Handshake Keys
+     *
+     * An endpoint MUST discard its Handshake keys
+     * when the TLS handshake is confirmed.
+     */
+    ngx_quic_discard_ctx(c, ssl_encryption_handshake);
+
+    ngx_quic_discover_path_mtu(c, qc->path);
+
+    /* start accepting clients on negotiated number of server ids */
+    if (ngx_quic_create_sockets(c) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    if (ngx_quic_init_streams(c) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_quic_init_connection(ngx_connection_t *c)
+{
+    u_char                  *p;
+    size_t                   clen;
+    ssize_t                  len;
+    ngx_str_t                dcid;
+    ngx_ssl_conn_t          *ssl_conn;
+    ngx_quic_socket_t       *qsock;
+    ngx_quic_connection_t   *qc;
+    static SSL_QUIC_METHOD   quic_method;
+
+    qc = ngx_quic_get_connection(c);
+
+    if (ngx_ssl_create_connection(qc->conf->ssl, c, 0) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    c->ssl->no_wait_shutdown = 1;
+
+    ssl_conn = c->ssl->connection;
+
+    if (!quic_method.send_alert) {
+#if (NGX_QUIC_BORINGSSL_API)
+        quic_method.set_read_secret = ngx_quic_set_read_secret;
+        quic_method.set_write_secret = ngx_quic_set_write_secret;
+#else
+        quic_method.set_encryption_secrets = ngx_quic_set_encryption_secrets;
+#endif
+        quic_method.add_handshake_data = ngx_quic_add_handshake_data;
+        quic_method.flush_flight = ngx_quic_flush_flight;
+        quic_method.send_alert = ngx_quic_send_alert;
+    }
+
+    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 OPENSSL_INFO_QUIC
+    if (SSL_CTX_get_max_early_data(qc->conf->ssl->ctx)) {
+        SSL_set_quic_early_data_enabled(ssl_conn, 1);
+    }
+#endif
+
+    qsock = ngx_quic_get_socket(c);
+
+    dcid.data = qsock->sid.id;
+    dcid.len = qsock->sid.len;
+
+    if (ngx_quic_new_sr_token(c, &dcid, qc->conf->sr_token_key, qc->tp.sr_token)
+        != NGX_OK)
+    {
+        return NGX_ERROR;
+    }
+
+    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_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic transport parameters len:%uz %*xs", len, len, p);
+#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;
+    }
+
+#ifdef OPENSSL_IS_BORINGSSL
+    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;
+}
diff --git a/src/event/quic/ngx_event_quic_ssl.h b/src/event/quic/ngx_event_quic_ssl.h
new file mode 100644
index 0000000..ee0aa07
--- /dev/null
+++ b/src/event/quic/ngx_event_quic_ssl.h
@@ -0,0 +1,19 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_EVENT_QUIC_SSL_H_INCLUDED_
+#define _NGX_EVENT_QUIC_SSL_H_INCLUDED_
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+ngx_int_t ngx_quic_init_connection(ngx_connection_t *c);
+
+ngx_int_t ngx_quic_handle_crypto_frame(ngx_connection_t *c,
+    ngx_quic_header_t *pkt, ngx_quic_frame_t *frame);
+
+#endif /* _NGX_EVENT_QUIC_SSL_H_INCLUDED_ */
diff --git a/src/event/quic/ngx_event_quic_streams.c b/src/event/quic/ngx_event_quic_streams.c
new file mode 100644
index 0000000..a9a21f5
--- /dev/null
+++ b/src/event/quic/ngx_event_quic_streams.c
@@ -0,0 +1,1828 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_event.h>
+#include <ngx_event_quic_connection.h>
+
+
+#define NGX_QUIC_STREAM_GONE     (void *) -1
+
+
+static ngx_int_t ngx_quic_do_reset_stream(ngx_quic_stream_t *qs,
+    ngx_uint_t err);
+static ngx_int_t ngx_quic_shutdown_stream_send(ngx_connection_t *c);
+static ngx_int_t ngx_quic_shutdown_stream_recv(ngx_connection_t *c);
+static ngx_quic_stream_t *ngx_quic_get_stream(ngx_connection_t *c, uint64_t id);
+static ngx_int_t ngx_quic_reject_stream(ngx_connection_t *c, uint64_t id);
+static void ngx_quic_init_stream_handler(ngx_event_t *ev);
+static void ngx_quic_init_streams_handler(ngx_connection_t *c);
+static ngx_int_t ngx_quic_do_init_streams(ngx_connection_t *c);
+static ngx_quic_stream_t *ngx_quic_create_stream(ngx_connection_t *c,
+    uint64_t id);
+static void ngx_quic_empty_handler(ngx_event_t *ev);
+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 ngx_int_t ngx_quic_stream_flush(ngx_quic_stream_t *qs);
+static void ngx_quic_stream_cleanup_handler(void *data);
+static ngx_int_t ngx_quic_close_stream(ngx_quic_stream_t *qs);
+static ngx_int_t ngx_quic_can_shutdown(ngx_connection_t *c);
+static ngx_int_t ngx_quic_control_flow(ngx_quic_stream_t *qs, uint64_t last);
+static ngx_int_t ngx_quic_update_flow(ngx_quic_stream_t *qs, uint64_t last);
+static ngx_int_t ngx_quic_update_max_stream_data(ngx_quic_stream_t *qs);
+static ngx_int_t ngx_quic_update_max_data(ngx_connection_t *c);
+static void ngx_quic_set_event(ngx_event_t *ev);
+
+
+ngx_connection_t *
+ngx_quic_open_stream(ngx_connection_t *c, ngx_uint_t bidi)
+{
+    uint64_t                id;
+    ngx_connection_t       *pc, *sc;
+    ngx_quic_stream_t      *qs;
+    ngx_quic_connection_t  *qc;
+
+    pc = c->quic ? c->quic->parent : c;
+    qc = ngx_quic_get_connection(pc);
+
+    if (qc->closing) {
+        return NULL;
+    }
+
+    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++;
+
+    } 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++;
+    }
+
+    qs = ngx_quic_create_stream(pc, id);
+    if (qs == NULL) {
+        return NULL;
+    }
+
+    sc = qs->connection;
+
+    sc->write->active = 1;
+    sc->write->ready = 1;
+
+    if (bidi) {
+        sc->read->active = 1;
+    }
+
+    return sc;
+}
+
+
+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);
+}
+
+
+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;
+}
+
+
+ngx_int_t
+ngx_quic_close_streams(ngx_connection_t *c, ngx_quic_connection_t *qc)
+{
+    ngx_pool_t         *pool;
+    ngx_queue_t        *q, posted_events;
+    ngx_rbtree_t       *tree;
+    ngx_connection_t   *sc;
+    ngx_rbtree_node_t  *node;
+    ngx_quic_stream_t  *qs;
+
+    while (!ngx_queue_empty(&qc->streams.uninitialized)) {
+        q = ngx_queue_head(&qc->streams.uninitialized);
+        ngx_queue_remove(q);
+
+        qs = ngx_queue_data(q, ngx_quic_stream_t, queue);
+        pool = qs->connection->pool;
+
+        ngx_close_connection(qs->connection);
+        ngx_destroy_pool(pool);
+    }
+
+    tree = &qc->streams.tree;
+
+    if (tree->root == tree->sentinel) {
+        return NGX_OK;
+    }
+
+    ngx_queue_init(&posted_events);
+
+    node = ngx_rbtree_min(tree->root, tree->sentinel);
+
+    while (node) {
+        qs = (ngx_quic_stream_t *) node;
+        node = ngx_rbtree_next(tree, node);
+        sc = qs->connection;
+
+        qs->recv_state = NGX_QUIC_STREAM_RECV_RESET_RECVD;
+        qs->send_state = NGX_QUIC_STREAM_SEND_RESET_SENT;
+
+        if (sc == NULL) {
+            ngx_quic_close_stream(qs);
+            continue;
+        }
+
+        sc->read->error = 1;
+        sc->read->ready = 1;
+        sc->write->error = 1;
+        sc->write->ready = 1;
+
+        sc->close = 1;
+
+        if (sc->read->posted) {
+            ngx_delete_posted_event(sc->read);
+        }
+
+        ngx_post_event(sc->read, &posted_events);
+    }
+
+    ngx_event_process_posted((ngx_cycle_t *) ngx_cycle, &posted_events);
+
+    if (tree->root == tree->sentinel) {
+        return NGX_OK;
+    }
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic connection has active streams");
+
+    return NGX_AGAIN;
+}
+
+
+ngx_int_t
+ngx_quic_reset_stream(ngx_connection_t *c, ngx_uint_t err)
+{
+    return ngx_quic_do_reset_stream(c->quic, err);
+}
+
+
+static ngx_int_t
+ngx_quic_do_reset_stream(ngx_quic_stream_t *qs, ngx_uint_t err)
+{
+    ngx_connection_t       *pc;
+    ngx_quic_frame_t       *frame;
+    ngx_quic_connection_t  *qc;
+
+    if (qs->send_state == NGX_QUIC_STREAM_SEND_DATA_RECVD
+        || qs->send_state == NGX_QUIC_STREAM_SEND_RESET_SENT
+        || qs->send_state == NGX_QUIC_STREAM_SEND_RESET_RECVD)
+    {
+        return NGX_OK;
+    }
+
+    qs->send_state = NGX_QUIC_STREAM_SEND_RESET_SENT;
+    qs->send_final_size = qs->send_offset;
+
+    if (qs->connection) {
+        qs->connection->write->error = 1;
+    }
+
+    pc = qs->parent;
+    qc = ngx_quic_get_connection(pc);
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, pc->log, 0,
+                   "quic stream id:0x%xL reset", qs->id);
+
+    frame = ngx_quic_alloc_frame(pc);
+    if (frame == NULL) {
+        return NGX_ERROR;
+    }
+
+    frame->level = ssl_encryption_application;
+    frame->type = NGX_QUIC_FT_RESET_STREAM;
+    frame->u.reset_stream.id = qs->id;
+    frame->u.reset_stream.error_code = err;
+    frame->u.reset_stream.final_size = qs->send_offset;
+
+    ngx_quic_queue_frame(qc, frame);
+
+    ngx_quic_free_buffer(pc, &qs->send);
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_quic_shutdown_stream(ngx_connection_t *c, int how)
+{
+    if (how == NGX_RDWR_SHUTDOWN || how == NGX_WRITE_SHUTDOWN) {
+        if (ngx_quic_shutdown_stream_send(c) != NGX_OK) {
+            return NGX_ERROR;
+        }
+    }
+
+    if (how == NGX_RDWR_SHUTDOWN || how == NGX_READ_SHUTDOWN) {
+        if (ngx_quic_shutdown_stream_recv(c) != NGX_OK) {
+            return NGX_ERROR;
+        }
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_shutdown_stream_send(ngx_connection_t *c)
+{
+    ngx_quic_stream_t  *qs;
+
+    qs = c->quic;
+
+    if (qs->send_state != NGX_QUIC_STREAM_SEND_READY
+        && qs->send_state != NGX_QUIC_STREAM_SEND_SEND)
+    {
+        return NGX_OK;
+    }
+
+    qs->send_state = NGX_QUIC_STREAM_SEND_SEND;
+    qs->send_final_size = c->sent;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, qs->parent->log, 0,
+                   "quic stream id:0x%xL send shutdown", qs->id);
+
+    return ngx_quic_stream_flush(qs);
+}
+
+
+static ngx_int_t
+ngx_quic_shutdown_stream_recv(ngx_connection_t *c)
+{
+    ngx_connection_t       *pc;
+    ngx_quic_frame_t       *frame;
+    ngx_quic_stream_t      *qs;
+    ngx_quic_connection_t  *qc;
+
+    qs = c->quic;
+
+    if (qs->recv_state != NGX_QUIC_STREAM_RECV_RECV
+        && qs->recv_state != NGX_QUIC_STREAM_RECV_SIZE_KNOWN)
+    {
+        return NGX_OK;
+    }
+
+    pc = qs->parent;
+    qc = ngx_quic_get_connection(pc);
+
+    if (qc->conf->stream_close_code == 0) {
+        return NGX_OK;
+    }
+
+    frame = ngx_quic_alloc_frame(pc);
+    if (frame == NULL) {
+        return NGX_ERROR;
+    }
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, pc->log, 0,
+                   "quic stream id:0x%xL recv shutdown", qs->id);
+
+    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 = qc->conf->stream_close_code;
+
+    ngx_quic_queue_frame(qc, frame);
+
+    return NGX_OK;
+}
+
+
+static ngx_quic_stream_t *
+ngx_quic_get_stream(ngx_connection_t *c, uint64_t id)
+{
+    uint64_t                min_id;
+    ngx_event_t            *rev;
+    ngx_quic_stream_t      *qs;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    qs = ngx_quic_find_stream(&qc->streams.tree, id);
+
+    if (qs) {
+        return qs;
+    }
+
+    if (qc->shutdown || qc->closing) {
+        return NGX_QUIC_STREAM_GONE;
+    }
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic stream id:0x%xL is missing", id);
+
+    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;
+
+    } 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;
+    }
+
+    /*
+     * RFC 9000, 2.1.  Stream Types and Identifiers
+     *
+     * successive streams of each type 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.
+     */
+
+#if (NGX_SUPPRESS_WARN)
+    qs = NULL;
+#endif
+
+    for ( /* void */ ; min_id <= id; min_id += 0x04) {
+
+        qs = ngx_quic_create_stream(c, min_id);
+
+        if (qs == NULL) {
+            if (ngx_quic_reject_stream(c, min_id) != NGX_OK) {
+                return NULL;
+            }
+
+            continue;
+        }
+
+        ngx_queue_insert_tail(&qc->streams.uninitialized, &qs->queue);
+
+        rev = qs->connection->read;
+        rev->handler = ngx_quic_init_stream_handler;
+
+        if (qc->streams.initialized) {
+            ngx_post_event(rev, &ngx_posted_events);
+
+            if (qc->push.posted) {
+                /*
+                 * The posted stream can produce output immediately.
+                 * By postponing the push event, we coalesce the stream
+                 * output with queued frames in one UDP datagram.
+                 */
+
+                ngx_delete_posted_event(&qc->push);
+                ngx_post_event(&qc->push, &ngx_posted_events);
+            }
+        }
+    }
+
+    if (qs == NULL) {
+        return NGX_QUIC_STREAM_GONE;
+    }
+
+    return qs;
+}
+
+
+static ngx_int_t
+ngx_quic_reject_stream(ngx_connection_t *c, uint64_t id)
+{
+    uint64_t                code;
+    ngx_quic_frame_t       *frame;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    code = (id & NGX_QUIC_STREAM_UNIDIRECTIONAL)
+           ? qc->conf->stream_reject_code_uni
+           : qc->conf->stream_reject_code_bidi;
+
+    if (code == 0) {
+        return NGX_DECLINED;
+    }
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic stream id:0x%xL reject err:0x%xL", id, code);
+
+    frame = ngx_quic_alloc_frame(c);
+    if (frame == NULL) {
+        return NGX_ERROR;
+    }
+
+    frame->level = ssl_encryption_application;
+    frame->type = NGX_QUIC_FT_RESET_STREAM;
+    frame->u.reset_stream.id = id;
+    frame->u.reset_stream.error_code = code;
+    frame->u.reset_stream.final_size = 0;
+
+    ngx_quic_queue_frame(qc, frame);
+
+    frame = ngx_quic_alloc_frame(c);
+    if (frame == NULL) {
+        return NGX_ERROR;
+    }
+
+    frame->level = ssl_encryption_application;
+    frame->type = NGX_QUIC_FT_STOP_SENDING;
+    frame->u.stop_sending.id = id;
+    frame->u.stop_sending.error_code = code;
+
+    ngx_quic_queue_frame(qc, frame);
+
+    return NGX_OK;
+}
+
+
+static void
+ngx_quic_init_stream_handler(ngx_event_t *ev)
+{
+    ngx_connection_t   *c;
+    ngx_quic_stream_t  *qs;
+
+    c = ev->data;
+    qs = c->quic;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, "quic init stream");
+
+    if ((qs->id & NGX_QUIC_STREAM_UNIDIRECTIONAL) == 0) {
+        c->write->active = 1;
+        c->write->ready = 1;
+    }
+
+    c->read->active = 1;
+
+    ngx_queue_remove(&qs->queue);
+
+    c->listening->handler(c);
+}
+
+
+ngx_int_t
+ngx_quic_init_streams(ngx_connection_t *c)
+{
+    ngx_int_t               rc;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    if (qc->streams.initialized) {
+        return NGX_OK;
+    }
+
+    rc = ngx_ssl_ocsp_validate(c);
+
+    if (rc == NGX_ERROR) {
+        return NGX_ERROR;
+    }
+
+    if (rc == NGX_AGAIN) {
+        c->ssl->handler = ngx_quic_init_streams_handler;
+        return NGX_OK;
+    }
+
+    return ngx_quic_do_init_streams(c);
+}
+
+
+static void
+ngx_quic_init_streams_handler(ngx_connection_t *c)
+{
+    if (ngx_quic_do_init_streams(c) != NGX_OK) {
+        ngx_quic_close_connection(c, NGX_ERROR);
+    }
+}
+
+
+static ngx_int_t
+ngx_quic_do_init_streams(ngx_connection_t *c)
+{
+    ngx_queue_t            *q;
+    ngx_quic_stream_t      *qs;
+    ngx_quic_connection_t  *qc;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, "quic init streams");
+
+    qc = ngx_quic_get_connection(c);
+
+    if (qc->conf->init) {
+        if (qc->conf->init(c) != NGX_OK) {
+            return NGX_ERROR;
+        }
+    }
+
+    for (q = ngx_queue_head(&qc->streams.uninitialized);
+         q != ngx_queue_sentinel(&qc->streams.uninitialized);
+         q = ngx_queue_next(q))
+    {
+        qs = ngx_queue_data(q, ngx_quic_stream_t, queue);
+        ngx_post_event(qs->connection->read, &ngx_posted_events);
+    }
+
+    qc->streams.initialized = 1;
+
+    if (!qc->closing && qc->close.timer_set) {
+        ngx_del_timer(&qc->close);
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_quic_stream_t *
+ngx_quic_create_stream(ngx_connection_t *c, uint64_t id)
+{
+    ngx_str_t               addr_text;
+    ngx_log_t              *log;
+    ngx_pool_t             *pool;
+    ngx_uint_t              reusable;
+    ngx_queue_t            *q;
+    struct sockaddr        *sockaddr;
+    ngx_connection_t       *sc;
+    ngx_quic_stream_t      *qs;
+    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);
+
+    if (!ngx_queue_empty(&qc->streams.free)) {
+        q = ngx_queue_head(&qc->streams.free);
+        qs = ngx_queue_data(q, ngx_quic_stream_t, queue);
+        ngx_queue_remove(&qs->queue);
+
+    } else {
+        /*
+         * the number of streams is limited by transport
+         * parameters and application requirements
+         */
+
+        qs = ngx_palloc(c->pool, sizeof(ngx_quic_stream_t));
+        if (qs == NULL) {
+            return NULL;
+        }
+    }
+
+    ngx_memzero(qs, sizeof(ngx_quic_stream_t));
+
+    qs->node.key = id;
+    qs->parent = c;
+    qs->id = id;
+    qs->send_final_size = (uint64_t) -1;
+    qs->recv_final_size = (uint64_t) -1;
+
+    pool = ngx_create_pool(NGX_DEFAULT_POOL_SIZE, c->log);
+    if (pool == NULL) {
+        ngx_queue_insert_tail(&qc->streams.free, &qs->queue);
+        return NULL;
+    }
+
+    log = ngx_palloc(pool, sizeof(ngx_log_t));
+    if (log == NULL) {
+        ngx_destroy_pool(pool);
+        ngx_queue_insert_tail(&qc->streams.free, &qs->queue);
+        return NULL;
+    }
+
+    *log = *c->log;
+    pool->log = log;
+
+    sockaddr = ngx_palloc(pool, c->socklen);
+    if (sockaddr == NULL) {
+        ngx_destroy_pool(pool);
+        ngx_queue_insert_tail(&qc->streams.free, &qs->queue);
+        return NULL;
+    }
+
+    ngx_memcpy(sockaddr, c->sockaddr, c->socklen);
+
+    if (c->addr_text.data) {
+        addr_text.data = ngx_pnalloc(pool, c->addr_text.len);
+        if (addr_text.data == NULL) {
+            ngx_destroy_pool(pool);
+            ngx_queue_insert_tail(&qc->streams.free, &qs->queue);
+            return NULL;
+        }
+
+        ngx_memcpy(addr_text.data, c->addr_text.data, c->addr_text.len);
+        addr_text.len = c->addr_text.len;
+
+    } else {
+        addr_text.len = 0;
+        addr_text.data = NULL;
+    }
+
+    reusable = c->reusable;
+    ngx_reusable_connection(c, 0);
+
+    sc = ngx_get_connection(c->fd, log);
+    if (sc == NULL) {
+        ngx_destroy_pool(pool);
+        ngx_queue_insert_tail(&qc->streams.free, &qs->queue);
+        ngx_reusable_connection(c, reusable);
+        return NULL;
+    }
+
+    qs->connection = sc;
+
+    sc->quic = qs;
+    sc->shared = 1;
+    sc->type = SOCK_STREAM;
+    sc->pool = pool;
+    sc->ssl = c->ssl;
+    sc->sockaddr = sockaddr;
+    sc->socklen = c->socklen;
+    sc->listening = c->listening;
+    sc->addr_text = addr_text;
+    sc->local_sockaddr = c->local_sockaddr;
+    sc->local_socklen = c->local_socklen;
+    sc->number = ngx_atomic_fetch_add(ngx_connection_counter, 1);
+    sc->start_time = c->start_time;
+    sc->tcp_nodelay = NGX_TCP_NODELAY_DISABLED;
+
+    sc->recv = ngx_quic_stream_recv;
+    sc->send = ngx_quic_stream_send;
+    sc->send_chain = ngx_quic_stream_send_chain;
+
+    sc->read->log = log;
+    sc->write->log = log;
+
+    sc->read->handler = ngx_quic_empty_handler;
+    sc->write->handler = ngx_quic_empty_handler;
+
+    log->connection = sc->number;
+
+    if (id & NGX_QUIC_STREAM_UNIDIRECTIONAL) {
+        if (id & NGX_QUIC_STREAM_SERVER_INITIATED) {
+            qs->send_max_data = qc->ctp.initial_max_stream_data_uni;
+            qs->recv_state = NGX_QUIC_STREAM_RECV_DATA_READ;
+            qs->send_state = NGX_QUIC_STREAM_SEND_READY;
+
+        } else {
+            qs->recv_max_data = qc->tp.initial_max_stream_data_uni;
+            qs->recv_state = NGX_QUIC_STREAM_RECV_RECV;
+            qs->send_state = NGX_QUIC_STREAM_SEND_DATA_RECVD;
+        }
+
+    } else {
+        if (id & NGX_QUIC_STREAM_SERVER_INITIATED) {
+            qs->send_max_data = qc->ctp.initial_max_stream_data_bidi_remote;
+            qs->recv_max_data = qc->tp.initial_max_stream_data_bidi_local;
+
+        } else {
+            qs->send_max_data = qc->ctp.initial_max_stream_data_bidi_local;
+            qs->recv_max_data = qc->tp.initial_max_stream_data_bidi_remote;
+        }
+
+        qs->recv_state = NGX_QUIC_STREAM_RECV_RECV;
+        qs->send_state = NGX_QUIC_STREAM_SEND_READY;
+    }
+
+    qs->recv_window = qs->recv_max_data;
+
+    cln = ngx_pool_cleanup_add(pool, 0);
+    if (cln == NULL) {
+        ngx_close_connection(sc);
+        ngx_destroy_pool(pool);
+        ngx_queue_insert_tail(&qc->streams.free, &qs->queue);
+        ngx_reusable_connection(c, reusable);
+        return NULL;
+    }
+
+    cln->handler = ngx_quic_stream_cleanup_handler;
+    cln->data = sc;
+
+    ngx_rbtree_insert(&qc->streams.tree, &qs->node);
+
+    return qs;
+}
+
+
+void
+ngx_quic_cancelable_stream(ngx_connection_t *c)
+{
+    ngx_connection_t       *pc;
+    ngx_quic_stream_t      *qs;
+    ngx_quic_connection_t  *qc;
+
+    qs = c->quic;
+    pc = qs->parent;
+    qc = ngx_quic_get_connection(pc);
+
+    if (!qs->cancelable) {
+        qs->cancelable = 1;
+
+        if (ngx_quic_can_shutdown(pc) == NGX_OK) {
+            ngx_reusable_connection(pc, 1);
+
+            if (qc->shutdown) {
+                ngx_quic_shutdown_quic(pc);
+            }
+        }
+    }
+}
+
+
+static void
+ngx_quic_empty_handler(ngx_event_t *ev)
+{
+}
+
+
+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_chain_t        *cl, *in;
+    ngx_event_t        *rev;
+    ngx_connection_t   *pc;
+    ngx_quic_stream_t  *qs;
+
+    qs = c->quic;
+    pc = qs->parent;
+    rev = c->read;
+
+    if (qs->recv_state == NGX_QUIC_STREAM_RECV_RESET_RECVD
+        || qs->recv_state == NGX_QUIC_STREAM_RECV_RESET_READ)
+    {
+        qs->recv_state = NGX_QUIC_STREAM_RECV_RESET_READ;
+        return NGX_ERROR;
+    }
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, pc->log, 0,
+                   "quic stream id:0x%xL recv buf:%uz", qs->id, size);
+
+    if (size == 0) {
+        return 0;
+    }
+
+    in = ngx_quic_read_buffer(pc, &qs->recv, size);
+    if (in == NGX_CHAIN_ERROR) {
+        return NGX_ERROR;
+    }
+
+    len = 0;
+
+    for (cl = in; cl; cl = cl->next) {
+        b = cl->buf;
+        len += b->last - b->pos;
+        buf = ngx_cpymem(buf, b->pos, b->last - b->pos);
+    }
+
+    ngx_quic_free_chain(pc, in);
+
+    if (len == 0) {
+        rev->ready = 0;
+
+        if (qs->recv_state == NGX_QUIC_STREAM_RECV_DATA_RECVD
+            && qs->recv_offset == qs->recv_final_size)
+        {
+            qs->recv_state = NGX_QUIC_STREAM_RECV_DATA_READ;
+        }
+
+        if (qs->recv_state == NGX_QUIC_STREAM_RECV_DATA_READ) {
+            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;
+    }
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic stream id:0x%xL recv len:%z", qs->id, len);
+
+    if (ngx_quic_update_flow(qs, qs->recv_offset + len) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    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)
+{
+    uint64_t                n, flow;
+    ngx_event_t            *wev;
+    ngx_connection_t       *pc;
+    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 (qs->send_state != NGX_QUIC_STREAM_SEND_READY
+        && qs->send_state != NGX_QUIC_STREAM_SEND_SEND)
+    {
+        wev->error = 1;
+        return NGX_CHAIN_ERROR;
+    }
+
+    qs->send_state = NGX_QUIC_STREAM_SEND_SEND;
+
+    flow = qs->acked + qc->conf->stream_buffer_size - qs->sent;
+
+    if (flow == 0) {
+        wev->ready = 0;
+        return in;
+    }
+
+    if (limit == 0 || limit > (off_t) flow) {
+        limit = flow;
+    }
+
+    n = qs->send.size;
+
+    in = ngx_quic_write_buffer(pc, &qs->send, in, limit, qs->sent);
+    if (in == NGX_CHAIN_ERROR) {
+        return NGX_CHAIN_ERROR;
+    }
+
+    n = qs->send.size - n;
+    c->sent += n;
+    qs->sent += n;
+    qc->streams.sent += n;
+
+    if (flow == n) {
+        wev->ready = 0;
+    }
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic send_chain sent:%uL", n);
+
+    if (ngx_quic_stream_flush(qs) != NGX_OK) {
+        return NGX_CHAIN_ERROR;
+    }
+
+    return in;
+}
+
+
+static ngx_int_t
+ngx_quic_stream_flush(ngx_quic_stream_t *qs)
+{
+    off_t                   limit, len;
+    ngx_uint_t              last;
+    ngx_chain_t            *out;
+    ngx_quic_frame_t       *frame;
+    ngx_connection_t       *pc;
+    ngx_quic_connection_t  *qc;
+
+    if (qs->send_state != NGX_QUIC_STREAM_SEND_SEND) {
+        return NGX_OK;
+    }
+
+    pc = qs->parent;
+    qc = ngx_quic_get_connection(pc);
+
+    if (qc->streams.send_max_data == 0) {
+        qc->streams.send_max_data = qc->ctp.initial_max_data;
+    }
+
+    limit = ngx_min(qc->streams.send_max_data - qc->streams.send_offset,
+                    qs->send_max_data - qs->send_offset);
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, pc->log, 0,
+                   "quic stream id:0x%xL flush limit:%O", qs->id, limit);
+
+    len = qs->send.offset;
+
+    out = ngx_quic_read_buffer(pc, &qs->send, limit);
+    if (out == NGX_CHAIN_ERROR) {
+        return NGX_ERROR;
+    }
+
+    len = qs->send.offset - len;
+    last = 0;
+
+    if (qs->send_final_size != (uint64_t) -1
+        && qs->send_final_size == qs->send.offset)
+    {
+        qs->send_state = NGX_QUIC_STREAM_SEND_DATA_SENT;
+        last = 1;
+    }
+
+    if (len == 0 && !last) {
+        return NGX_OK;
+    }
+
+    frame = ngx_quic_alloc_frame(pc);
+    if (frame == NULL) {
+        return NGX_ERROR;
+    }
+
+    frame->level = ssl_encryption_application;
+    frame->type = NGX_QUIC_FT_STREAM;
+    frame->data = out;
+
+    frame->u.stream.off = 1;
+    frame->u.stream.len = 1;
+    frame->u.stream.fin = last;
+
+    frame->u.stream.stream_id = qs->id;
+    frame->u.stream.offset = qs->send_offset;
+    frame->u.stream.length = len;
+
+    ngx_quic_queue_frame(qc, frame);
+
+    qs->send_offset += len;
+    qc->streams.send_offset += len;
+
+    ngx_log_debug3(NGX_LOG_DEBUG_EVENT, pc->log, 0,
+                   "quic stream id:0x%xL flush len:%O last:%ui",
+                   qs->id, len, last);
+
+    if (qs->connection == NULL) {
+        return ngx_quic_close_stream(qs);
+    }
+
+    return NGX_OK;
+}
+
+
+static void
+ngx_quic_stream_cleanup_handler(void *data)
+{
+    ngx_connection_t *c = data;
+
+    ngx_quic_stream_t      *qs;
+    ngx_quic_connection_t  *qc;
+
+    qs = c->quic;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, qs->parent->log, 0,
+                   "quic stream id:0x%xL cleanup", qs->id);
+
+    if (ngx_quic_shutdown_stream(c, NGX_RDWR_SHUTDOWN) != NGX_OK) {
+        qs->connection = NULL;
+        goto failed;
+    }
+
+    qs->connection = NULL;
+
+    if (ngx_quic_close_stream(qs) != NGX_OK) {
+        goto failed;
+    }
+
+    return;
+
+failed:
+
+    qc = ngx_quic_get_connection(qs->parent);
+    qc->error = NGX_QUIC_ERR_INTERNAL_ERROR;
+
+    ngx_post_event(&qc->close, &ngx_posted_events);
+}
+
+
+static ngx_int_t
+ngx_quic_close_stream(ngx_quic_stream_t *qs)
+{
+    ngx_connection_t       *pc;
+    ngx_quic_frame_t       *frame;
+    ngx_quic_connection_t  *qc;
+
+    pc = qs->parent;
+    qc = ngx_quic_get_connection(pc);
+
+    if (!qc->closing) {
+        /* make sure everything is sent and final size is received */
+
+        if (qs->recv_state == NGX_QUIC_STREAM_RECV_RECV) {
+            return NGX_OK;
+        }
+
+        if (qs->send_state != NGX_QUIC_STREAM_SEND_DATA_RECVD
+            && qs->send_state != NGX_QUIC_STREAM_SEND_RESET_RECVD)
+        {
+            return NGX_OK;
+        }
+    }
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, pc->log, 0,
+                   "quic stream id:0x%xL close", qs->id);
+
+    ngx_quic_free_buffer(pc, &qs->send);
+    ngx_quic_free_buffer(pc, &qs->recv);
+
+    ngx_rbtree_delete(&qc->streams.tree, &qs->node);
+    ngx_queue_insert_tail(&qc->streams.free, &qs->queue);
+
+    if (qc->closing) {
+        /* schedule handler call to continue ngx_quic_close_connection() */
+        ngx_post_event(&qc->close, &ngx_posted_events);
+        return NGX_OK;
+    }
+
+    if (!pc->reusable && ngx_quic_can_shutdown(pc) == NGX_OK) {
+        ngx_reusable_connection(pc, 1);
+    }
+
+    if (qc->shutdown) {
+        ngx_quic_shutdown_quic(pc);
+        return NGX_OK;
+    }
+
+    if ((qs->id & NGX_QUIC_STREAM_SERVER_INITIATED) == 0) {
+        frame = ngx_quic_alloc_frame(pc);
+        if (frame == NULL) {
+            return NGX_ERROR;
+        }
+
+        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);
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_can_shutdown(ngx_connection_t *c)
+{
+    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 (tree->root != tree->sentinel) {
+        for (node = ngx_rbtree_min(tree->root, tree->sentinel);
+             node;
+             node = ngx_rbtree_next(tree, node))
+        {
+            qs = (ngx_quic_stream_t *) node;
+
+            if (!qs->cancelable) {
+                return NGX_DECLINED;
+            }
+        }
+    }
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_quic_handle_stream_frame(ngx_connection_t *c, ngx_quic_header_t *pkt,
+    ngx_quic_frame_t *frame)
+{
+    uint64_t                  last;
+    ngx_quic_stream_t        *qs;
+    ngx_quic_connection_t    *qc;
+    ngx_quic_stream_frame_t  *f;
+
+    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;
+
+    qs = ngx_quic_get_stream(c, f->stream_id);
+
+    if (qs == NULL) {
+        return NGX_ERROR;
+    }
+
+    if (qs == NGX_QUIC_STREAM_GONE) {
+        return NGX_OK;
+    }
+
+    if (qs->recv_state != NGX_QUIC_STREAM_RECV_RECV
+        && qs->recv_state != NGX_QUIC_STREAM_RECV_SIZE_KNOWN)
+    {
+        return NGX_OK;
+    }
+
+    if (ngx_quic_control_flow(qs, last) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    if (qs->recv_final_size != (uint64_t) -1 && last > qs->recv_final_size) {
+        qc->error = NGX_QUIC_ERR_FINAL_SIZE_ERROR;
+        return NGX_ERROR;
+    }
+
+    if (last < qs->recv_offset) {
+        return NGX_OK;
+    }
+
+    if (f->fin) {
+        if (qs->recv_final_size != (uint64_t) -1 && qs->recv_final_size != last)
+        {
+            qc->error = NGX_QUIC_ERR_FINAL_SIZE_ERROR;
+            return NGX_ERROR;
+        }
+
+        if (qs->recv_last > last) {
+            qc->error = NGX_QUIC_ERR_FINAL_SIZE_ERROR;
+            return NGX_ERROR;
+        }
+
+        qs->recv_final_size = last;
+        qs->recv_state = NGX_QUIC_STREAM_RECV_SIZE_KNOWN;
+    }
+
+    if (ngx_quic_write_buffer(c, &qs->recv, frame->data, f->length, f->offset)
+        == NGX_CHAIN_ERROR)
+    {
+        return NGX_ERROR;
+    }
+
+    if (qs->recv_state == NGX_QUIC_STREAM_RECV_SIZE_KNOWN
+        && qs->recv.size == qs->recv_final_size)
+    {
+        qs->recv_state = NGX_QUIC_STREAM_RECV_DATA_RECVD;
+    }
+
+    if (qs->connection == NULL) {
+        return ngx_quic_close_stream(qs);
+    }
+
+    if (f->offset <= qs->recv_offset) {
+        ngx_quic_set_event(qs->connection->read);
+    }
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_quic_handle_max_data_frame(ngx_connection_t *c,
+    ngx_quic_max_data_frame_t *f)
+{
+    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 (tree->root == tree->sentinel
+        || qc->streams.send_offset < qc->streams.send_max_data)
+    {
+        /* not blocked on MAX_DATA */
+        qc->streams.send_max_data = f->max_data;
+        return NGX_OK;
+    }
+
+    qc->streams.send_max_data = f->max_data;
+    node = ngx_rbtree_min(tree->root, tree->sentinel);
+
+    while (node && qc->streams.send_offset < qc->streams.send_max_data) {
+
+        qs = (ngx_quic_stream_t *) node;
+        node = ngx_rbtree_next(tree, node);
+
+        if (ngx_quic_stream_flush(qs) != NGX_OK) {
+            return NGX_ERROR;
+        }
+    }
+
+    return NGX_OK;
+}
+
+
+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;
+}
+
+
+ngx_int_t
+ngx_quic_handle_data_blocked_frame(ngx_connection_t *c,
+    ngx_quic_header_t *pkt, ngx_quic_data_blocked_frame_t *f)
+{
+    return ngx_quic_update_max_data(c);
+}
+
+
+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)
+{
+    ngx_quic_stream_t      *qs;
+    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;
+    }
+
+    qs = ngx_quic_get_stream(c, f->id);
+
+    if (qs == NULL) {
+        return NGX_ERROR;
+    }
+
+    if (qs == NGX_QUIC_STREAM_GONE) {
+        return NGX_OK;
+    }
+
+    return ngx_quic_update_max_stream_data(qs);
+}
+
+
+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)
+{
+    ngx_quic_stream_t      *qs;
+    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;
+    }
+
+    qs = ngx_quic_get_stream(c, f->id);
+
+    if (qs == NULL) {
+        return NGX_ERROR;
+    }
+
+    if (qs == NGX_QUIC_STREAM_GONE) {
+        return NGX_OK;
+    }
+
+    if (f->limit <= qs->send_max_data) {
+        return NGX_OK;
+    }
+
+    if (qs->send_offset < qs->send_max_data) {
+        /* not blocked on MAX_STREAM_DATA */
+        qs->send_max_data = f->limit;
+        return NGX_OK;
+    }
+
+    qs->send_max_data = f->limit;
+
+    return ngx_quic_stream_flush(qs);
+}
+
+
+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_quic_stream_t      *qs;
+    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;
+    }
+
+    qs = ngx_quic_get_stream(c, f->id);
+
+    if (qs == NULL) {
+        return NGX_ERROR;
+    }
+
+    if (qs == NGX_QUIC_STREAM_GONE) {
+        return NGX_OK;
+    }
+
+    if (qs->recv_state == NGX_QUIC_STREAM_RECV_RESET_RECVD
+        || qs->recv_state == NGX_QUIC_STREAM_RECV_RESET_READ)
+    {
+        return NGX_OK;
+    }
+
+    qs->recv_state = NGX_QUIC_STREAM_RECV_RESET_RECVD;
+
+    if (ngx_quic_control_flow(qs, f->final_size) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    if (qs->recv_final_size != (uint64_t) -1
+        && qs->recv_final_size != f->final_size)
+    {
+        qc->error = NGX_QUIC_ERR_FINAL_SIZE_ERROR;
+        return NGX_ERROR;
+    }
+
+    if (qs->recv_last > f->final_size) {
+        qc->error = NGX_QUIC_ERR_FINAL_SIZE_ERROR;
+        return NGX_ERROR;
+    }
+
+    qs->recv_final_size = f->final_size;
+
+    if (ngx_quic_update_flow(qs, qs->recv_final_size) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    if (qs->connection == NULL) {
+        return ngx_quic_close_stream(qs);
+    }
+
+    rev = qs->connection->read;
+    rev->error = 1;
+
+    ngx_quic_set_event(rev);
+
+    return NGX_OK;
+}
+
+
+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_quic_stream_t      *qs;
+    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;
+    }
+
+    qs = ngx_quic_get_stream(c, f->id);
+
+    if (qs == NULL) {
+        return NGX_ERROR;
+    }
+
+    if (qs == NGX_QUIC_STREAM_GONE) {
+        return NGX_OK;
+    }
+
+    if (ngx_quic_do_reset_stream(qs, f->error_code) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    if (qs->connection == NULL) {
+        return ngx_quic_close_stream(qs);
+    }
+
+    ngx_quic_set_event(qs->connection->write);
+
+    return NGX_OK;
+}
+
+
+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;
+}
+
+
+void
+ngx_quic_handle_stream_ack(ngx_connection_t *c, ngx_quic_frame_t *f)
+{
+    uint64_t                acked;
+    ngx_quic_stream_t      *qs;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    switch (f->type) {
+
+    case NGX_QUIC_FT_RESET_STREAM:
+
+        qs = ngx_quic_find_stream(&qc->streams.tree, f->u.reset_stream.id);
+        if (qs == NULL) {
+            return;
+        }
+
+        qs->send_state = NGX_QUIC_STREAM_SEND_RESET_RECVD;
+
+        ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic stream id:0x%xL ack reset final_size:%uL",
+                       qs->id, f->u.reset_stream.final_size);
+
+        break;
+
+    case NGX_QUIC_FT_STREAM:
+
+        qs = ngx_quic_find_stream(&qc->streams.tree, f->u.stream.stream_id);
+        if (qs == NULL) {
+            return;
+        }
+
+        acked = qs->acked;
+        qs->acked += f->u.stream.length;
+
+        if (f->u.stream.fin) {
+            qs->fin_acked = 1;
+        }
+
+        if (qs->send_state == NGX_QUIC_STREAM_SEND_DATA_SENT
+            && qs->acked == qs->sent && qs->fin_acked)
+        {
+            qs->send_state = NGX_QUIC_STREAM_SEND_DATA_RECVD;
+        }
+
+        ngx_log_debug4(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic stream id:0x%xL ack len:%uL fin:%d unacked:%uL",
+                       qs->id, f->u.stream.length, f->u.stream.fin,
+                       qs->sent - qs->acked);
+
+        if (qs->connection
+            && qs->sent - acked == qc->conf->stream_buffer_size
+            && f->u.stream.length > 0)
+        {
+            ngx_quic_set_event(qs->connection->write);
+        }
+
+        break;
+
+    default:
+        return;
+    }
+
+    if (qs->connection == NULL) {
+        ngx_quic_close_stream(qs);
+    }
+}
+
+
+static ngx_int_t
+ngx_quic_control_flow(ngx_quic_stream_t *qs, uint64_t last)
+{
+    uint64_t                len;
+    ngx_connection_t       *pc;
+    ngx_quic_connection_t  *qc;
+
+    pc = qs->parent;
+    qc = ngx_quic_get_connection(pc);
+
+    if (last <= qs->recv_last) {
+        return NGX_OK;
+    }
+
+    len = last - qs->recv_last;
+
+    ngx_log_debug5(NGX_LOG_DEBUG_EVENT, pc->log, 0,
+                   "quic stream id:0x%xL flow control msd:%uL/%uL md:%uL/%uL",
+                   qs->id, last, qs->recv_max_data, qc->streams.recv_last + len,
+                   qc->streams.recv_max_data);
+
+    qs->recv_last += len;
+
+    if (qs->recv_state == NGX_QUIC_STREAM_RECV_RECV
+        && qs->recv_last > qs->recv_max_data)
+    {
+        qc->error = NGX_QUIC_ERR_FLOW_CONTROL_ERROR;
+        return NGX_ERROR;
+    }
+
+    qc->streams.recv_last += len;
+
+    if (qc->streams.recv_last > qc->streams.recv_max_data) {
+        qc->error = NGX_QUIC_ERR_FLOW_CONTROL_ERROR;
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_update_flow(ngx_quic_stream_t *qs, uint64_t last)
+{
+    uint64_t                len;
+    ngx_connection_t       *pc;
+    ngx_quic_connection_t  *qc;
+
+    pc = qs->parent;
+    qc = ngx_quic_get_connection(pc);
+
+    if (last <= qs->recv_offset) {
+        return NGX_OK;
+    }
+
+    len = last - qs->recv_offset;
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, pc->log, 0,
+                   "quic stream id:0x%xL flow update %uL", qs->id, last);
+
+    qs->recv_offset += len;
+
+    if (qs->recv_max_data <= qs->recv_offset + qs->recv_window / 2) {
+        if (ngx_quic_update_max_stream_data(qs) != NGX_OK) {
+            return NGX_ERROR;
+        }
+    }
+
+    qc->streams.recv_offset += len;
+
+    if (qc->streams.recv_max_data
+        <= qc->streams.recv_offset + qc->streams.recv_window / 2)
+    {
+        if (ngx_quic_update_max_data(pc) != NGX_OK) {
+            return NGX_ERROR;
+        }
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_update_max_stream_data(ngx_quic_stream_t *qs)
+{
+    uint64_t                recv_max_data;
+    ngx_connection_t       *pc;
+    ngx_quic_frame_t       *frame;
+    ngx_quic_connection_t  *qc;
+
+    pc = qs->parent;
+    qc = ngx_quic_get_connection(pc);
+
+    if (qs->recv_state != NGX_QUIC_STREAM_RECV_RECV) {
+        return NGX_OK;
+    }
+
+    recv_max_data = qs->recv_offset + qs->recv_window;
+
+    if (qs->recv_max_data == recv_max_data) {
+        return NGX_OK;
+    }
+
+    qs->recv_max_data = recv_max_data;
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, pc->log, 0,
+                   "quic stream id:0x%xL flow update msd:%uL",
+                   qs->id, qs->recv_max_data);
+
+    frame = ngx_quic_alloc_frame(pc);
+    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->recv_max_data;
+
+    ngx_quic_queue_frame(qc, frame);
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_update_max_data(ngx_connection_t *c)
+{
+    uint64_t                recv_max_data;
+    ngx_quic_frame_t       *frame;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    recv_max_data = qc->streams.recv_offset + qc->streams.recv_window;
+
+    if (qc->streams.recv_max_data == recv_max_data) {
+        return NGX_OK;
+    }
+
+    qc->streams.recv_max_data = recv_max_data;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic flow update md:%uL", qc->streams.recv_max_data);
+
+    frame = ngx_quic_alloc_frame(c);
+    if (frame == NULL) {
+        return NGX_ERROR;
+    }
+
+    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);
+
+    return NGX_OK;
+}
+
+
+static void
+ngx_quic_set_event(ngx_event_t *ev)
+{
+    ev->ready = 1;
+
+    if (ev->active) {
+        ngx_post_event(ev, &ngx_posted_events);
+    }
+}
diff --git a/src/event/quic/ngx_event_quic_streams.h b/src/event/quic/ngx_event_quic_streams.h
new file mode 100644
index 0000000..fb6dbbd
--- /dev/null
+++ b/src/event/quic/ngx_event_quic_streams.h
@@ -0,0 +1,44 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_EVENT_QUIC_STREAMS_H_INCLUDED_
+#define _NGX_EVENT_QUIC_STREAMS_H_INCLUDED_
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+
+ngx_int_t ngx_quic_handle_stream_frame(ngx_connection_t *c,
+    ngx_quic_header_t *pkt, ngx_quic_frame_t *frame);
+void ngx_quic_handle_stream_ack(ngx_connection_t *c,
+    ngx_quic_frame_t *f);
+ngx_int_t ngx_quic_handle_max_data_frame(ngx_connection_t *c,
+    ngx_quic_max_data_frame_t *f);
+ngx_int_t ngx_quic_handle_streams_blocked_frame(ngx_connection_t *c,
+    ngx_quic_header_t *pkt, ngx_quic_streams_blocked_frame_t *f);
+ngx_int_t ngx_quic_handle_data_blocked_frame(ngx_connection_t *c,
+    ngx_quic_header_t *pkt, ngx_quic_data_blocked_frame_t *f);
+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);
+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);
+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_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_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_int_t ngx_quic_init_streams(ngx_connection_t *c);
+void ngx_quic_rbtree_insert_stream(ngx_rbtree_node_t *temp,
+    ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel);
+ngx_quic_stream_t *ngx_quic_find_stream(ngx_rbtree_t *rbtree,
+    uint64_t id);
+ngx_int_t ngx_quic_close_streams(ngx_connection_t *c,
+    ngx_quic_connection_t *qc);
+
+#endif /* _NGX_EVENT_QUIC_STREAMS_H_INCLUDED_ */
diff --git a/src/event/quic/ngx_event_quic_tokens.c b/src/event/quic/ngx_event_quic_tokens.c
new file mode 100644
index 0000000..c1da0d4
--- /dev/null
+++ b/src/event/quic/ngx_event_quic_tokens.c
@@ -0,0 +1,309 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_event.h>
+#include <ngx_sha1.h>
+#include <ngx_event_quic_connection.h>
+
+
+static void ngx_quic_address_hash(struct sockaddr *sockaddr, socklen_t socklen,
+    ngx_uint_t no_port, u_char buf[20]);
+
+
+ngx_int_t
+ngx_quic_new_sr_token(ngx_connection_t *c, ngx_str_t *cid, u_char *secret,
+    u_char *token)
+{
+    ngx_str_t  tmp;
+
+    tmp.data = secret;
+    tmp.len = NGX_QUIC_SR_KEY_LEN;
+
+    if (ngx_quic_derive_key(c->log, "sr_token_key", &tmp, cid, token,
+                            NGX_QUIC_SR_TOKEN_LEN)
+        != NGX_OK)
+    {
+        return NGX_ERROR;
+    }
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic stateless reset token %*xs",
+                    (size_t) NGX_QUIC_SR_TOKEN_LEN, token);
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_quic_new_token(ngx_log_t *log, struct sockaddr *sockaddr,
+    socklen_t socklen, u_char *key, ngx_str_t *token, ngx_str_t *odcid,
+    time_t exp, ngx_uint_t is_retry)
+{
+    int                len, iv_len;
+    u_char            *p, *iv;
+    EVP_CIPHER_CTX    *ctx;
+    const EVP_CIPHER  *cipher;
+
+    u_char             in[NGX_QUIC_MAX_TOKEN_SIZE];
+
+    ngx_quic_address_hash(sockaddr, socklen, !is_retry, in);
+
+    p = in + 20;
+
+    p = ngx_cpymem(p, &exp, sizeof(time_t));
+
+    *p++ = is_retry ? 1 : 0;
+
+    if (odcid) {
+        *p++ = odcid->len;
+        p = ngx_cpymem(p, odcid->data, odcid->len);
+
+    } else {
+        *p++ = 0;
+    }
+
+    len = p - in;
+
+    cipher = EVP_aes_256_gcm();
+    iv_len = NGX_QUIC_AES_256_GCM_IV_LEN;
+
+    if ((size_t) (iv_len + len + NGX_QUIC_AES_256_GCM_TAG_LEN) > token->len) {
+        ngx_log_error(NGX_LOG_ALERT, log, 0, "quic token buffer is too small");
+        return NGX_ERROR;
+    }
+
+    ctx = EVP_CIPHER_CTX_new();
+    if (ctx == NULL) {
+        return NGX_ERROR;
+    }
+
+    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;
+
+    if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG,
+                            NGX_QUIC_AES_256_GCM_TAG_LEN,
+                            token->data + token->len)
+        == 0)
+    {
+        EVP_CIPHER_CTX_free(ctx);
+        return NGX_ERROR;
+    }
+
+    token->len += NGX_QUIC_AES_256_GCM_TAG_LEN;
+
+    EVP_CIPHER_CTX_free(ctx);
+
+#ifdef NGX_QUIC_DEBUG_PACKETS
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, log, 0,
+                   "quic new token len:%uz %xV", token->len, token);
+#endif
+
+    return NGX_OK;
+}
+
+
+static void
+ngx_quic_address_hash(struct sockaddr *sockaddr, socklen_t socklen,
+    ngx_uint_t no_port, u_char buf[20])
+{
+    size_t                len;
+    u_char               *data;
+    ngx_sha1_t            sha1;
+    struct sockaddr_in   *sin;
+#if (NGX_HAVE_INET6)
+    struct sockaddr_in6  *sin6;
+#endif
+
+    len = (size_t) socklen;
+    data = (u_char *) sockaddr;
+
+    if (no_port) {
+        switch (sockaddr->sa_family) {
+
+#if (NGX_HAVE_INET6)
+        case AF_INET6:
+            sin6 = (struct sockaddr_in6 *) sockaddr;
+
+            len = sizeof(struct in6_addr);
+            data = sin6->sin6_addr.s6_addr;
+
+            break;
+#endif
+
+        case AF_INET:
+            sin = (struct sockaddr_in *) sockaddr;
+
+            len = sizeof(in_addr_t);
+            data = (u_char *) &sin->sin_addr;
+
+            break;
+        }
+    }
+
+    ngx_sha1_init(&sha1);
+    ngx_sha1_update(&sha1, data, len);
+    ngx_sha1_final(buf, &sha1);
+}
+
+
+ngx_int_t
+ngx_quic_validate_token(ngx_connection_t *c, u_char *key,
+    ngx_quic_header_t *pkt)
+{
+    int                len, tlen, iv_len;
+    u_char            *iv, *p;
+    time_t             now, exp;
+    size_t             total;
+    ngx_str_t          odcid;
+    EVP_CIPHER_CTX    *ctx;
+    const EVP_CIPHER  *cipher;
+
+    u_char             addr_hash[20];
+    u_char             tdec[NGX_QUIC_MAX_TOKEN_SIZE];
+
+#if NGX_SUPPRESS_WARN
+    ngx_str_null(&odcid);
+#endif
+
+    /* Retry token or NEW_TOKEN in a previous connection */
+
+    cipher = EVP_aes_256_gcm();
+    iv = pkt->token.data;
+    iv_len = NGX_QUIC_AES_256_GCM_IV_LEN;
+
+    /* sanity checks */
+
+    if (pkt->token.len < (size_t) iv_len + NGX_QUIC_AES_256_GCM_TAG_LEN) {
+        goto garbage;
+    }
+
+    if (pkt->token.len > (size_t) iv_len + NGX_QUIC_MAX_TOKEN_SIZE
+                         + NGX_QUIC_AES_256_GCM_TAG_LEN)
+    {
+        goto garbage;
+    }
+
+    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 - NGX_QUIC_AES_256_GCM_TAG_LEN;
+
+    if (EVP_DecryptUpdate(ctx, tdec, &tlen, p, len) != 1) {
+        EVP_CIPHER_CTX_free(ctx);
+        goto garbage;
+    }
+    total = tlen;
+
+    if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG,
+                            NGX_QUIC_AES_256_GCM_TAG_LEN, p + len)
+        == 0)
+    {
+        EVP_CIPHER_CTX_free(ctx);
+        goto garbage;
+    }
+
+    if (EVP_DecryptFinal_ex(ctx, tdec + tlen, &tlen) <= 0) {
+        EVP_CIPHER_CTX_free(ctx);
+        goto garbage;
+    }
+    total += tlen;
+
+    EVP_CIPHER_CTX_free(ctx);
+
+    if (total < (20 + sizeof(time_t) + 2)) {
+        goto garbage;
+    }
+
+    p = tdec + 20;
+
+    ngx_memcpy(&exp, p, sizeof(time_t));
+    p += sizeof(time_t);
+
+    pkt->retried = (*p++ == 1);
+
+    ngx_quic_address_hash(c->sockaddr, c->socklen, !pkt->retried, addr_hash);
+
+    if (ngx_memcmp(tdec, addr_hash, 20) != 0) {
+        goto bad_token;
+    }
+
+    odcid.len = *p++;
+    if (odcid.len) {
+        if (odcid.len > NGX_QUIC_MAX_CID_LEN) {
+            goto bad_token;
+        }
+
+        if ((size_t)(tdec + total - p) < odcid.len) {
+            goto bad_token;
+        }
+
+        odcid.data = p;
+    }
+
+    now = ngx_time();
+
+    if (now > exp) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0, "quic expired token");
+        return NGX_DECLINED;
+    }
+
+    if (odcid.len) {
+        pkt->odcid.len = odcid.len;
+        pkt->odcid.data = pkt->odcid_buf;
+        ngx_memcpy(pkt->odcid.data, odcid.data, odcid.len);
+
+    } else {
+        pkt->odcid = pkt->dcid;
+    }
+
+    pkt->validated = 1;
+
+    return NGX_OK;
+
+garbage:
+
+    ngx_log_error(NGX_LOG_INFO, c->log, 0, "quic garbage token");
+
+    return NGX_ABORT;
+
+bad_token:
+
+    ngx_log_error(NGX_LOG_INFO, c->log, 0, "quic invalid token");
+
+    return NGX_DECLINED;
+}
diff --git a/src/event/quic/ngx_event_quic_tokens.h b/src/event/quic/ngx_event_quic_tokens.h
new file mode 100644
index 0000000..ee3fe5b
--- /dev/null
+++ b/src/event/quic/ngx_event_quic_tokens.h
@@ -0,0 +1,34 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_EVENT_QUIC_TOKENS_H_INCLUDED_
+#define _NGX_EVENT_QUIC_TOKENS_H_INCLUDED_
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+
+#define NGX_QUIC_MAX_TOKEN_SIZE              64
+    /* SHA-1(addr)=20 + sizeof(time_t) + retry(1) + odcid.len(1) + odcid */
+
+#define NGX_QUIC_AES_256_GCM_IV_LEN          12
+#define NGX_QUIC_AES_256_GCM_TAG_LEN         16
+
+#define NGX_QUIC_TOKEN_BUF_SIZE             (NGX_QUIC_AES_256_GCM_IV_LEN      \
+                                             + NGX_QUIC_MAX_TOKEN_SIZE        \
+                                             + NGX_QUIC_AES_256_GCM_TAG_LEN)
+
+
+ngx_int_t ngx_quic_new_sr_token(ngx_connection_t *c, ngx_str_t *cid,
+    u_char *secret, u_char *token);
+ngx_int_t ngx_quic_new_token(ngx_log_t *log, struct sockaddr *sockaddr,
+    socklen_t socklen, u_char *key, ngx_str_t *token, ngx_str_t *odcid,
+    time_t expires, ngx_uint_t is_retry);
+ngx_int_t ngx_quic_validate_token(ngx_connection_t *c,
+    u_char *key, ngx_quic_header_t *pkt);
+
+#endif /* _NGX_EVENT_QUIC_TOKENS_H_INCLUDED_ */
diff --git a/src/event/quic/ngx_event_quic_transport.c b/src/event/quic/ngx_event_quic_transport.c
new file mode 100644
index 0000000..bb13447
--- /dev/null
+++ b/src/event/quic/ngx_event_quic_transport.c
@@ -0,0 +1,2215 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_event.h>
+#include <ngx_event_quic_connection.h>
+
+
+#define NGX_QUIC_LONG_DCID_LEN_OFFSET  5
+#define NGX_QUIC_LONG_DCID_OFFSET      6
+#define NGX_QUIC_SHORT_DCID_OFFSET     1
+
+#define NGX_QUIC_STREAM_FRAME_FIN      0x01
+#define NGX_QUIC_STREAM_FRAME_LEN      0x02
+#define NGX_QUIC_STREAM_FRAME_OFF      0x04
+
+
+#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_uint64(p, s)                                           \
+    ((p)[0] = (u_char) ((s) >> 56),                                           \
+     (p)[1] = (u_char) ((s) >> 48),                                           \
+     (p)[2] = (u_char) ((s) >> 40),                                           \
+     (p)[3] = (u_char) ((s) >> 32),                                           \
+     (p)[4] = (u_char) ((s) >> 24),                                           \
+     (p)[5] = (u_char) ((s) >> 16),                                           \
+     (p)[6] = (u_char) ((s) >> 8),                                            \
+     (p)[7] = (u_char)  (s),                                                  \
+     (p) + sizeof(uint64_t))
+
+#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_build_int_set(p, value, len, bits)                           \
+    (*(p)++ = ((value >> ((len) * 8)) & 0xff) | ((bits) << 6))
+
+
+static u_char *ngx_quic_parse_int(u_char *pos, u_char *end, uint64_t *out);
+static ngx_uint_t ngx_quic_varint_len(uint64_t value);
+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,
+    u_char **pnp);
+static size_t ngx_quic_create_short_header(ngx_quic_header_t *pkt, u_char *out,
+    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_ping(u_char *p);
+static size_t ngx_quic_create_ack(u_char *p, ngx_quic_ack_frame_t *ack,
+    ngx_chain_t *ranges);
+static size_t ngx_quic_create_reset_stream(u_char *p,
+    ngx_quic_reset_stream_frame_t *rs);
+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, ngx_chain_t *data);
+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, ngx_chain_t *data);
+static size_t ngx_quic_create_stream(u_char *p, ngx_quic_stream_frame_t *sf,
+    ngx_chain_t *data);
+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_challenge(u_char *p,
+    ngx_quic_path_challenge_frame_t *pc);
+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_frame_t *f);
+
+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[] = {
+    /* QUICv1 */
+    0x00000001,
+};
+
+#define NGX_QUIC_NVERSIONS \
+    (sizeof(ngx_quic_versions) / sizeof(ngx_quic_versions[0]))
+
+
+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 ngx_inline ngx_uint_t
+ngx_quic_varint_len(uint64_t value)
+{
+    if (value < (1 << 6)) {
+        return 1;
+    }
+
+    if (value < (1 << 14)) {
+        return 2;
+    }
+
+    if (value < (1 << 30)) {
+        return 4;
+    }
+
+    return 8;
+}
+
+
+static ngx_inline void
+ngx_quic_build_int(u_char **pos, uint64_t value)
+{
+    u_char  *p;
+
+    p = *pos;
+
+    if (value < (1 << 6)) {
+        ngx_quic_build_int_set(p, value, 0, 0);
+
+    } else if (value < (1 << 14)) {
+        ngx_quic_build_int_set(p, value, 1, 1);
+        ngx_quic_build_int_set(p, value, 0, 0);
+
+    } else if (value < (1 << 30)) {
+        ngx_quic_build_int_set(p, value, 3, 2);
+        ngx_quic_build_int_set(p, value, 2, 0);
+        ngx_quic_build_int_set(p, value, 1, 0);
+        ngx_quic_build_int_set(p, value, 0, 0);
+
+    } else {
+        ngx_quic_build_int_set(p, value, 7, 3);
+        ngx_quic_build_int_set(p, value, 6, 0);
+        ngx_quic_build_int_set(p, value, 5, 0);
+        ngx_quic_build_int_set(p, value, 4, 0);
+        ngx_quic_build_int_set(p, value, 3, 0);
+        ngx_quic_build_int_set(p, value, 2, 0);
+        ngx_quic_build_int_set(p, value, 1, 0);
+        ngx_quic_build_int_set(p, value, 0, 0);
+    }
+
+    *pos = p;
+}
+
+
+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_ERROR;
+        }
+
+        return NGX_OK;
+    }
+
+    if (ngx_quic_parse_long_header(pkt) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    if (pkt->version == 0) {
+        /* version negotiation */
+        return NGX_ERROR;
+    }
+
+    if (!ngx_quic_supported_version(pkt->version)) {
+        return NGX_ABORT;
+    }
+
+    if (ngx_quic_parse_long_header_v1(pkt) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    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;
+}
+
+
+/* returns the amount of payload quic packet of "pkt_len" size may fit or 0 */
+size_t
+ngx_quic_payload_size(ngx_quic_header_t *pkt, size_t pkt_len)
+{
+    size_t  len;
+
+    if (ngx_quic_short_pkt(pkt->flags)) {
+
+        len = 1 + pkt->dcid.len + pkt->num_len + NGX_QUIC_TAG_LEN;
+        if (len > pkt_len) {
+            return 0;
+        }
+
+        return pkt_len - len;
+    }
+
+    /* flags, version, dcid and scid with lengths and zero-length token */
+    len = 5 + 2 + pkt->dcid.len + pkt->scid.len
+          + (pkt->level == ssl_encryption_initial ? 1 : 0);
+
+    if (len > pkt_len) {
+        return 0;
+    }
+
+    /* (pkt_len - len) is 'remainder' packet length (see RFC 9000, 17.2) */
+    len += ngx_quic_varint_len(pkt_len - len)
+           + pkt->num_len + NGX_QUIC_TAG_LEN;
+
+    if (len > pkt_len) {
+        return 0;
+    }
+
+    return pkt_len - len;
+}
+
+
+size_t
+ngx_quic_create_header(ngx_quic_header_t *pkt, u_char *out, u_char **pnp)
+{
+    return ngx_quic_short_pkt(pkt->flags)
+           ? ngx_quic_create_short_header(pkt, out, pnp)
+           : ngx_quic_create_long_header(pkt, out, pnp);
+}
+
+
+static size_t
+ngx_quic_create_long_header(ngx_quic_header_t *pkt, u_char *out,
+    u_char **pnp)
+{
+    size_t   rem_len;
+    u_char  *p, *start;
+
+    rem_len = pkt->num_len + pkt->payload.len + NGX_QUIC_TAG_LEN;
+
+    if (out == NULL) {
+        return 5 + 2 + pkt->dcid.len + pkt->scid.len
+               + ngx_quic_varint_len(rem_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, rem_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,
+    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;
+}
+
+
+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_buf_t   *b;
+    ngx_uint_t   i;
+
+    b = f->data->buf;
+
+    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;
+    }
+
+    if (varint > NGX_QUIC_FT_LAST) {
+        pkt->error = NGX_QUIC_ERR_FRAME_ENCODING_ERROR;
+        ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                      "quic unknown frame type 0x%xL", varint);
+        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, &b->pos);
+        if (p == NULL) {
+            goto error;
+        }
+
+        b->last = p;
+
+        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:
+
+        p = ngx_quic_parse_int(p, end, &f->u.ack.largest);
+        if (p == NULL) {
+            goto error;
+        }
+
+        p = ngx_quic_parse_int(p, end, &f->u.ack.delay);
+        if (p == NULL) {
+            goto error;
+        }
+
+        p = ngx_quic_parse_int(p, end, &f->u.ack.range_count);
+        if (p == NULL) {
+            goto error;
+        }
+
+        p = ngx_quic_parse_int(p, end, &f->u.ack.first_range);
+        if (p == NULL) {
+            goto error;
+        }
+
+        b->pos = 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 == NULL) {
+                goto error;
+            }
+
+            p = ngx_quic_parse_int(p, end, &varint);
+            if (p == NULL) {
+                goto error;
+            }
+        }
+
+        b->last = p;
+
+        f->u.ack.ranges_length = b->last - b->pos;
+
+        if (f->type == NGX_QUIC_FT_ACK_ECN) {
+
+            p = ngx_quic_parse_int(p, end, &f->u.ack.ect0);
+            if (p == NULL) {
+                goto error;
+            }
+
+            p = ngx_quic_parse_int(p, end, &f->u.ack.ect1);
+            if (p == NULL) {
+                goto error;
+            }
+
+            p = ngx_quic_parse_int(p, end, &f->u.ack.ce);
+            if (p == NULL) {
+                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_STREAM:
+    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.fin = (f->type & NGX_QUIC_STREAM_FRAME_FIN) ? 1 : 0;
+
+        p = ngx_quic_parse_int(p, end, &f->u.stream.stream_id);
+        if (p == NULL) {
+            goto error;
+        }
+
+        if (f->type & NGX_QUIC_STREAM_FRAME_OFF) {
+            f->u.stream.off = 1;
+
+            p = ngx_quic_parse_int(p, end, &f->u.stream.offset);
+            if (p == NULL) {
+                goto error;
+            }
+
+        } else {
+            f->u.stream.off = 0;
+            f->u.stream.offset = 0;
+        }
+
+        if (f->type & NGX_QUIC_STREAM_FRAME_LEN) {
+            f->u.stream.len = 1;
+
+            p = ngx_quic_parse_int(p, end, &f->u.stream.length);
+            if (p == NULL) {
+                goto error;
+            }
+
+        } else {
+            f->u.stream.len = 0;
+            f->u.stream.length = end - p; /* up to packet end */
+        }
+
+        p = ngx_quic_read_bytes(p, end, f->u.stream.length, &b->pos);
+        if (p == NULL) {
+            goto error;
+        }
+
+        b->last = p;
+
+        f->type = NGX_QUIC_FT_STREAM;
+        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:
+
+        p = ngx_quic_parse_int(p, end, &f->u.reset_stream.id);
+        if (p == NULL) {
+            goto error;
+        }
+
+        p = ngx_quic_parse_int(p, end, &f->u.reset_stream.error_code);
+        if (p == NULL) {
+            goto error;
+        }
+
+        p = ngx_quic_parse_int(p, end, &f->u.reset_stream.final_size);
+        if (p == NULL) {
+            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;
+        }
+
+        if (f->u.streams_blocked.limit > 0x1000000000000000) {
+            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;
+        }
+
+        if (f->u.max_streams.limit > 0x1000000000000000) {
+            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;
+#if (NGX_DEBUG)
+    f->pnum = pkt->pn;
+#endif
+
+    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;
+
+    /*
+     * RFC 9000, 12.4. Frames and Frame Types: Table 3
+     *
+     * Frame permissions per packet: 4 bits: IH01
+     */
+    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 */
+         /* STREAM */                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 */         0x1,
+         /* CONNECTION_CLOSE */      0xF,
+         /* CONNECTION_CLOSE2 */     0x3,
+         /* 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)
+{
+    /*
+     *  RFC 9002, 2.  Conventions and Definitions
+     *
+     *  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_PING:
+        return ngx_quic_create_ping(p);
+
+    case NGX_QUIC_FT_ACK:
+        f->need_ack = 0;
+        return ngx_quic_create_ack(p, &f->u.ack, f->data);
+
+    case NGX_QUIC_FT_RESET_STREAM:
+        return ngx_quic_create_reset_stream(p, &f->u.reset_stream);
+
+    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, f->data);
+
+    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, f->data);
+
+    case NGX_QUIC_FT_STREAM:
+        return ngx_quic_create_stream(p, &f->u.stream, f->data);
+
+    case NGX_QUIC_FT_CONNECTION_CLOSE:
+    case NGX_QUIC_FT_CONNECTION_CLOSE_APP:
+        f->need_ack = 0;
+        return ngx_quic_create_close(p, f);
+
+    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_CHALLENGE:
+        return ngx_quic_create_path_challenge(p, &f->u.path_challenge);
+
+    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_ping(u_char *p)
+{
+    u_char  *start;
+
+    if (p == NULL) {
+        return ngx_quic_varint_len(NGX_QUIC_FT_PING);
+    }
+
+    start = p;
+
+    ngx_quic_build_int(&p, NGX_QUIC_FT_PING);
+
+    return p - start;
+}
+
+
+static size_t
+ngx_quic_create_ack(u_char *p, ngx_quic_ack_frame_t *ack, ngx_chain_t *ranges)
+{
+    size_t      len;
+    u_char     *start;
+    ngx_buf_t  *b;
+
+    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_length;
+
+        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);
+
+    while (ranges) {
+        b = ranges->buf;
+        p = ngx_cpymem(p, b->pos, b->last - b->pos);
+        ranges = ranges->next;
+    }
+
+    return p - start;
+}
+
+
+static size_t
+ngx_quic_create_reset_stream(u_char *p, ngx_quic_reset_stream_frame_t *rs)
+{
+    size_t   len;
+    u_char  *start;
+
+    if (p == NULL) {
+        len = ngx_quic_varint_len(NGX_QUIC_FT_RESET_STREAM);
+        len += ngx_quic_varint_len(rs->id);
+        len += ngx_quic_varint_len(rs->error_code);
+        len += ngx_quic_varint_len(rs->final_size);
+        return len;
+    }
+
+    start = p;
+
+    ngx_quic_build_int(&p, NGX_QUIC_FT_RESET_STREAM);
+    ngx_quic_build_int(&p, rs->id);
+    ngx_quic_build_int(&p, rs->error_code);
+    ngx_quic_build_int(&p, rs->final_size);
+
+    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,
+    ngx_chain_t *data)
+{
+    size_t      len;
+    u_char     *start;
+    ngx_buf_t  *b;
+
+    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);
+
+    while (data) {
+        b = data->buf;
+        p = ngx_cpymem(p, b->pos, b->last - b->pos);
+        data = data->next;
+    }
+
+    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,
+    ngx_chain_t *data)
+{
+    size_t      len;
+    u_char     *start;
+    ngx_buf_t  *b;
+
+    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);
+
+    while (data) {
+        b = data->buf;
+        p = ngx_cpymem(p, b->pos, b->last - b->pos);
+        data = data->next;
+    }
+
+    return p - start;
+}
+
+
+static size_t
+ngx_quic_create_stream(u_char *p, ngx_quic_stream_frame_t *sf,
+    ngx_chain_t *data)
+{
+    size_t      len;
+    u_char     *start, type;
+    ngx_buf_t  *b;
+
+    type = NGX_QUIC_FT_STREAM;
+
+    if (sf->off) {
+        type |= NGX_QUIC_STREAM_FRAME_OFF;
+    }
+
+    if (sf->len) {
+        type |= NGX_QUIC_STREAM_FRAME_LEN;
+    }
+
+    if (sf->fin) {
+        type |= NGX_QUIC_STREAM_FRAME_FIN;
+    }
+
+    if (p == NULL) {
+        len = ngx_quic_varint_len(type);
+        len += ngx_quic_varint_len(sf->stream_id);
+
+        if (sf->off) {
+            len += ngx_quic_varint_len(sf->offset);
+        }
+
+        if (sf->len) {
+            len += ngx_quic_varint_len(sf->length);
+        }
+
+        len += sf->length;
+
+        return len;
+    }
+
+    start = p;
+
+    ngx_quic_build_int(&p, type);
+    ngx_quic_build_int(&p, sf->stream_id);
+
+    if (sf->off) {
+        ngx_quic_build_int(&p, sf->offset);
+    }
+
+    if (sf->len) {
+        ngx_quic_build_int(&p, sf->length);
+    }
+
+    while (data) {
+        b = data->buf;
+        p = ngx_cpymem(p, b->pos, b->last - b->pos);
+        data = data->next;
+    }
+
+    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;
+        }
+
+        if ((size_t) (end - p) < len) {
+            ngx_log_error(NGX_LOG_INFO, log, 0,
+                          "quic failed to parse"
+                          " transport param id:0x%xL, data length %uL too long",
+                          id, len);
+            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 %s transport param id:0x%xL, skipped",
+                          (id % 31 == 27) ? "reserved" : "unknown", 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);
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, log, 0,
+                   "quic tp initial source_connection_id len:%uz %xV",
+                   tp->initial_scid.len, &tp->initial_scid);
+
+    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_challenge(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_CHALLENGE);
+        len += sizeof(pc->data);
+        return len;
+    }
+
+    start = p;
+
+    ngx_quic_build_int(&p, NGX_QUIC_FT_PATH_CHALLENGE);
+    p = ngx_cpymem(p, &pc->data, sizeof(pc->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;
+}
+
+
+ngx_int_t
+ngx_quic_init_transport_params(ngx_quic_tp_t *tp, ngx_quic_conf_t *qcf)
+{
+    ngx_uint_t  nstreams;
+
+    ngx_memzero(tp, sizeof(ngx_quic_tp_t));
+
+    /*
+     * set by ngx_memzero():
+     *
+     *     tp->disable_active_migration = 0;
+     *     tp->original_dcid = { 0, NULL };
+     *     tp->initial_scid = { 0, NULL };
+     *     tp->retry_scid = { 0, NULL };
+     *     tp->sr_token = { 0 }
+     *     tp->sr_enabled = 0
+     *     tp->preferred_address = NULL
+     */
+
+    tp->max_idle_timeout = qcf->idle_timeout;
+
+    tp->max_udp_payload_size = NGX_QUIC_MAX_UDP_PAYLOAD_SIZE;
+
+    nstreams = qcf->max_concurrent_streams_bidi
+               + qcf->max_concurrent_streams_uni;
+
+    tp->initial_max_data = nstreams * qcf->stream_buffer_size;
+    tp->initial_max_stream_data_bidi_local = qcf->stream_buffer_size;
+    tp->initial_max_stream_data_bidi_remote = qcf->stream_buffer_size;
+    tp->initial_max_stream_data_uni = qcf->stream_buffer_size;
+
+    tp->initial_max_streams_bidi = qcf->max_concurrent_streams_bidi;
+    tp->initial_max_streams_uni = qcf->max_concurrent_streams_uni;
+
+    tp->max_ack_delay = NGX_QUIC_DEFAULT_MAX_ACK_DELAY;
+    tp->ack_delay_exponent = NGX_QUIC_DEFAULT_ACK_DELAY_EXPONENT;
+
+    tp->active_connection_id_limit = qcf->active_connection_id_limit;
+    tp->disable_active_migration = qcf->disable_active_migration;
+
+    return NGX_OK;
+}
+
+
+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)
+
+    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);
+
+    len += ngx_quic_tp_len(NGX_QUIC_TP_MAX_UDP_PAYLOAD_SIZE,
+                           tp->max_udp_payload_size);
+
+    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);
+
+    /* transport parameters listed above will be saved in 0-RTT context */
+    if (clen) {
+        *clen = len;
+    }
+
+    len += ngx_quic_tp_len(NGX_QUIC_TP_MAX_ACK_DELAY,
+                           tp->max_ack_delay);
+
+    len += ngx_quic_tp_len(NGX_QUIC_TP_ACK_DELAY_EXPONENT,
+                           tp->ack_delay_exponent);
+
+    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);
+    }
+
+    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;
+    }
+
+    p = pos;
+
+    ngx_quic_tp_vint(NGX_QUIC_TP_INITIAL_MAX_DATA,
+                     tp->initial_max_data);
+
+    ngx_quic_tp_vint(NGX_QUIC_TP_INITIAL_MAX_STREAMS_UNI,
+                     tp->initial_max_streams_uni);
+
+    ngx_quic_tp_vint(NGX_QUIC_TP_INITIAL_MAX_STREAMS_BIDI,
+                     tp->initial_max_streams_bidi);
+
+    ngx_quic_tp_vint(NGX_QUIC_TP_INITIAL_MAX_STREAM_DATA_BIDI_LOCAL,
+                     tp->initial_max_stream_data_bidi_local);
+
+    ngx_quic_tp_vint(NGX_QUIC_TP_INITIAL_MAX_STREAM_DATA_BIDI_REMOTE,
+                     tp->initial_max_stream_data_bidi_remote);
+
+    ngx_quic_tp_vint(NGX_QUIC_TP_INITIAL_MAX_STREAM_DATA_UNI,
+                     tp->initial_max_stream_data_uni);
+
+    ngx_quic_tp_vint(NGX_QUIC_TP_MAX_IDLE_TIMEOUT,
+                     tp->max_idle_timeout);
+
+    ngx_quic_tp_vint(NGX_QUIC_TP_MAX_UDP_PAYLOAD_SIZE,
+                     tp->max_udp_payload_size);
+
+    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);
+
+    ngx_quic_tp_vint(NGX_QUIC_TP_MAX_ACK_DELAY,
+                     tp->max_ack_delay);
+
+    ngx_quic_tp_vint(NGX_QUIC_TP_ACK_DELAY_EXPONENT,
+                     tp->ack_delay_exponent);
+
+    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);
+    }
+
+    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_frame_t *f)
+{
+    size_t                   len;
+    u_char                  *start;
+    ngx_quic_close_frame_t  *cl;
+
+    cl = &f->u.close;
+
+    if (p == NULL) {
+        len = ngx_quic_varint_len(f->type);
+        len += ngx_quic_varint_len(cl->error_code);
+
+        if (f->type != NGX_QUIC_FT_CONNECTION_CLOSE_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, f->type);
+    ngx_quic_build_int(&p, cl->error_code);
+
+    if (f->type != NGX_QUIC_FT_CONNECTION_CLOSE_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;
+}
+
+
+void
+ngx_quic_dcid_encode_key(u_char *dcid, uint64_t key)
+{
+    (void) ngx_quic_write_uint64(dcid, key);
+}
diff --git a/src/event/quic/ngx_event_quic_transport.h b/src/event/quic/ngx_event_quic_transport.h
new file mode 100644
index 0000000..3e32039
--- /dev/null
+++ b/src/event/quic/ngx_event_quic_transport.h
@@ -0,0 +1,397 @@
+
+/*
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_EVENT_QUIC_TRANSPORT_H_INCLUDED_
+#define _NGX_EVENT_QUIC_TRANSPORT_H_INCLUDED_
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+
+/*
+ * RFC 9000, 17.2.  Long Header Packets
+ *           17.3.  Short Header Packets
+ *
+ * QUIC flags in first byte
+ */
+#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"
+
+#define NGX_QUIC_MAX_CID_LEN                             20
+#define NGX_QUIC_SERVER_CID_LEN                          NGX_QUIC_MAX_CID_LEN
+
+/* 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_STREAM                               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
+
+#define NGX_QUIC_FT_LAST  NGX_QUIC_FT_HANDSHAKE_DONE
+
+/* 22.5.  QUIC Transport Error Codes Registry */
+#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_AEAD_LIMIT_REACHED                  0x0F
+#define NGX_QUIC_ERR_NO_VIABLE_PATH                      0x10
+
+#define NGX_QUIC_ERR_CRYPTO_ERROR                       0x100
+
+#define NGX_QUIC_ERR_CRYPTO(e)  (NGX_QUIC_ERR_CRYPTO_ERROR + (e))
+
+
+/* 22.3.  QUIC Transport Parameters Registry */
+#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;
+    uint64_t                                    ranges_length;
+} 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;
+} 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;
+} 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;
+
+    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_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                                  send_time;
+    ssize_t                                     len;
+    unsigned                                    need_ack:1;
+    unsigned                                    pkt_need_ack:1;
+    unsigned                                    ignore_congestion:1;
+
+    ngx_chain_t                                *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_path_t                            *path;
+
+    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 */
+    u_char                                      odcid_buf[NGX_QUIC_MAX_CID_LEN];
+    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;
+    unsigned                                    validated:1;
+    unsigned                                    retried:1;
+    unsigned                                    first:1;
+    unsigned                                    rebound:1;
+    unsigned                                    path_challenged:1;
+} ngx_quic_header_t;
+
+
+typedef struct {
+    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;
+
+
+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_payload_size(ngx_quic_header_t *pkt, size_t pkt_len);
+
+size_t ngx_quic_create_header(ngx_quic_header_t *pkt, u_char *out,
+    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_init_transport_params(ngx_quic_tp_t *tp,
+    ngx_quic_conf_t *qcf);
+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);
+
+void ngx_quic_dcid_encode_key(u_char *dcid, uint64_t key);
+
+#endif /* _NGX_EVENT_QUIC_TRANSPORT_H_INCLUDED_ */
diff --git a/src/event/quic/ngx_event_quic_udp.c b/src/event/quic/ngx_event_quic_udp.c
new file mode 100644
index 0000000..15b54bc
--- /dev/null
+++ b/src/event/quic/ngx_event_quic_udp.c
@@ -0,0 +1,420 @@
+
+/*
+ * Copyright (C) Roman Arutyunyan
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_event.h>
+#include <ngx_event_quic_connection.h>
+
+
+static void ngx_quic_close_accepted_connection(ngx_connection_t *c);
+static ngx_connection_t *ngx_quic_lookup_connection(ngx_listening_t *ls,
+    ngx_str_t *key, struct sockaddr *local_sockaddr, socklen_t local_socklen);
+
+
+void
+ngx_quic_recvmsg(ngx_event_t *ev)
+{
+    ssize_t             n;
+    ngx_str_t           key;
+    ngx_buf_t           buf;
+    ngx_log_t          *log;
+    ngx_err_t           err;
+    socklen_t           socklen, local_socklen;
+    ngx_event_t        *rev, *wev;
+    struct iovec        iov[1];
+    struct msghdr       msg;
+    ngx_sockaddr_t      sa, lsa;
+    struct sockaddr    *sockaddr, *local_sockaddr;
+    ngx_listening_t    *ls;
+    ngx_event_conf_t   *ecf;
+    ngx_connection_t   *c, *lc;
+    ngx_quic_socket_t  *qsock;
+    static u_char       buffer[NGX_QUIC_MAX_UDP_PAYLOAD_SIZE];
+
+#if (NGX_HAVE_ADDRINFO_CMSG)
+    u_char             msg_control[CMSG_SPACE(sizeof(ngx_addrinfo_t))];
+#endif
+
+    if (ev->timedout) {
+        if (ngx_enable_accept_events((ngx_cycle_t *) ngx_cycle) != NGX_OK) {
+            return;
+        }
+
+        ev->timedout = 0;
+    }
+
+    ecf = ngx_event_get_conf(ngx_cycle->conf_ctx, ngx_event_core_module);
+
+    if (!(ngx_event_flags & NGX_USE_KQUEUE_EVENT)) {
+        ev->available = ecf->multi_accept;
+    }
+
+    lc = ev->data;
+    ls = lc->listening;
+    ev->ready = 0;
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, ev->log, 0,
+                   "quic recvmsg on %V, ready: %d",
+                   &ls->addr_text, ev->available);
+
+    do {
+        ngx_memzero(&msg, sizeof(struct msghdr));
+
+        iov[0].iov_base = (void *) buffer;
+        iov[0].iov_len = sizeof(buffer);
+
+        msg.msg_name = &sa;
+        msg.msg_namelen = sizeof(ngx_sockaddr_t);
+        msg.msg_iov = iov;
+        msg.msg_iovlen = 1;
+
+#if (NGX_HAVE_ADDRINFO_CMSG)
+        if (ls->wildcard) {
+            msg.msg_control = &msg_control;
+            msg.msg_controllen = sizeof(msg_control);
+
+            ngx_memzero(&msg_control, sizeof(msg_control));
+        }
+#endif
+
+        n = recvmsg(lc->fd, &msg, 0);
+
+        if (n == -1) {
+            err = ngx_socket_errno;
+
+            if (err == NGX_EAGAIN) {
+                ngx_log_debug0(NGX_LOG_DEBUG_EVENT, ev->log, err,
+                               "quic recvmsg() not ready");
+                return;
+            }
+
+            ngx_log_error(NGX_LOG_ALERT, ev->log, err, "quic recvmsg() failed");
+
+            return;
+        }
+
+#if (NGX_HAVE_ADDRINFO_CMSG)
+        if (msg.msg_flags & (MSG_TRUNC|MSG_CTRUNC)) {
+            ngx_log_error(NGX_LOG_ALERT, ev->log, 0,
+                          "quic recvmsg() truncated data");
+            continue;
+        }
+#endif
+
+        sockaddr = msg.msg_name;
+        socklen = msg.msg_namelen;
+
+        if (socklen > (socklen_t) sizeof(ngx_sockaddr_t)) {
+            socklen = sizeof(ngx_sockaddr_t);
+        }
+
+#if (NGX_HAVE_UNIX_DOMAIN)
+
+        if (sockaddr->sa_family == AF_UNIX) {
+            struct sockaddr_un *saun = (struct sockaddr_un *) sockaddr;
+
+            if (socklen <= (socklen_t) offsetof(struct sockaddr_un, sun_path)
+                || saun->sun_path[0] == '\0')
+            {
+                ngx_log_debug0(NGX_LOG_DEBUG_EVENT, ngx_cycle->log, 0,
+                               "unbound unix socket");
+                goto next;
+            }
+        }
+
+#endif
+
+        local_sockaddr = ls->sockaddr;
+        local_socklen = ls->socklen;
+
+#if (NGX_HAVE_ADDRINFO_CMSG)
+
+        if (ls->wildcard) {
+            struct cmsghdr  *cmsg;
+
+            ngx_memcpy(&lsa, local_sockaddr, local_socklen);
+            local_sockaddr = &lsa.sockaddr;
+
+            for (cmsg = CMSG_FIRSTHDR(&msg);
+                 cmsg != NULL;
+                 cmsg = CMSG_NXTHDR(&msg, cmsg))
+            {
+                if (ngx_get_srcaddr_cmsg(cmsg, local_sockaddr) == NGX_OK) {
+                    break;
+                }
+            }
+        }
+
+#endif
+
+        if (ngx_quic_get_packet_dcid(ev->log, buffer, n, &key) != NGX_OK) {
+            goto next;
+        }
+
+        c = ngx_quic_lookup_connection(ls, &key, local_sockaddr, local_socklen);
+
+        if (c) {
+
+#if (NGX_DEBUG)
+            if (c->log->log_level & NGX_LOG_DEBUG_EVENT) {
+                ngx_log_handler_pt  handler;
+
+                handler = c->log->handler;
+                c->log->handler = NULL;
+
+                ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                               "quic recvmsg: fd:%d n:%z", c->fd, n);
+
+                c->log->handler = handler;
+            }
+#endif
+
+            ngx_memzero(&buf, sizeof(ngx_buf_t));
+
+            buf.pos = buffer;
+            buf.last = buffer + n;
+            buf.start = buf.pos;
+            buf.end = buffer + sizeof(buffer);
+
+            qsock = ngx_quic_get_socket(c);
+
+            ngx_memcpy(&qsock->sockaddr, sockaddr, socklen);
+            qsock->socklen = socklen;
+
+            c->udp->buffer = &buf;
+
+            rev = c->read;
+            rev->ready = 1;
+            rev->active = 0;
+
+            rev->handler(rev);
+
+            if (c->udp) {
+                c->udp->buffer = NULL;
+            }
+
+            rev->ready = 0;
+            rev->active = 1;
+
+            goto next;
+        }
+
+#if (NGX_STAT_STUB)
+        (void) ngx_atomic_fetch_add(ngx_stat_accepted, 1);
+#endif
+
+        ngx_accept_disabled = ngx_cycle->connection_n / 8
+                              - ngx_cycle->free_connection_n;
+
+        c = ngx_get_connection(lc->fd, ev->log);
+        if (c == NULL) {
+            return;
+        }
+
+        c->shared = 1;
+        c->type = SOCK_DGRAM;
+        c->socklen = socklen;
+
+#if (NGX_STAT_STUB)
+        (void) ngx_atomic_fetch_add(ngx_stat_active, 1);
+#endif
+
+        c->pool = ngx_create_pool(ls->pool_size, ev->log);
+        if (c->pool == NULL) {
+            ngx_quic_close_accepted_connection(c);
+            return;
+        }
+
+        c->sockaddr = ngx_palloc(c->pool, NGX_SOCKADDRLEN);
+        if (c->sockaddr == NULL) {
+            ngx_quic_close_accepted_connection(c);
+            return;
+        }
+
+        ngx_memcpy(c->sockaddr, sockaddr, socklen);
+
+        log = ngx_palloc(c->pool, sizeof(ngx_log_t));
+        if (log == NULL) {
+            ngx_quic_close_accepted_connection(c);
+            return;
+        }
+
+        *log = ls->log;
+
+        c->log = log;
+        c->pool->log = log;
+        c->listening = ls;
+
+        if (local_sockaddr == &lsa.sockaddr) {
+            local_sockaddr = ngx_palloc(c->pool, local_socklen);
+            if (local_sockaddr == NULL) {
+                ngx_quic_close_accepted_connection(c);
+                return;
+            }
+
+            ngx_memcpy(local_sockaddr, &lsa, local_socklen);
+        }
+
+        c->local_sockaddr = local_sockaddr;
+        c->local_socklen = local_socklen;
+
+        c->buffer = ngx_create_temp_buf(c->pool, n);
+        if (c->buffer == NULL) {
+            ngx_quic_close_accepted_connection(c);
+            return;
+        }
+
+        c->buffer->last = ngx_cpymem(c->buffer->last, buffer, n);
+
+        rev = c->read;
+        wev = c->write;
+
+        rev->active = 1;
+        wev->ready = 1;
+
+        rev->log = log;
+        wev->log = log;
+
+        /*
+         * TODO: MT: - ngx_atomic_fetch_add()
+         *             or protection by critical section or light mutex
+         *
+         * TODO: MP: - allocated in a shared memory
+         *           - ngx_atomic_fetch_add()
+         *             or protection by critical section or light mutex
+         */
+
+        c->number = ngx_atomic_fetch_add(ngx_connection_counter, 1);
+
+        c->start_time = ngx_current_msec;
+
+#if (NGX_STAT_STUB)
+        (void) ngx_atomic_fetch_add(ngx_stat_handled, 1);
+#endif
+
+        if (ls->addr_ntop) {
+            c->addr_text.data = ngx_pnalloc(c->pool, ls->addr_text_max_len);
+            if (c->addr_text.data == NULL) {
+                ngx_quic_close_accepted_connection(c);
+                return;
+            }
+
+            c->addr_text.len = ngx_sock_ntop(c->sockaddr, c->socklen,
+                                             c->addr_text.data,
+                                             ls->addr_text_max_len, 0);
+            if (c->addr_text.len == 0) {
+                ngx_quic_close_accepted_connection(c);
+                return;
+            }
+        }
+
+#if (NGX_DEBUG)
+        {
+        ngx_str_t  addr;
+        u_char     text[NGX_SOCKADDR_STRLEN];
+
+        ngx_debug_accepted_connection(ecf, c);
+
+        if (log->log_level & NGX_LOG_DEBUG_EVENT) {
+            addr.data = text;
+            addr.len = ngx_sock_ntop(c->sockaddr, c->socklen, text,
+                                     NGX_SOCKADDR_STRLEN, 1);
+
+            ngx_log_debug4(NGX_LOG_DEBUG_EVENT, log, 0,
+                           "*%uA quic recvmsg: %V fd:%d n:%z",
+                           c->number, &addr, c->fd, n);
+        }
+
+        }
+#endif
+
+        log->data = NULL;
+        log->handler = NULL;
+
+        ls->handler(c);
+
+    next:
+
+        if (ngx_event_flags & NGX_USE_KQUEUE_EVENT) {
+            ev->available -= n;
+        }
+
+    } while (ev->available);
+}
+
+
+static void
+ngx_quic_close_accepted_connection(ngx_connection_t *c)
+{
+    ngx_free_connection(c);
+
+    c->fd = (ngx_socket_t) -1;
+
+    if (c->pool) {
+        ngx_destroy_pool(c->pool);
+    }
+
+#if (NGX_STAT_STUB)
+    (void) ngx_atomic_fetch_add(ngx_stat_active, -1);
+#endif
+}
+
+
+static ngx_connection_t *
+ngx_quic_lookup_connection(ngx_listening_t *ls, ngx_str_t *key,
+    struct sockaddr *local_sockaddr, socklen_t local_socklen)
+{
+    uint32_t            hash;
+    ngx_int_t           rc;
+    ngx_connection_t   *c;
+    ngx_rbtree_node_t  *node, *sentinel;
+    ngx_quic_socket_t  *qsock;
+
+    if (key->len == 0) {
+        return NULL;
+    }
+
+    node = ls->rbtree.root;
+    sentinel = ls->rbtree.sentinel;
+    hash = ngx_crc32_long(key->data, key->len);
+
+    while (node != sentinel) {
+
+        if (hash < node->key) {
+            node = node->left;
+            continue;
+        }
+
+        if (hash > node->key) {
+            node = node->right;
+            continue;
+        }
+
+        /* hash == node->key */
+
+        qsock = (ngx_quic_socket_t *) node;
+
+        rc = ngx_memn2cmp(key->data, qsock->sid.id, key->len, qsock->sid.len);
+
+        c = qsock->udp.connection;
+
+        if (rc == 0 && ls->wildcard) {
+            rc = ngx_cmp_sockaddr(local_sockaddr, local_socklen,
+                                  c->local_sockaddr, c->local_socklen, 1);
+        }
+
+        if (rc == 0) {
+            c->udp = &qsock->udp;
+            return c;
+        }
+
+        node = (rc < 0) ? node->left : node->right;
+    }
+
+    return NULL;
+}
diff --git a/src/http/modules/ngx_http_access_module.c b/src/http/modules/ngx_http_access_module.c
index 7355de9..ea75520 100644
--- a/src/http/modules/ngx_http_access_module.c
+++ b/src/http/modules/ngx_http_access_module.c
@@ -148,7 +148,7 @@ ngx_http_access_handler(ngx_http_request_t *r)
         p = sin6->sin6_addr.s6_addr;
 
         if (alcf->rules && IN6_IS_ADDR_V4MAPPED(&sin6->sin6_addr)) {
-            addr = p[12] << 24;
+            addr = (in_addr_t) p[12] << 24;
             addr += p[13] << 16;
             addr += p[14] << 8;
             addr += p[15];
diff --git a/src/http/modules/ngx_http_auth_basic_module.c b/src/http/modules/ngx_http_auth_basic_module.c
index 0693319..02d41e8 100644
--- a/src/http/modules/ngx_http_auth_basic_module.c
+++ b/src/http/modules/ngx_http_auth_basic_module.c
@@ -339,6 +339,7 @@ ngx_http_auth_basic_set_realm(ngx_http_request_t *r, ngx_str_t *realm)
     *p = '"';
 
     r->headers_out.www_authenticate->hash = 1;
+    r->headers_out.www_authenticate->next = NULL;
     ngx_str_set(&r->headers_out.www_authenticate->key, "WWW-Authenticate");
     r->headers_out.www_authenticate->value.data = basic;
     r->headers_out.www_authenticate->value.len = len;
diff --git a/src/http/modules/ngx_http_auth_request_module.c b/src/http/modules/ngx_http_auth_request_module.c
index bab79e4..8bc98aa 100644
--- a/src/http/modules/ngx_http_auth_request_module.c
+++ b/src/http/modules/ngx_http_auth_request_module.c
@@ -101,7 +101,7 @@ ngx_module_t  ngx_http_auth_request_module = {
 static ngx_int_t
 ngx_http_auth_request_handler(ngx_http_request_t *r)
 {
-    ngx_table_elt_t               *h, *ho;
+    ngx_table_elt_t               *h, *ho, **ph;
     ngx_http_request_t            *sr;
     ngx_http_post_subrequest_t    *ps;
     ngx_http_auth_request_ctx_t   *ctx;
@@ -147,15 +147,21 @@ ngx_http_auth_request_handler(ngx_http_request_t *r)
                 h = sr->upstream->headers_in.www_authenticate;
             }
 
-            if (h) {
+            ph = &r->headers_out.www_authenticate;
+
+            while (h) {
                 ho = ngx_list_push(&r->headers_out.headers);
                 if (ho == NULL) {
                     return NGX_ERROR;
                 }
 
                 *ho = *h;
+                ho->next = NULL;
+
+                *ph = ho;
+                ph = &ho->next;
 
-                r->headers_out.www_authenticate = ho;
+                h = h->next;
             }
 
             return ctx->status;
diff --git a/src/http/modules/ngx_http_dav_module.c b/src/http/modules/ngx_http_dav_module.c
index 0cc9ae1..cfb9892 100644
--- a/src/http/modules/ngx_http_dav_module.c
+++ b/src/http/modules/ngx_http_dav_module.c
@@ -1082,6 +1082,7 @@ ngx_http_dav_location(ngx_http_request_t *r)
     }
 
     r->headers_out.location->hash = 1;
+    r->headers_out.location->next = NULL;
     ngx_str_set(&r->headers_out.location->key, "Location");
 
     escape = 2 * ngx_escape_uri(NULL, r->uri.data, r->uri.len, NGX_ESCAPE_URI);
diff --git a/src/http/modules/ngx_http_fastcgi_module.c b/src/http/modules/ngx_http_fastcgi_module.c
index 4a8dc33..b989083 100644
--- a/src/http/modules/ngx_http_fastcgi_module.c
+++ b/src/http/modules/ngx_http_fastcgi_module.c
@@ -835,14 +835,14 @@ static ngx_int_t
 ngx_http_fastcgi_create_request(ngx_http_request_t *r)
 {
     off_t                         file_pos;
-    u_char                        ch, *pos, *lowcase_key;
+    u_char                        ch, sep, *pos, *lowcase_key;
     size_t                        size, len, key_len, val_len, padding,
                                   allocated;
     ngx_uint_t                    i, n, next, hash, skip_empty, header_params;
     ngx_buf_t                    *b;
     ngx_chain_t                  *cl, *body;
     ngx_list_part_t              *part;
-    ngx_table_elt_t              *header, **ignored;
+    ngx_table_elt_t              *header, *hn, **ignored;
     ngx_http_upstream_t          *u;
     ngx_http_script_code_pt       code;
     ngx_http_script_engine_t      e, le;
@@ -900,7 +900,11 @@ ngx_http_fastcgi_create_request(ngx_http_request_t *r)
         allocated = 0;
         lowcase_key = NULL;
 
-        if (params->number) {
+        if (ngx_http_link_multi_headers(r) != NGX_OK) {
+            return NGX_ERROR;
+        }
+
+        if (params->number || r->headers_in.multi) {
             n = 0;
             part = &r->headers_in.headers.part;
 
@@ -930,6 +934,12 @@ ngx_http_fastcgi_create_request(ngx_http_request_t *r)
                 i = 0;
             }
 
+            for (n = 0; n < header_params; n++) {
+                if (&header[i] == ignored[n]) {
+                    goto next_length;
+                }
+            }
+
             if (params->number) {
                 if (allocated < header[i].key.len) {
                     allocated = header[i].key.len + 16;
@@ -959,15 +969,23 @@ ngx_http_fastcgi_create_request(ngx_http_request_t *r)
                     ignored[header_params++] = &header[i];
                     continue;
                 }
+            }
 
-                n += sizeof("HTTP_") - 1;
+            key_len = sizeof("HTTP_") - 1 + header[i].key.len;
 
-            } else {
-                n = sizeof("HTTP_") - 1 + header[i].key.len;
+            val_len = header[i].value.len;
+
+            for (hn = header[i].next; hn; hn = hn->next) {
+                val_len += hn->value.len + 2;
+                ignored[header_params++] = hn;
             }
 
-            len += ((n > 127) ? 4 : 1) + ((header[i].value.len > 127) ? 4 : 1)
-                + n + header[i].value.len;
+            len += ((key_len > 127) ? 4 : 1) + key_len
+                   + ((val_len > 127) ? 4 : 1) + val_len;
+
+        next_length:
+
+            continue;
         }
     }
 
@@ -1109,7 +1127,7 @@ ngx_http_fastcgi_create_request(ngx_http_request_t *r)
 
             for (n = 0; n < header_params; n++) {
                 if (&header[i] == ignored[n]) {
-                    goto next;
+                    goto next_value;
                 }
             }
 
@@ -1125,6 +1143,11 @@ ngx_http_fastcgi_create_request(ngx_http_request_t *r)
             }
 
             val_len = header[i].value.len;
+
+            for (hn = header[i].next; hn; hn = hn->next) {
+                val_len += hn->value.len + 2;
+            }
+
             if (val_len > 127) {
                 *b->last++ = (u_char) (((val_len >> 24) & 0x7f) | 0x80);
                 *b->last++ = (u_char) ((val_len >> 16) & 0xff);
@@ -1150,13 +1173,34 @@ ngx_http_fastcgi_create_request(ngx_http_request_t *r)
                 *b->last++ = ch;
             }
 
-            b->last = ngx_copy(b->last, header[i].value.data, val_len);
+            b->last = ngx_copy(b->last, header[i].value.data,
+                               header[i].value.len);
+
+            if (header[i].next) {
+
+                if (header[i].key.len == sizeof("Cookie") - 1
+                    && ngx_strncasecmp(header[i].key.data, (u_char *) "Cookie",
+                                       sizeof("Cookie") - 1)
+                       == 0)
+                {
+                    sep = ';';
+
+                } else {
+                    sep = ',';
+                }
+
+                for (hn = header[i].next; hn; hn = hn->next) {
+                    *b->last++ = sep;
+                    *b->last++ = ' ';
+                    b->last = ngx_copy(b->last, hn->value.data, hn->value.len);
+                }
+            }
 
             ngx_log_debug4(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                            "fastcgi param: \"%*s: %*s\"",
                            key_len, b->last - (key_len + val_len),
                            val_len, b->last - val_len);
-        next:
+        next_value:
 
             continue;
         }
@@ -1963,8 +2007,12 @@ ngx_http_fastcgi_process_header(ngx_http_request_t *r)
                 hh = ngx_hash_find(&umcf->headers_in_hash, h->hash,
                                    h->lowcase_key, h->key.len);
 
-                if (hh && hh->handler(r, h, hh->offset) != NGX_OK) {
-                    return NGX_ERROR;
+                if (hh) {
+                    rc = hh->handler(r, h, hh->offset);
+
+                    if (rc != NGX_OK) {
+                        return rc;
+                    }
                 }
 
                 ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
@@ -2000,7 +2048,10 @@ ngx_http_fastcgi_process_header(ngx_http_request_t *r)
                     }
 
                     u->headers_in.status_n = status;
-                    u->headers_in.status_line = *status_line;
+
+                    if (status_line->len > 3) {
+                        u->headers_in.status_line = *status_line;
+                    }
 
                 } else if (u->headers_in.location) {
                     u->headers_in.status_n = 302;
diff --git a/src/http/modules/ngx_http_flv_module.c b/src/http/modules/ngx_http_flv_module.c
index cc06d53..ef2bff4 100644
--- a/src/http/modules/ngx_http_flv_module.c
+++ b/src/http/modules/ngx_http_flv_module.c
@@ -232,9 +232,10 @@ ngx_http_flv_handler(ngx_http_request_t *r)
     b->file_pos = start;
     b->file_last = of.size;
 
-    b->in_file = b->file_last ? 1: 0;
+    b->in_file = b->file_last ? 1 : 0;
     b->last_buf = (r == r->main) ? 1 : 0;
     b->last_in_chain = 1;
+    b->sync = (b->last_buf || b->in_file) ? 0 : 1;
 
     b->file->fd = of.fd;
     b->file->name = path;
diff --git a/src/http/modules/ngx_http_geo_module.c b/src/http/modules/ngx_http_geo_module.c
index 153b6aa..75c0397 100644
--- a/src/http/modules/ngx_http_geo_module.c
+++ b/src/http/modules/ngx_http_geo_module.c
@@ -199,7 +199,7 @@ ngx_http_geo_cidr_variable(ngx_http_request_t *r, ngx_http_variable_value_t *v,
         p = inaddr6->s6_addr;
 
         if (IN6_IS_ADDR_V4MAPPED(inaddr6)) {
-            inaddr = p[12] << 24;
+            inaddr = (in_addr_t) p[12] << 24;
             inaddr += p[13] << 16;
             inaddr += p[14] << 8;
             inaddr += p[15];
@@ -272,7 +272,7 @@ ngx_http_geo_range_variable(ngx_http_request_t *r, ngx_http_variable_value_t *v,
             if (IN6_IS_ADDR_V4MAPPED(inaddr6)) {
                 p = inaddr6->s6_addr;
 
-                inaddr = p[12] << 24;
+                inaddr = (in_addr_t) p[12] << 24;
                 inaddr += p[13] << 16;
                 inaddr += p[14] << 8;
                 inaddr += p[15];
@@ -327,15 +327,15 @@ static ngx_int_t
 ngx_http_geo_addr(ngx_http_request_t *r, ngx_http_geo_ctx_t *ctx,
     ngx_addr_t *addr)
 {
-    ngx_array_t  *xfwd;
+    ngx_table_elt_t  *xfwd;
 
     if (ngx_http_geo_real_addr(r, ctx, addr) != NGX_OK) {
         return NGX_ERROR;
     }
 
-    xfwd = &r->headers_in.x_forwarded_for;
+    xfwd = r->headers_in.x_forwarded_for;
 
-    if (xfwd->nelts > 0 && ctx->proxies != NULL) {
+    if (xfwd != NULL && ctx->proxies != NULL) {
         (void) ngx_http_get_forwarded_addr(r, addr, xfwd, NULL,
                                            ctx->proxies, ctx->proxy_recursive);
     }
@@ -1259,7 +1259,7 @@ ngx_http_geo_value(ngx_conf_t *cf, ngx_http_geo_conf_ctx_t *ctx,
         return gvvn->value;
     }
 
-    val = ngx_palloc(ctx->pool, sizeof(ngx_http_variable_value_t));
+    val = ngx_pcalloc(ctx->pool, sizeof(ngx_http_variable_value_t));
     if (val == NULL) {
         return NULL;
     }
@@ -1271,8 +1271,6 @@ ngx_http_geo_value(ngx_conf_t *cf, ngx_http_geo_conf_ctx_t *ctx,
     }
 
     val->valid = 1;
-    val->no_cacheable = 0;
-    val->not_found = 0;
 
     gvvn = ngx_palloc(ctx->temp_pool,
                       sizeof(ngx_http_geo_variable_value_node_t));
diff --git a/src/http/modules/ngx_http_geoip_module.c b/src/http/modules/ngx_http_geoip_module.c
index 5ea4f5f..5b8b11f 100644
--- a/src/http/modules/ngx_http_geoip_module.c
+++ b/src/http/modules/ngx_http_geoip_module.c
@@ -240,16 +240,16 @@ static u_long
 ngx_http_geoip_addr(ngx_http_request_t *r, ngx_http_geoip_conf_t *gcf)
 {
     ngx_addr_t           addr;
-    ngx_array_t         *xfwd;
+    ngx_table_elt_t     *xfwd;
     struct sockaddr_in  *sin;
 
     addr.sockaddr = r->connection->sockaddr;
     addr.socklen = r->connection->socklen;
     /* addr.name = r->connection->addr_text; */
 
-    xfwd = &r->headers_in.x_forwarded_for;
+    xfwd = r->headers_in.x_forwarded_for;
 
-    if (xfwd->nelts > 0 && gcf->proxies != NULL) {
+    if (xfwd != NULL && gcf->proxies != NULL) {
         (void) ngx_http_get_forwarded_addr(r, &addr, xfwd, NULL,
                                            gcf->proxies, gcf->proxy_recursive);
     }
@@ -266,7 +266,7 @@ ngx_http_geoip_addr(ngx_http_request_t *r, ngx_http_geoip_conf_t *gcf)
         if (IN6_IS_ADDR_V4MAPPED(inaddr6)) {
             p = inaddr6->s6_addr;
 
-            inaddr = p[12] << 24;
+            inaddr = (in_addr_t) p[12] << 24;
             inaddr += p[13] << 16;
             inaddr += p[14] << 8;
             inaddr += p[15];
@@ -292,7 +292,7 @@ static geoipv6_t
 ngx_http_geoip_addr_v6(ngx_http_request_t *r, ngx_http_geoip_conf_t *gcf)
 {
     ngx_addr_t            addr;
-    ngx_array_t          *xfwd;
+    ngx_table_elt_t      *xfwd;
     in_addr_t             addr4;
     struct in6_addr       addr6;
     struct sockaddr_in   *sin;
@@ -302,9 +302,9 @@ ngx_http_geoip_addr_v6(ngx_http_request_t *r, ngx_http_geoip_conf_t *gcf)
     addr.socklen = r->connection->socklen;
     /* addr.name = r->connection->addr_text; */
 
-    xfwd = &r->headers_in.x_forwarded_for;
+    xfwd = r->headers_in.x_forwarded_for;
 
-    if (xfwd->nelts > 0 && gcf->proxies != NULL) {
+    if (xfwd != NULL && gcf->proxies != NULL) {
         (void) ngx_http_get_forwarded_addr(r, &addr, xfwd, NULL,
                                            gcf->proxies, gcf->proxy_recursive);
     }
diff --git a/src/http/modules/ngx_http_grpc_module.c b/src/http/modules/ngx_http_grpc_module.c
index 864fc4f..e7726f3 100644
--- a/src/http/modules/ngx_http_grpc_module.c
+++ b/src/http/modules/ngx_http_grpc_module.c
@@ -209,6 +209,8 @@ static char *ngx_http_grpc_ssl_password_file(ngx_conf_t *cf,
     ngx_command_t *cmd, void *conf);
 static char *ngx_http_grpc_ssl_conf_command_check(ngx_conf_t *cf, void *post,
     void *data);
+static ngx_int_t ngx_http_grpc_merge_ssl(ngx_conf_t *cf,
+    ngx_http_grpc_loc_conf_t *conf, ngx_http_grpc_loc_conf_t *prev);
 static ngx_int_t ngx_http_grpc_set_ssl(ngx_conf_t *cf,
     ngx_http_grpc_loc_conf_t *glcf);
 #endif
@@ -562,7 +564,7 @@ ngx_http_grpc_handler(ngx_http_request_t *r)
         ctx->host = glcf->host;
 
 #if (NGX_HTTP_SSL)
-        u->ssl = (glcf->upstream.ssl != NULL);
+        u->ssl = glcf->ssl;
 
         if (u->ssl) {
             ngx_str_set(&u->schema, "grpcs://");
@@ -1229,7 +1231,7 @@ ngx_http_grpc_body_output_filter(void *data, ngx_chain_t *in)
     ngx_buf_t              *b;
     ngx_int_t               rc;
     ngx_uint_t              next, last;
-    ngx_chain_t            *cl, *out, **ll;
+    ngx_chain_t            *cl, *out, *ln, **ll;
     ngx_http_upstream_t    *u;
     ngx_http_grpc_ctx_t    *ctx;
     ngx_http_grpc_frame_t  *f;
@@ -1457,7 +1459,10 @@ ngx_http_grpc_body_output_filter(void *data, ngx_chain_t *in)
             last = 1;
         }
 
+        ln = in;
         in = in->next;
+
+        ngx_free_chain(r->pool, ln);
     }
 
     ctx->in = in;
@@ -1891,8 +1896,12 @@ ngx_http_grpc_process_header(ngx_http_request_t *r)
                 hh = ngx_hash_find(&umcf->headers_in_hash, h->hash,
                                    h->lowcase_key, h->key.len);
 
-                if (hh && hh->handler(r, h, hh->offset) != NGX_OK) {
-                    return NGX_ERROR;
+                if (hh) {
+                    rc = hh->handler(r, h, hh->offset);
+
+                    if (rc != NGX_OK) {
+                        return rc;
+                    }
                 }
 
                 continue;
@@ -4459,12 +4468,17 @@ ngx_http_grpc_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child)
 
 #if (NGX_HTTP_SSL)
 
+    if (ngx_http_grpc_merge_ssl(cf, conf, prev) != NGX_OK) {
+        return NGX_CONF_ERROR;
+    }
+
     ngx_conf_merge_value(conf->upstream.ssl_session_reuse,
                               prev->upstream.ssl_session_reuse, 1);
 
     ngx_conf_merge_bitmask_value(conf->ssl_protocols, prev->ssl_protocols,
-                                 (NGX_CONF_BITMASK_SET|NGX_SSL_TLSv1
-                                  |NGX_SSL_TLSv1_1|NGX_SSL_TLSv1_2));
+                                 (NGX_CONF_BITMASK_SET
+                                  |NGX_SSL_TLSv1|NGX_SSL_TLSv1_1
+                                  |NGX_SSL_TLSv1_2|NGX_SSL_TLSv1_3));
 
     ngx_conf_merge_str_value(conf->ssl_ciphers, prev->ssl_ciphers,
                              "DEFAULT");
@@ -4520,7 +4534,7 @@ ngx_http_grpc_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child)
         conf->grpc_values = prev->grpc_values;
 
 #if (NGX_HTTP_SSL)
-        conf->upstream.ssl = prev->upstream.ssl;
+        conf->ssl = prev->ssl;
 #endif
     }
 
@@ -4870,16 +4884,62 @@ ngx_http_grpc_ssl_conf_command_check(ngx_conf_t *cf, void *post, void *data)
 
 
 static ngx_int_t
-ngx_http_grpc_set_ssl(ngx_conf_t *cf, ngx_http_grpc_loc_conf_t *glcf)
+ngx_http_grpc_merge_ssl(ngx_conf_t *cf, ngx_http_grpc_loc_conf_t *conf,
+    ngx_http_grpc_loc_conf_t *prev)
 {
-    ngx_pool_cleanup_t  *cln;
+    ngx_uint_t  preserve;
+
+    if (conf->ssl_protocols == 0
+        && conf->ssl_ciphers.data == NULL
+        && conf->upstream.ssl_certificate == NGX_CONF_UNSET_PTR
+        && conf->upstream.ssl_certificate_key == NGX_CONF_UNSET_PTR
+        && conf->upstream.ssl_passwords == NGX_CONF_UNSET_PTR
+        && conf->upstream.ssl_verify == NGX_CONF_UNSET
+        && conf->ssl_verify_depth == NGX_CONF_UNSET_UINT
+        && conf->ssl_trusted_certificate.data == NULL
+        && conf->ssl_crl.data == NULL
+        && conf->upstream.ssl_session_reuse == NGX_CONF_UNSET
+        && conf->ssl_conf_commands == NGX_CONF_UNSET_PTR)
+    {
+        if (prev->upstream.ssl) {
+            conf->upstream.ssl = prev->upstream.ssl;
+            return NGX_OK;
+        }
 
-    glcf->upstream.ssl = ngx_pcalloc(cf->pool, sizeof(ngx_ssl_t));
-    if (glcf->upstream.ssl == NULL) {
+        preserve = 1;
+
+    } else {
+        preserve = 0;
+    }
+
+    conf->upstream.ssl = ngx_pcalloc(cf->pool, sizeof(ngx_ssl_t));
+    if (conf->upstream.ssl == NULL) {
         return NGX_ERROR;
     }
 
-    glcf->upstream.ssl->log = cf->log;
+    conf->upstream.ssl->log = cf->log;
+
+    /*
+     * special handling to preserve conf->upstream.ssl
+     * in the "http" section to inherit it to all servers
+     */
+
+    if (preserve) {
+        prev->upstream.ssl = conf->upstream.ssl;
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_http_grpc_set_ssl(ngx_conf_t *cf, ngx_http_grpc_loc_conf_t *glcf)
+{
+    ngx_pool_cleanup_t  *cln;
+
+    if (glcf->upstream.ssl->ctx) {
+        return NGX_OK;
+    }
 
     if (ngx_ssl_create(glcf->upstream.ssl, glcf->ssl_protocols, NULL)
         != NGX_OK)
@@ -4902,8 +4962,9 @@ ngx_http_grpc_set_ssl(ngx_conf_t *cf, ngx_http_grpc_loc_conf_t *glcf)
         return NGX_ERROR;
     }
 
-    if (glcf->upstream.ssl_certificate) {
-
+    if (glcf->upstream.ssl_certificate
+        && glcf->upstream.ssl_certificate->value.len)
+    {
         if (glcf->upstream.ssl_certificate_key == NULL) {
             ngx_log_error(NGX_LOG_EMERG, cf->log, 0,
                           "no \"grpc_ssl_certificate_key\" is defined "
diff --git a/src/http/modules/ngx_http_gunzip_filter_module.c b/src/http/modules/ngx_http_gunzip_filter_module.c
index c1341f5..5d170a1 100644
--- a/src/http/modules/ngx_http_gunzip_filter_module.c
+++ b/src/http/modules/ngx_http_gunzip_filter_module.c
@@ -333,6 +333,8 @@ static ngx_int_t
 ngx_http_gunzip_filter_add_data(ngx_http_request_t *r,
     ngx_http_gunzip_ctx_t *ctx)
 {
+    ngx_chain_t  *cl;
+
     if (ctx->zstream.avail_in || ctx->flush != Z_NO_FLUSH || ctx->redo) {
         return NGX_OK;
     }
@@ -344,8 +346,11 @@ ngx_http_gunzip_filter_add_data(ngx_http_request_t *r,
         return NGX_DECLINED;
     }
 
-    ctx->in_buf = ctx->in->buf;
-    ctx->in = ctx->in->next;
+    cl = ctx->in;
+    ctx->in_buf = cl->buf;
+    ctx->in = cl->next;
+
+    ngx_free_chain(r->pool, cl);
 
     ctx->zstream.next_in = ctx->in_buf->pos;
     ctx->zstream.avail_in = ctx->in_buf->last - ctx->in_buf->pos;
@@ -374,6 +379,7 @@ static ngx_int_t
 ngx_http_gunzip_filter_get_buf(ngx_http_request_t *r,
     ngx_http_gunzip_ctx_t *ctx)
 {
+    ngx_chain_t             *cl;
     ngx_http_gunzip_conf_t  *conf;
 
     if (ctx->zstream.avail_out) {
@@ -383,8 +389,12 @@ ngx_http_gunzip_filter_get_buf(ngx_http_request_t *r,
     conf = ngx_http_get_module_loc_conf(r, ngx_http_gunzip_filter_module);
 
     if (ctx->free) {
-        ctx->out_buf = ctx->free->buf;
-        ctx->free = ctx->free->next;
+
+        cl = ctx->free;
+        ctx->out_buf = cl->buf;
+        ctx->free = cl->next;
+
+        ngx_free_chain(r->pool, cl);
 
         ctx->out_buf->flush = 0;
 
diff --git a/src/http/modules/ngx_http_gzip_filter_module.c b/src/http/modules/ngx_http_gzip_filter_module.c
index b8c5ccc..7113df6 100644
--- a/src/http/modules/ngx_http_gzip_filter_module.c
+++ b/src/http/modules/ngx_http_gzip_filter_module.c
@@ -57,6 +57,7 @@ typedef struct {
     unsigned             nomem:1;
     unsigned             buffering:1;
     unsigned             zlib_ng:1;
+    unsigned             state_allocated:1;
 
     size_t               zin;
     size_t               zout;
@@ -280,6 +281,7 @@ ngx_http_gzip_header_filter(ngx_http_request_t *r)
     }
 
     h->hash = 1;
+    h->next = NULL;
     ngx_str_set(&h->key, "Content-Encoding");
     ngx_str_set(&h->value, "gzip");
     r->headers_out.content_encoding = h;
@@ -513,9 +515,12 @@ ngx_http_gzip_filter_memory(ngx_http_request_t *r, ngx_http_gzip_ctx_t *ctx)
     } else {
         /*
          * Another zlib variant, https://github.com/zlib-ng/zlib-ng.
-         * It forces window bits to 13 for fast compression level,
-         * uses 16-byte padding in one of window-sized buffers, and
-         * uses 128K hash.
+         * It used to force window bits to 13 for fast compression level,
+         * used (64 + sizeof(void*)) additional space on all allocations
+         * for alignment and 16-byte padding in one of window-sized buffers,
+         * uses a single allocation with up to 200 bytes for alignment and
+         * internal pointers, 5/4 times more memory for the pending buffer,
+         * and 128K hash.
          */
 
         if (conf->level == 1) {
@@ -523,7 +528,8 @@ ngx_http_gzip_filter_memory(ngx_http_request_t *r, ngx_http_gzip_ctx_t *ctx)
         }
 
         ctx->allocated = 8192 + 16 + (1 << (wbits + 2))
-                         + 131072 + (1 << (memlevel + 8));
+                         + 131072 + (5 << (memlevel + 6))
+                         + 4 * (64 + sizeof(void*));
         ctx->zlib_ng = 1;
     }
 }
@@ -925,13 +931,16 @@ ngx_http_gzip_filter_alloc(void *opaque, u_int items, u_int size)
 
     alloc = items * size;
 
-    if (items == 1 && alloc % 512 != 0 && alloc < 8192) {
-
+    if (items == 1 && alloc % 512 != 0 && alloc < 8192
+        && !ctx->state_allocated)
+    {
         /*
          * The zlib deflate_state allocation, it takes about 6K,
          * we allocate 8K.  Other allocations are divisible by 512.
          */
 
+        ctx->state_allocated = 1;
+
         alloc = 8192;
     }
 
@@ -978,10 +987,14 @@ static void
 ngx_http_gzip_filter_free_copy_buf(ngx_http_request_t *r,
     ngx_http_gzip_ctx_t *ctx)
 {
-    ngx_chain_t  *cl;
+    ngx_chain_t  *cl, *ln;
+
+    for (cl = ctx->copied; cl; /* void */) {
+        ln = cl;
+        cl = cl->next;
 
-    for (cl = ctx->copied; cl; cl = cl->next) {
-        ngx_pfree(r->pool, cl->buf->start);
+        ngx_pfree(r->pool, ln->buf->start);
+        ngx_free_chain(r->pool, ln);
     }
 
     ctx->copied = NULL;
diff --git a/src/http/modules/ngx_http_gzip_static_module.c b/src/http/modules/ngx_http_gzip_static_module.c
index 7652a9a..91b38d1 100644
--- a/src/http/modules/ngx_http_gzip_static_module.c
+++ b/src/http/modules/ngx_http_gzip_static_module.c
@@ -242,10 +242,13 @@ ngx_http_gzip_static_handler(ngx_http_request_t *r)
     }
 
     h->hash = 1;
+    h->next = NULL;
     ngx_str_set(&h->key, "Content-Encoding");
     ngx_str_set(&h->value, "gzip");
     r->headers_out.content_encoding = h;
 
+    r->allow_ranges = 1;
+
     /* we need to allocate all before the header would be sent */
 
     b = ngx_calloc_buf(r->pool);
@@ -270,6 +273,7 @@ ngx_http_gzip_static_handler(ngx_http_request_t *r)
     b->in_file = b->file_last ? 1 : 0;
     b->last_buf = (r == r->main) ? 1 : 0;
     b->last_in_chain = 1;
+    b->sync = (b->last_buf || b->in_file) ? 0 : 1;
 
     b->file->fd = of.fd;
     b->file->name = path;
diff --git a/src/http/modules/ngx_http_headers_filter_module.c b/src/http/modules/ngx_http_headers_filter_module.c
index a4c8cc2..90e6da8 100644
--- a/src/http/modules/ngx_http_headers_filter_module.c
+++ b/src/http/modules/ngx_http_headers_filter_module.c
@@ -329,8 +329,7 @@ ngx_http_set_expires(ngx_http_request_t *r, ngx_http_headers_conf_t *conf)
     time_t               now, expires_time, max_age;
     ngx_str_t            value;
     ngx_int_t            rc;
-    ngx_uint_t           i;
-    ngx_table_elt_t     *e, *cc, **ccp;
+    ngx_table_elt_t     *e, *cc;
     ngx_http_expires_t   expires;
 
     expires = conf->expires;
@@ -363,6 +362,7 @@ ngx_http_set_expires(ngx_http_request_t *r, ngx_http_headers_conf_t *conf)
         }
 
         r->headers_out.expires = e;
+        e->next = NULL;
 
         e->hash = 1;
         ngx_str_set(&e->key, "Expires");
@@ -371,38 +371,29 @@ ngx_http_set_expires(ngx_http_request_t *r, ngx_http_headers_conf_t *conf)
     len = sizeof("Mon, 28 Sep 1970 06:00:00 GMT");
     e->value.len = len - 1;
 
-    ccp = r->headers_out.cache_control.elts;
+    cc = r->headers_out.cache_control;
 
-    if (ccp == NULL) {
-
-        if (ngx_array_init(&r->headers_out.cache_control, r->pool,
-                           1, sizeof(ngx_table_elt_t *))
-            != NGX_OK)
-        {
-            return NGX_ERROR;
-        }
+    if (cc == NULL) {
 
         cc = ngx_list_push(&r->headers_out.headers);
         if (cc == NULL) {
+            e->hash = 0;
             return NGX_ERROR;
         }
 
+        r->headers_out.cache_control = cc;
+        cc->next = NULL;
+
         cc->hash = 1;
         ngx_str_set(&cc->key, "Cache-Control");
 
-        ccp = ngx_array_push(&r->headers_out.cache_control);
-        if (ccp == NULL) {
-            return NGX_ERROR;
-        }
-
-        *ccp = cc;
-
     } else {
-        for (i = 1; i < r->headers_out.cache_control.nelts; i++) {
-            ccp[i]->hash = 0;
+        for (cc = cc->next; cc; cc = cc->next) {
+            cc->hash = 0;
         }
 
-        cc = ccp[0];
+        cc = r->headers_out.cache_control;
+        cc->next = NULL;
     }
 
     if (expires == NGX_HTTP_EXPIRES_EPOCH) {
@@ -420,6 +411,8 @@ ngx_http_set_expires(ngx_http_request_t *r, ngx_http_headers_conf_t *conf)
 
     e->value.data = ngx_pnalloc(r->pool, len);
     if (e->value.data == NULL) {
+        e->hash = 0;
+        cc->hash = 0;
         return NGX_ERROR;
     }
 
@@ -457,6 +450,7 @@ ngx_http_set_expires(ngx_http_request_t *r, ngx_http_headers_conf_t *conf)
     cc->value.data = ngx_pnalloc(r->pool,
                                  sizeof("max-age=") + NGX_TIME_T_LEN + 1);
     if (cc->value.data == NULL) {
+        cc->hash = 0;
         return NGX_ERROR;
     }
 
@@ -564,22 +558,12 @@ static ngx_int_t
 ngx_http_add_multi_header_lines(ngx_http_request_t *r,
     ngx_http_header_val_t *hv, ngx_str_t *value)
 {
-    ngx_array_t      *pa;
     ngx_table_elt_t  *h, **ph;
 
     if (value->len == 0) {
         return NGX_OK;
     }
 
-    pa = (ngx_array_t *) ((char *) &r->headers_out + hv->offset);
-
-    if (pa->elts == NULL) {
-        if (ngx_array_init(pa, r->pool, 1, sizeof(ngx_table_elt_t *)) != NGX_OK)
-        {
-            return NGX_ERROR;
-        }
-    }
-
     h = ngx_list_push(&r->headers_out.headers);
     if (h == NULL) {
         return NGX_ERROR;
@@ -589,12 +573,12 @@ ngx_http_add_multi_header_lines(ngx_http_request_t *r,
     h->key = hv->key;
     h->value = *value;
 
-    ph = ngx_array_push(pa);
-    if (ph == NULL) {
-        return NGX_ERROR;
-    }
+    ph = (ngx_table_elt_t **) ((char *) &r->headers_out + hv->offset);
+
+    while (*ph) { ph = &(*ph)->next; }
 
     *ph = h;
+    h->next = NULL;
 
     return NGX_OK;
 }
@@ -642,6 +626,7 @@ ngx_http_set_response_header(ngx_http_request_t *r, ngx_http_header_val_t *hv,
         }
 
         *old = h;
+        h->next = NULL;
     }
 
     h->hash = 1;
diff --git a/src/http/modules/ngx_http_memcached_module.c b/src/http/modules/ngx_http_memcached_module.c
index c82df6e..11bbd91 100644
--- a/src/http/modules/ngx_http_memcached_module.c
+++ b/src/http/modules/ngx_http_memcached_module.c
@@ -401,6 +401,7 @@ found:
             }
 
             h->hash = 1;
+            h->next = NULL;
             ngx_str_set(&h->key, "Content-Encoding");
             ngx_str_set(&h->value, "gzip");
             r->headers_out.content_encoding = h;
diff --git a/src/http/modules/ngx_http_mp4_module.c b/src/http/modules/ngx_http_mp4_module.c
index 4eff01e..b7bd192 100644
--- a/src/http/modules/ngx_http_mp4_module.c
+++ b/src/http/modules/ngx_http_mp4_module.c
@@ -714,6 +714,7 @@ ngx_http_mp4_handler(ngx_http_request_t *r)
     b->in_file = b->file_last ? 1 : 0;
     b->last_buf = (r == r->main) ? 1 : 0;
     b->last_in_chain = 1;
+    b->sync = (b->last_buf || b->in_file) ? 0 : 1;
 
     b->file->fd = of.fd;
     b->file->name = path;
@@ -2430,7 +2431,7 @@ ngx_http_mp4_crop_stts_data(ngx_http_mp4_file_t *mp4,
         }
 
         start_sample += count;
-        start_time -= count * duration;
+        start_time -= (uint64_t) count * duration;
         entries--;
         entry++;
     }
@@ -3098,7 +3099,8 @@ static ngx_int_t
 ngx_http_mp4_crop_stsc_data(ngx_http_mp4_file_t *mp4,
     ngx_http_mp4_trak_t *trak, ngx_uint_t start)
 {
-    uint32_t               start_sample, chunk, samples, id, next_chunk, n,
+    uint64_t               n;
+    uint32_t               start_sample, chunk, samples, id, next_chunk,
                            prev_samples;
     ngx_buf_t             *data, *buf;
     ngx_uint_t             entries, target_chunk, chunk_samples;
@@ -3154,12 +3156,19 @@ ngx_http_mp4_crop_stsc_data(ngx_http_mp4_file_t *mp4,
 
         next_chunk = ngx_mp4_get_32value(entry->chunk);
 
+        if (next_chunk < chunk) {
+            ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
+                          "unordered mp4 stsc chunks in \"%s\"",
+                          mp4->file.name.data);
+            return NGX_ERROR;
+        }
+
         ngx_log_debug5(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                        "sample:%uD, chunk:%uD, chunks:%uD, "
                        "samples:%uD, id:%uD",
                        start_sample, chunk, next_chunk - chunk, samples, id);
 
-        n = (next_chunk - chunk) * samples;
+        n = (uint64_t) (next_chunk - chunk) * samples;
 
         if (start_sample < n) {
             goto found;
@@ -3167,7 +3176,10 @@ ngx_http_mp4_crop_stsc_data(ngx_http_mp4_file_t *mp4,
 
         start_sample -= n;
 
-        prev_samples = samples;
+        if (next_chunk > chunk) {
+            prev_samples = samples;
+        }
+
         chunk = next_chunk;
         samples = ngx_mp4_get_32value(entry->samples);
         id = ngx_mp4_get_32value(entry->id);
@@ -3177,11 +3189,18 @@ ngx_http_mp4_crop_stsc_data(ngx_http_mp4_file_t *mp4,
 
     next_chunk = trak->chunks + 1;
 
+    if (next_chunk < chunk) {
+        ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
+                      "unordered mp4 stsc chunks in \"%s\"",
+                      mp4->file.name.data);
+        return NGX_ERROR;
+    }
+
     ngx_log_debug4(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                    "sample:%uD, chunk:%uD, chunks:%uD, samples:%uD",
                    start_sample, chunk, next_chunk - chunk, samples);
 
-    n = (next_chunk - chunk) * samples;
+    n = (uint64_t) (next_chunk - chunk) * samples;
 
     if (start_sample > n) {
         ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
@@ -3202,6 +3221,12 @@ found:
         return NGX_ERROR;
     }
 
+    if (chunk == 0) {
+        ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
+                      "zero chunk in \"%s\"", mp4->file.name.data);
+        return NGX_ERROR;
+    }
+
     target_chunk = chunk - 1;
     target_chunk += start_sample / samples;
     chunk_samples = start_sample % samples;
diff --git a/src/http/modules/ngx_http_proxy_module.c b/src/http/modules/ngx_http_proxy_module.c
index 7c4061c..9cc202c 100644
--- a/src/http/modules/ngx_http_proxy_module.c
+++ b/src/http/modules/ngx_http_proxy_module.c
@@ -236,6 +236,8 @@ static ngx_int_t ngx_http_proxy_rewrite_regex(ngx_conf_t *cf,
     ngx_http_proxy_rewrite_t *pr, ngx_str_t *regex, ngx_uint_t caseless);
 
 #if (NGX_HTTP_SSL)
+static ngx_int_t ngx_http_proxy_merge_ssl(ngx_conf_t *cf,
+    ngx_http_proxy_loc_conf_t *conf, ngx_http_proxy_loc_conf_t *prev);
 static ngx_int_t ngx_http_proxy_set_ssl(ngx_conf_t *cf,
     ngx_http_proxy_loc_conf_t *plcf);
 #endif
@@ -959,7 +961,7 @@ ngx_http_proxy_handler(ngx_http_request_t *r)
         ctx->vars = plcf->vars;
         u->schema = plcf->vars.schema;
 #if (NGX_HTTP_SSL)
-        u->ssl = (plcf->upstream.ssl != NULL);
+        u->ssl = plcf->ssl;
 #endif
 
     } else {
@@ -1930,8 +1932,12 @@ ngx_http_proxy_process_header(ngx_http_request_t *r)
             hh = ngx_hash_find(&umcf->headers_in_hash, h->hash,
                                h->lowcase_key, h->key.len);
 
-            if (hh && hh->handler(r, h, hh->offset) != NGX_OK) {
-                return NGX_ERROR;
+            if (hh) {
+                rc = hh->handler(r, h, hh->offset);
+
+                if (rc != NGX_OK) {
+                    return rc;
+                }
             }
 
             ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
@@ -1965,6 +1971,7 @@ ngx_http_proxy_process_header(ngx_http_request_t *r)
                 ngx_str_set(&h->key, "Server");
                 ngx_str_null(&h->value);
                 h->lowcase_key = (u_char *) "server";
+                h->next = NULL;
             }
 
             if (r->upstream->headers_in.date == NULL) {
@@ -1978,6 +1985,7 @@ ngx_http_proxy_process_header(ngx_http_request_t *r)
                 ngx_str_set(&h->key, "Date");
                 ngx_str_null(&h->value);
                 h->lowcase_key = (u_char *) "date";
+                h->next = NULL;
             }
 
             /* clear content length if response is chunked */
@@ -2559,22 +2567,20 @@ static ngx_int_t
 ngx_http_proxy_add_x_forwarded_for_variable(ngx_http_request_t *r,
     ngx_http_variable_value_t *v, uintptr_t data)
 {
-    size_t             len;
-    u_char            *p;
-    ngx_uint_t         i, n;
-    ngx_table_elt_t  **h;
+    size_t            len;
+    u_char           *p;
+    ngx_table_elt_t  *h, *xfwd;
 
     v->valid = 1;
     v->no_cacheable = 0;
     v->not_found = 0;
 
-    n = r->headers_in.x_forwarded_for.nelts;
-    h = r->headers_in.x_forwarded_for.elts;
+    xfwd = r->headers_in.x_forwarded_for;
 
     len = 0;
 
-    for (i = 0; i < n; i++) {
-        len += h[i]->value.len + sizeof(", ") - 1;
+    for (h = xfwd; h; h = h->next) {
+        len += h->value.len + sizeof(", ") - 1;
     }
 
     if (len == 0) {
@@ -2593,8 +2599,8 @@ ngx_http_proxy_add_x_forwarded_for_variable(ngx_http_request_t *r,
     v->len = len;
     v->data = p;
 
-    for (i = 0; i < n; i++) {
-        p = ngx_copy(p, h[i]->value.data, h[i]->value.len);
+    for (h = xfwd; h; h = h->next) {
+        p = ngx_copy(p, h->value.data, h->value.len);
         *p++ = ','; *p++ = ' ';
     }
 
@@ -3720,12 +3726,17 @@ ngx_http_proxy_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child)
 
 #if (NGX_HTTP_SSL)
 
+    if (ngx_http_proxy_merge_ssl(cf, conf, prev) != NGX_OK) {
+        return NGX_CONF_ERROR;
+    }
+
     ngx_conf_merge_value(conf->upstream.ssl_session_reuse,
                               prev->upstream.ssl_session_reuse, 1);
 
     ngx_conf_merge_bitmask_value(conf->ssl_protocols, prev->ssl_protocols,
-                                 (NGX_CONF_BITMASK_SET|NGX_SSL_TLSv1
-                                  |NGX_SSL_TLSv1_1|NGX_SSL_TLSv1_2));
+                                 (NGX_CONF_BITMASK_SET
+                                  |NGX_SSL_TLSv1|NGX_SSL_TLSv1_1
+                                  |NGX_SSL_TLSv1_2|NGX_SSL_TLSv1_3));
 
     ngx_conf_merge_str_value(conf->ssl_ciphers, prev->ssl_ciphers,
                              "DEFAULT");
@@ -3853,7 +3864,7 @@ ngx_http_proxy_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child)
         conf->proxy_values = prev->proxy_values;
 
 #if (NGX_HTTP_SSL)
-        conf->upstream.ssl = prev->upstream.ssl;
+        conf->ssl = prev->ssl;
 #endif
     }
 
@@ -4919,16 +4930,62 @@ ngx_http_proxy_ssl_conf_command_check(ngx_conf_t *cf, void *post, void *data)
 
 
 static ngx_int_t
-ngx_http_proxy_set_ssl(ngx_conf_t *cf, ngx_http_proxy_loc_conf_t *plcf)
+ngx_http_proxy_merge_ssl(ngx_conf_t *cf, ngx_http_proxy_loc_conf_t *conf,
+    ngx_http_proxy_loc_conf_t *prev)
 {
-    ngx_pool_cleanup_t  *cln;
+    ngx_uint_t  preserve;
+
+    if (conf->ssl_protocols == 0
+        && conf->ssl_ciphers.data == NULL
+        && conf->upstream.ssl_certificate == NGX_CONF_UNSET_PTR
+        && conf->upstream.ssl_certificate_key == NGX_CONF_UNSET_PTR
+        && conf->upstream.ssl_passwords == NGX_CONF_UNSET_PTR
+        && conf->upstream.ssl_verify == NGX_CONF_UNSET
+        && conf->ssl_verify_depth == NGX_CONF_UNSET_UINT
+        && conf->ssl_trusted_certificate.data == NULL
+        && conf->ssl_crl.data == NULL
+        && conf->upstream.ssl_session_reuse == NGX_CONF_UNSET
+        && conf->ssl_conf_commands == NGX_CONF_UNSET_PTR)
+    {
+        if (prev->upstream.ssl) {
+            conf->upstream.ssl = prev->upstream.ssl;
+            return NGX_OK;
+        }
+
+        preserve = 1;
+
+    } else {
+        preserve = 0;
+    }
 
-    plcf->upstream.ssl = ngx_pcalloc(cf->pool, sizeof(ngx_ssl_t));
-    if (plcf->upstream.ssl == NULL) {
+    conf->upstream.ssl = ngx_pcalloc(cf->pool, sizeof(ngx_ssl_t));
+    if (conf->upstream.ssl == NULL) {
         return NGX_ERROR;
     }
 
-    plcf->upstream.ssl->log = cf->log;
+    conf->upstream.ssl->log = cf->log;
+
+    /*
+     * special handling to preserve conf->upstream.ssl
+     * in the "http" section to inherit it to all servers
+     */
+
+    if (preserve) {
+        prev->upstream.ssl = conf->upstream.ssl;
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_http_proxy_set_ssl(ngx_conf_t *cf, ngx_http_proxy_loc_conf_t *plcf)
+{
+    ngx_pool_cleanup_t  *cln;
+
+    if (plcf->upstream.ssl->ctx) {
+        return NGX_OK;
+    }
 
     if (ngx_ssl_create(plcf->upstream.ssl, plcf->ssl_protocols, NULL)
         != NGX_OK)
@@ -4951,8 +5008,9 @@ ngx_http_proxy_set_ssl(ngx_conf_t *cf, ngx_http_proxy_loc_conf_t *plcf)
         return NGX_ERROR;
     }
 
-    if (plcf->upstream.ssl_certificate) {
-
+    if (plcf->upstream.ssl_certificate
+        && plcf->upstream.ssl_certificate->value.len)
+    {
         if (plcf->upstream.ssl_certificate_key == NULL) {
             ngx_log_error(NGX_LOG_EMERG, cf->log, 0,
                           "no \"proxy_ssl_certificate_key\" is defined "
diff --git a/src/http/modules/ngx_http_range_filter_module.c b/src/http/modules/ngx_http_range_filter_module.c
index ae08ebb..27d1875 100644
--- a/src/http/modules/ngx_http_range_filter_module.c
+++ b/src/http/modules/ngx_http_range_filter_module.c
@@ -258,6 +258,7 @@ next_filter:
     }
 
     r->headers_out.accept_ranges->hash = 1;
+    r->headers_out.accept_ranges->next = NULL;
     ngx_str_set(&r->headers_out.accept_ranges->key, "Accept-Ranges");
     ngx_str_set(&r->headers_out.accept_ranges->value, "bytes");
 
@@ -424,9 +425,14 @@ ngx_http_range_singlepart_header(ngx_http_request_t *r,
         return NGX_ERROR;
     }
 
+    if (r->headers_out.content_range) {
+        r->headers_out.content_range->hash = 0;
+    }
+
     r->headers_out.content_range = content_range;
 
     content_range->hash = 1;
+    content_range->next = NULL;
     ngx_str_set(&content_range->key, "Content-Range");
 
     content_range->value.data = ngx_pnalloc(r->pool,
@@ -580,6 +586,11 @@ ngx_http_range_multipart_header(ngx_http_request_t *r,
         r->headers_out.content_length = NULL;
     }
 
+    if (r->headers_out.content_range) {
+        r->headers_out.content_range->hash = 0;
+        r->headers_out.content_range = NULL;
+    }
+
     return ngx_http_next_header_filter(r);
 }
 
@@ -596,9 +607,14 @@ ngx_http_range_not_satisfiable(ngx_http_request_t *r)
         return NGX_ERROR;
     }
 
+    if (r->headers_out.content_range) {
+        r->headers_out.content_range->hash = 0;
+    }
+
     r->headers_out.content_range = content_range;
 
     content_range->hash = 1;
+    content_range->next = NULL;
     ngx_str_set(&content_range->key, "Content-Range");
 
     content_range->value.data = ngx_pnalloc(r->pool,
diff --git a/src/http/modules/ngx_http_realip_module.c b/src/http/modules/ngx_http_realip_module.c
index 9586ebe..f6731e7 100644
--- a/src/http/modules/ngx_http_realip_module.c
+++ b/src/http/modules/ngx_http_realip_module.c
@@ -134,9 +134,8 @@ ngx_http_realip_handler(ngx_http_request_t *r)
     ngx_str_t                   *value;
     ngx_uint_t                   i, hash;
     ngx_addr_t                   addr;
-    ngx_array_t                 *xfwd;
     ngx_list_part_t             *part;
-    ngx_table_elt_t             *header;
+    ngx_table_elt_t             *header, *xfwd;
     ngx_connection_t            *c;
     ngx_http_realip_ctx_t       *ctx;
     ngx_http_realip_loc_conf_t  *rlcf;
@@ -168,9 +167,9 @@ ngx_http_realip_handler(ngx_http_request_t *r)
 
     case NGX_HTTP_REALIP_XFWD:
 
-        xfwd = &r->headers_in.x_forwarded_for;
+        xfwd = r->headers_in.x_forwarded_for;
 
-        if (xfwd->elts == NULL) {
+        if (xfwd == NULL) {
             return NGX_DECLINED;
         }
 
diff --git a/src/http/modules/ngx_http_referer_module.c b/src/http/modules/ngx_http_referer_module.c
index 599dd3a..11a681b 100644
--- a/src/http/modules/ngx_http_referer_module.c
+++ b/src/http/modules/ngx_http_referer_module.c
@@ -631,7 +631,7 @@ ngx_http_add_regex_referer(ngx_conf_t *cf, ngx_http_referer_conf_t *rlcf,
 #else
 
     ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
-                       "the using of the regex \"%V\" requires PCRE library",
+                       "using regex \"%V\" requires PCRE library",
                        name);
 
     return NGX_ERROR;
diff --git a/src/http/modules/ngx_http_rewrite_module.c b/src/http/modules/ngx_http_rewrite_module.c
index 0e6d4df..ff3b687 100644
--- a/src/http/modules/ngx_http_rewrite_module.c
+++ b/src/http/modules/ngx_http_rewrite_module.c
@@ -489,6 +489,7 @@ ngx_http_rewrite_return(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
         }
 
         if (cf->args->nelts == 2) {
+            ngx_str_set(&ret->text.value, "");
             return NGX_CONF_OK;
         }
 
diff --git a/src/http/modules/ngx_http_scgi_module.c b/src/http/modules/ngx_http_scgi_module.c
index e5d31ae..3acea87 100644
--- a/src/http/modules/ngx_http_scgi_module.c
+++ b/src/http/modules/ngx_http_scgi_module.c
@@ -633,14 +633,14 @@ static ngx_int_t
 ngx_http_scgi_create_request(ngx_http_request_t *r)
 {
     off_t                         content_length_n;
-    u_char                        ch, *key, *val, *lowcase_key;
+    u_char                        ch, sep, *key, *val, *lowcase_key;
     size_t                        len, key_len, val_len, allocated;
     ngx_buf_t                    *b;
     ngx_str_t                     content_length;
     ngx_uint_t                    i, n, hash, skip_empty, header_params;
     ngx_chain_t                  *cl, *body;
     ngx_list_part_t              *part;
-    ngx_table_elt_t              *header, **ignored;
+    ngx_table_elt_t              *header, *hn, **ignored;
     ngx_http_scgi_params_t       *params;
     ngx_http_script_code_pt       code;
     ngx_http_script_engine_t      e, le;
@@ -707,7 +707,11 @@ ngx_http_scgi_create_request(ngx_http_request_t *r)
         allocated = 0;
         lowcase_key = NULL;
 
-        if (params->number) {
+        if (ngx_http_link_multi_headers(r) != NGX_OK) {
+            return NGX_ERROR;
+        }
+
+        if (params->number || r->headers_in.multi) {
             n = 0;
             part = &r->headers_in.headers.part;
 
@@ -737,6 +741,12 @@ ngx_http_scgi_create_request(ngx_http_request_t *r)
                 i = 0;
             }
 
+            for (n = 0; n < header_params; n++) {
+                if (&header[i] == ignored[n]) {
+                    goto next_length;
+                }
+            }
+
             if (params->number) {
                 if (allocated < header[i].key.len) {
                     allocated = header[i].key.len + 16;
@@ -770,6 +780,15 @@ ngx_http_scgi_create_request(ngx_http_request_t *r)
 
             len += sizeof("HTTP_") - 1 + header[i].key.len + 1
                 + header[i].value.len + 1;
+
+            for (hn = header[i].next; hn; hn = hn->next) {
+                len += hn->value.len + 2;
+                ignored[header_params++] = hn;
+            }
+
+        next_length:
+
+            continue;
         }
     }
 
@@ -869,7 +888,7 @@ ngx_http_scgi_create_request(ngx_http_request_t *r)
 
             for (n = 0; n < header_params; n++) {
                 if (&header[i] == ignored[n]) {
-                    goto next;
+                    goto next_value;
                 }
             }
 
@@ -893,12 +912,33 @@ ngx_http_scgi_create_request(ngx_http_request_t *r)
 
             val = b->last;
             b->last = ngx_copy(val, header[i].value.data, header[i].value.len);
+
+            if (header[i].next) {
+
+                if (header[i].key.len == sizeof("Cookie") - 1
+                    && ngx_strncasecmp(header[i].key.data, (u_char *) "Cookie",
+                                       sizeof("Cookie") - 1)
+                       == 0)
+                {
+                    sep = ';';
+
+                } else {
+                    sep = ',';
+                }
+
+                for (hn = header[i].next; hn; hn = hn->next) {
+                    *b->last++ = sep;
+                    *b->last++ = ' ';
+                    b->last = ngx_copy(b->last, hn->value.data, hn->value.len);
+                }
+            }
+
             *b->last++ = (u_char) 0;
 
             ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                            "scgi param: \"%s: %s\"", key, val);
 
-        next:
+        next_value:
 
             continue;
         }
@@ -1074,8 +1114,12 @@ ngx_http_scgi_process_header(ngx_http_request_t *r)
             hh = ngx_hash_find(&umcf->headers_in_hash, h->hash,
                                h->lowcase_key, h->key.len);
 
-            if (hh && hh->handler(r, h, hh->offset) != NGX_OK) {
-                return NGX_ERROR;
+            if (hh) {
+                rc = hh->handler(r, h, hh->offset);
+
+                if (rc != NGX_OK) {
+                    return rc;
+                }
             }
 
             ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
@@ -1109,7 +1153,10 @@ ngx_http_scgi_process_header(ngx_http_request_t *r)
                 }
 
                 u->headers_in.status_n = status;
-                u->headers_in.status_line = *status_line;
+
+                if (status_line->len > 3) {
+                    u->headers_in.status_line = *status_line;
+                }
 
             } else if (u->headers_in.location) {
                 u->headers_in.status_n = 302;
diff --git a/src/http/modules/ngx_http_ssi_filter_module.c b/src/http/modules/ngx_http_ssi_filter_module.c
index 6737965..47068f7 100644
--- a/src/http/modules/ngx_http_ssi_filter_module.c
+++ b/src/http/modules/ngx_http_ssi_filter_module.c
@@ -329,7 +329,7 @@ static ngx_http_variable_t  ngx_http_ssi_vars[] = {
 static ngx_int_t
 ngx_http_ssi_header_filter(ngx_http_request_t *r)
 {
-    ngx_http_ssi_ctx_t       *ctx;
+    ngx_http_ssi_ctx_t       *ctx, *mctx;
     ngx_http_ssi_loc_conf_t  *slcf;
 
     slcf = ngx_http_get_module_loc_conf(r, ngx_http_ssi_filter_module);
@@ -341,6 +341,8 @@ ngx_http_ssi_header_filter(ngx_http_request_t *r)
         return ngx_http_next_header_filter(r);
     }
 
+    mctx = ngx_http_get_module_ctx(r->main, ngx_http_ssi_filter_module);
+
     ctx = ngx_pcalloc(r->pool, sizeof(ngx_http_ssi_ctx_t));
     if (ctx == NULL) {
         return NGX_ERROR;
@@ -367,6 +369,26 @@ ngx_http_ssi_header_filter(ngx_http_request_t *r)
     r->filter_need_in_memory = 1;
 
     if (r == r->main) {
+
+        if (mctx) {
+
+            /*
+             * if there was a shared context previously used as main,
+             * copy variables and blocks
+             */
+
+            ctx->variables = mctx->variables;
+            ctx->blocks = mctx->blocks;
+
+#if (NGX_PCRE)
+            ctx->ncaptures = mctx->ncaptures;
+            ctx->captures = mctx->captures;
+            ctx->captures_data = mctx->captures_data;
+#endif
+
+            mctx->shared = 0;
+        }
+
         ngx_http_clear_content_length(r);
         ngx_http_clear_accept_ranges(r);
 
@@ -379,6 +401,10 @@ ngx_http_ssi_header_filter(ngx_http_request_t *r)
         } else {
             ngx_http_weak_etag(r);
         }
+
+    } else if (mctx == NULL) {
+        ngx_http_set_ctx(r->main, ctx, ngx_http_ssi_filter_module);
+        ctx->shared = 1;
     }
 
     return ngx_http_next_header_filter(r);
@@ -405,6 +431,7 @@ ngx_http_ssi_body_filter(ngx_http_request_t *r, ngx_chain_t *in)
     ctx = ngx_http_get_module_ctx(r, ngx_http_ssi_filter_module);
 
     if (ctx == NULL
+        || (ctx->shared && r == r->main)
         || (in == NULL
             && ctx->buf == NULL
             && ctx->in == NULL
@@ -455,9 +482,13 @@ ngx_http_ssi_body_filter(ngx_http_request_t *r, ngx_chain_t *in)
     while (ctx->in || ctx->buf) {
 
         if (ctx->buf == NULL) {
-            ctx->buf = ctx->in->buf;
-            ctx->in = ctx->in->next;
+
+            cl = ctx->in;
+            ctx->buf = cl->buf;
+            ctx->in = cl->next;
             ctx->pos = ctx->buf->pos;
+
+            ngx_free_chain(r->pool, cl);
         }
 
         if (ctx->state == ssi_start_state) {
@@ -1974,7 +2005,7 @@ ngx_http_ssi_regex_match(ngx_http_request_t *r, ngx_str_t *pattern,
 #else
 
     ngx_log_error(NGX_LOG_ALERT, r->connection->log, 0,
-                  "the using of the regex \"%V\" in SSI requires PCRE library",
+                  "using regex \"%V\" in SSI requires PCRE library",
                   pattern);
     return NGX_HTTP_SSI_ERROR;
 
diff --git a/src/http/modules/ngx_http_ssi_filter_module.h b/src/http/modules/ngx_http_ssi_filter_module.h
index 0bd01a0..419bf97 100644
--- a/src/http/modules/ngx_http_ssi_filter_module.h
+++ b/src/http/modules/ngx_http_ssi_filter_module.h
@@ -71,6 +71,7 @@ typedef struct {
     u_char                   *captures_data;
 #endif
 
+    unsigned                  shared:1;
     unsigned                  conditional:2;
     unsigned                  encoding:2;
     unsigned                  block:1;
diff --git a/src/http/modules/ngx_http_ssl_module.c b/src/http/modules/ngx_http_ssl_module.c
index d74d460..1c92d9f 100644
--- a/src/http/modules/ngx_http_ssl_module.c
+++ b/src/http/modules/ngx_http_ssl_module.c
@@ -9,6 +9,10 @@
 #include <ngx_core.h>
 #include <ngx_http.h>
 
+#if (NGX_QUIC_OPENSSL_COMPAT)
+#include <ngx_event_quic_openssl_compat.h>
+#endif
+
 
 typedef ngx_int_t (*ngx_ssl_variable_handler_pt)(ngx_connection_t *c,
     ngx_pool_t *pool, ngx_str_t *s);
@@ -39,8 +43,6 @@ static char *ngx_http_ssl_merge_srv_conf(ngx_conf_t *cf,
 static ngx_int_t ngx_http_ssl_compile_certificates(ngx_conf_t *cf,
     ngx_http_ssl_srv_conf_t *conf);
 
-static char *ngx_http_ssl_enable(ngx_conf_t *cf, ngx_command_t *cmd,
-    void *conf);
 static char *ngx_http_ssl_password_file(ngx_conf_t *cf, ngx_command_t *cmd,
     void *conf);
 static char *ngx_http_ssl_session_cache(ngx_conf_t *cf, ngx_command_t *cmd,
@@ -52,6 +54,10 @@ static char *ngx_http_ssl_conf_command_check(ngx_conf_t *cf, void *post,
     void *data);
 
 static ngx_int_t ngx_http_ssl_init(ngx_conf_t *cf);
+#if (NGX_QUIC_OPENSSL_COMPAT)
+static ngx_int_t ngx_http_ssl_quic_compat_init(ngx_conf_t *cf,
+    ngx_http_conf_addr_t *addr);
+#endif
 
 
 static ngx_conf_bitmask_t  ngx_http_ssl_protocols[] = {
@@ -82,24 +88,12 @@ static ngx_conf_enum_t  ngx_http_ssl_ocsp[] = {
 };
 
 
-static ngx_conf_deprecated_t  ngx_http_ssl_deprecated = {
-    ngx_conf_deprecated, "ssl", "listen ... ssl"
-};
-
-
 static ngx_conf_post_t  ngx_http_ssl_conf_command_post =
     { ngx_http_ssl_conf_command_check };
 
 
 static ngx_command_t  ngx_http_ssl_commands[] = {
 
-    { ngx_string("ssl"),
-      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_FLAG,
-      ngx_http_ssl_enable,
-      NGX_HTTP_SRV_CONF_OFFSET,
-      offsetof(ngx_http_ssl_srv_conf_t, enable),
-      &ngx_http_ssl_deprecated },
-
     { ngx_string("ssl_certificate"),
       NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1,
       ngx_conf_set_str_array_slot,
@@ -419,16 +413,22 @@ ngx_http_ssl_alpn_select(ngx_ssl_conn_t *ssl_conn, const unsigned char **out,
     unsigned char *outlen, const unsigned char *in, unsigned int inlen,
     void *arg)
 {
-    unsigned int            srvlen;
-    unsigned char          *srv;
+    unsigned int             srvlen;
+    unsigned char           *srv;
 #if (NGX_DEBUG)
-    unsigned int            i;
+    unsigned int             i;
+#endif
+#if (NGX_HTTP_V2 || NGX_HTTP_V3)
+    ngx_http_connection_t   *hc;
 #endif
 #if (NGX_HTTP_V2)
-    ngx_http_connection_t  *hc;
+    ngx_http_v2_srv_conf_t  *h2scf;
 #endif
-#if (NGX_HTTP_V2 || NGX_DEBUG)
-    ngx_connection_t       *c;
+#if (NGX_HTTP_V3)
+    ngx_http_v3_srv_conf_t  *h3scf;
+#endif
+#if (NGX_HTTP_V2 || NGX_HTTP_V3 || NGX_DEBUG)
+    ngx_connection_t        *c;
 
     c = ngx_ssl_get_connection(ssl_conn);
 #endif
@@ -441,17 +441,49 @@ ngx_http_ssl_alpn_select(ngx_ssl_conn_t *ssl_conn, const unsigned char **out,
     }
 #endif
 
-#if (NGX_HTTP_V2)
+#if (NGX_HTTP_V2 || NGX_HTTP_V3)
     hc = c->data;
+#endif
+
+#if (NGX_HTTP_V3)
+    if (hc->addr_conf->quic) {
+
+        h3scf = ngx_http_get_module_srv_conf(hc->conf_ctx, ngx_http_v3_module);
+
+        if (h3scf->enable && h3scf->enable_hq) {
+            srv = (unsigned char *) NGX_HTTP_V3_ALPN_PROTO
+                                    NGX_HTTP_V3_HQ_ALPN_PROTO;
+            srvlen = sizeof(NGX_HTTP_V3_ALPN_PROTO NGX_HTTP_V3_HQ_ALPN_PROTO)
+                     - 1;
+
+        } else if (h3scf->enable_hq) {
+            srv = (unsigned char *) NGX_HTTP_V3_HQ_ALPN_PROTO;
+            srvlen = sizeof(NGX_HTTP_V3_HQ_ALPN_PROTO) - 1;
+
+        } else if (h3scf->enable) {
+            srv = (unsigned char *) NGX_HTTP_V3_ALPN_PROTO;
+            srvlen = sizeof(NGX_HTTP_V3_ALPN_PROTO) - 1;
+
+        } else {
+            return SSL_TLSEXT_ERR_ALERT_FATAL;
+        }
 
-    if (hc->addr_conf->http2) {
-        srv = (unsigned char *) NGX_HTTP_V2_ALPN_PROTO NGX_HTTP_ALPN_PROTOS;
-        srvlen = sizeof(NGX_HTTP_V2_ALPN_PROTO NGX_HTTP_ALPN_PROTOS) - 1;
     } else
 #endif
     {
-        srv = (unsigned char *) NGX_HTTP_ALPN_PROTOS;
-        srvlen = sizeof(NGX_HTTP_ALPN_PROTOS) - 1;
+#if (NGX_HTTP_V2)
+        h2scf = ngx_http_get_module_srv_conf(hc->conf_ctx, ngx_http_v2_module);
+
+        if (h2scf->enable || hc->addr_conf->http2) {
+            srv = (unsigned char *) NGX_HTTP_V2_ALPN_PROTO NGX_HTTP_ALPN_PROTOS;
+            srvlen = sizeof(NGX_HTTP_V2_ALPN_PROTO NGX_HTTP_ALPN_PROTOS) - 1;
+
+        } else
+#endif
+        {
+            srv = (unsigned char *) NGX_HTTP_ALPN_PROTOS;
+            srvlen = sizeof(NGX_HTTP_ALPN_PROTOS) - 1;
+        }
     }
 
     if (SSL_select_next_proto((unsigned char **) out, outlen, srv, srvlen,
@@ -579,7 +611,6 @@ ngx_http_ssl_create_srv_conf(ngx_conf_t *cf)
      *     sscf->stapling_responder = { 0, NULL };
      */
 
-    sscf->enable = NGX_CONF_UNSET;
     sscf->prefer_server_ciphers = NGX_CONF_UNSET;
     sscf->early_data = NGX_CONF_UNSET;
     sscf->reject_handshake = NGX_CONF_UNSET;
@@ -611,17 +642,6 @@ ngx_http_ssl_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child)
 
     ngx_pool_cleanup_t  *cln;
 
-    if (conf->enable == NGX_CONF_UNSET) {
-        if (prev->enable == NGX_CONF_UNSET) {
-            conf->enable = 0;
-
-        } else {
-            conf->enable = prev->enable;
-            conf->file = prev->file;
-            conf->line = prev->line;
-        }
-    }
-
     ngx_conf_merge_value(conf->session_timeout,
                          prev->session_timeout, 300);
 
@@ -632,8 +652,9 @@ ngx_http_ssl_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child)
     ngx_conf_merge_value(conf->reject_handshake, prev->reject_handshake, 0);
 
     ngx_conf_merge_bitmask_value(conf->protocols, prev->protocols,
-                         (NGX_CONF_BITMASK_SET|NGX_SSL_TLSv1
-                          |NGX_SSL_TLSv1_1|NGX_SSL_TLSv1_2));
+                         (NGX_CONF_BITMASK_SET
+                          |NGX_SSL_TLSv1|NGX_SSL_TLSv1_1
+                          |NGX_SSL_TLSv1_2|NGX_SSL_TLSv1_3));
 
     ngx_conf_merge_size_value(conf->buffer_size, prev->buffer_size,
                          NGX_SSL_BUFSIZE);
@@ -675,37 +696,7 @@ ngx_http_ssl_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child)
 
     conf->ssl.log = cf->log;
 
-    if (conf->enable) {
-
-        if (conf->certificates) {
-            if (conf->certificate_keys == NULL) {
-                ngx_log_error(NGX_LOG_EMERG, cf->log, 0,
-                              "no \"ssl_certificate_key\" is defined for "
-                              "the \"ssl\" directive in %s:%ui",
-                              conf->file, conf->line);
-                return NGX_CONF_ERROR;
-            }
-
-            if (conf->certificate_keys->nelts < conf->certificates->nelts) {
-                ngx_log_error(NGX_LOG_EMERG, cf->log, 0,
-                              "no \"ssl_certificate_key\" is defined "
-                              "for certificate \"%V\" and "
-                              "the \"ssl\" directive in %s:%ui",
-                              ((ngx_str_t *) conf->certificates->elts)
-                              + conf->certificates->nelts - 1,
-                              conf->file, conf->line);
-                return NGX_CONF_ERROR;
-            }
-
-        } else if (!conf->reject_handshake) {
-            ngx_log_error(NGX_LOG_EMERG, cf->log, 0,
-                          "no \"ssl_certificate\" is defined for "
-                          "the \"ssl\" directive in %s:%ui",
-                          conf->file, conf->line);
-            return NGX_CONF_ERROR;
-        }
-
-    } else if (conf->certificates) {
+    if (conf->certificates) {
 
         if (conf->certificate_keys == NULL
             || conf->certificate_keys->nelts < conf->certificates->nelts)
@@ -991,26 +982,6 @@ found:
 }
 
 
-static char *
-ngx_http_ssl_enable(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
-{
-    ngx_http_ssl_srv_conf_t *sscf = conf;
-
-    char  *rv;
-
-    rv = ngx_conf_set_flag_slot(cf, cmd, conf);
-
-    if (rv != NGX_CONF_OK) {
-        return rv;
-    }
-
-    sscf->file = cf->conf_file->file.name.data;
-    sscf->line = cf->conf_file->line;
-
-    return NGX_CONF_OK;
-}
-
-
 static char *
 ngx_http_ssl_password_file(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
 {
@@ -1093,7 +1064,7 @@ ngx_http_ssl_session_cache(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
                 len++;
             }
 
-            if (len == 0) {
+            if (len == 0 || j == value[i].len) {
                 goto invalid;
             }
 
@@ -1183,7 +1154,7 @@ ngx_http_ssl_ocsp_cache(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
         len++;
     }
 
-    if (len == 0) {
+    if (len == 0 || j == value[1].len) {
         goto invalid;
     }
 
@@ -1240,6 +1211,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;
@@ -1289,22 +1261,44 @@ 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.quic) {
+                name = "quic";
+
+#if (NGX_QUIC_OPENSSL_COMPAT)
+                if (ngx_http_ssl_quic_compat_init(cf, &addr[a]) != NGX_OK) {
+                    return NGX_ERROR;
+                }
+#endif
+
+            } else {
+                name = "ssl";
+            }
+
             cscf = addr[a].default_server;
             sscf = cscf->ctx->srv_conf[ngx_http_ssl_module.ctx_index];
 
             if (sscf->certificates) {
+
+                if (addr[a].opt.quic && !(sscf->protocols & NGX_SSL_TLSv1_3)) {
+                    ngx_log_error(NGX_LOG_EMERG, cf->log, 0,
+                                  "\"ssl_protocols\" must enable TLSv1.3 for "
+                                  "the \"listen ... %s\" directive in %s:%ui",
+                                  name, cscf->file_name, cscf->line);
+                    return NGX_ERROR;
+                }
+
                 continue;
             }
 
             if (!sscf->reject_handshake) {
                 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;
             }
 
@@ -1325,8 +1319,8 @@ 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;
             }
         }
@@ -1334,3 +1328,31 @@ ngx_http_ssl_init(ngx_conf_t *cf)
 
     return NGX_OK;
 }
+
+
+#if (NGX_QUIC_OPENSSL_COMPAT)
+
+static ngx_int_t
+ngx_http_ssl_quic_compat_init(ngx_conf_t *cf, ngx_http_conf_addr_t *addr)
+{
+    ngx_uint_t                  s;
+    ngx_http_ssl_srv_conf_t    *sscf;
+    ngx_http_core_srv_conf_t  **cscfp, *cscf;
+
+    cscfp = addr->servers.elts;
+    for (s = 0; s < addr->servers.nelts; s++) {
+
+        cscf = cscfp[s];
+        sscf = cscf->ctx->srv_conf[ngx_http_ssl_module.ctx_index];
+
+        if (sscf->certificates || sscf->reject_handshake) {
+            if (ngx_quic_compat_init(cf, sscf->ssl.ctx) != NGX_OK) {
+                return NGX_ERROR;
+            }
+        }
+    }
+
+    return NGX_OK;
+}
+
+#endif
diff --git a/src/http/modules/ngx_http_ssl_module.h b/src/http/modules/ngx_http_ssl_module.h
index 7ab0f7e..c69c8ff 100644
--- a/src/http/modules/ngx_http_ssl_module.h
+++ b/src/http/modules/ngx_http_ssl_module.h
@@ -15,8 +15,6 @@
 
 
 typedef struct {
-    ngx_flag_t                      enable;
-
     ngx_ssl_t                       ssl;
 
     ngx_flag_t                      prefer_server_ciphers;
@@ -64,9 +62,6 @@ typedef struct {
     ngx_flag_t                      stapling_verify;
     ngx_str_t                       stapling_file;
     ngx_str_t                       stapling_responder;
-
-    u_char                         *file;
-    ngx_uint_t                      line;
 } ngx_http_ssl_srv_conf_t;
 
 
diff --git a/src/http/modules/ngx_http_static_module.c b/src/http/modules/ngx_http_static_module.c
index cf29d5a..8b0bb14 100644
--- a/src/http/modules/ngx_http_static_module.c
+++ b/src/http/modules/ngx_http_static_module.c
@@ -195,6 +195,7 @@ ngx_http_static_handler(ngx_http_request_t *r)
         }
 
         r->headers_out.location->hash = 1;
+        r->headers_out.location->next = NULL;
         ngx_str_set(&r->headers_out.location->key, "Location");
         r->headers_out.location->value.len = len;
         r->headers_out.location->value.data = location;
@@ -237,10 +238,6 @@ ngx_http_static_handler(ngx_http_request_t *r)
         return NGX_HTTP_INTERNAL_SERVER_ERROR;
     }
 
-    if (r != r->main && of.size == 0) {
-        return ngx_http_send_header(r);
-    }
-
     r->allow_ranges = 1;
 
     /* we need to allocate all before the header would be sent */
@@ -264,9 +261,10 @@ ngx_http_static_handler(ngx_http_request_t *r)
     b->file_pos = 0;
     b->file_last = of.size;
 
-    b->in_file = b->file_last ? 1: 0;
-    b->last_buf = (r == r->main) ? 1: 0;
+    b->in_file = b->file_last ? 1 : 0;
+    b->last_buf = (r == r->main) ? 1 : 0;
     b->last_in_chain = 1;
+    b->sync = (b->last_buf || b->in_file) ? 0 : 1;
 
     b->file->fd = of.fd;
     b->file->name = path;
diff --git a/src/http/modules/ngx_http_sub_filter_module.c b/src/http/modules/ngx_http_sub_filter_module.c
index 6d3de59..456bb27 100644
--- a/src/http/modules/ngx_http_sub_filter_module.c
+++ b/src/http/modules/ngx_http_sub_filter_module.c
@@ -335,9 +335,13 @@ ngx_http_sub_body_filter(ngx_http_request_t *r, ngx_chain_t *in)
     while (ctx->in || ctx->buf) {
 
         if (ctx->buf == NULL) {
-            ctx->buf = ctx->in->buf;
-            ctx->in = ctx->in->next;
+
+            cl = ctx->in;
+            ctx->buf = cl->buf;
+            ctx->in = cl->next;
             ctx->pos = ctx->buf->pos;
+
+            ngx_free_chain(r->pool, cl);
         }
 
         if (ctx->buf->flush || ctx->buf->recycled) {
diff --git a/src/http/modules/ngx_http_userid_filter_module.c b/src/http/modules/ngx_http_userid_filter_module.c
index 1e33c5c..e528444 100644
--- a/src/http/modules/ngx_http_userid_filter_module.c
+++ b/src/http/modules/ngx_http_userid_filter_module.c
@@ -319,10 +319,9 @@ ngx_http_userid_set_variable(ngx_http_request_t *r,
 static ngx_http_userid_ctx_t *
 ngx_http_userid_get_uid(ngx_http_request_t *r, ngx_http_userid_conf_t *conf)
 {
-    ngx_int_t                n;
-    ngx_str_t                src, dst;
-    ngx_table_elt_t        **cookies;
-    ngx_http_userid_ctx_t   *ctx;
+    ngx_str_t               src, dst;
+    ngx_table_elt_t        *cookie;
+    ngx_http_userid_ctx_t  *ctx;
 
     ctx = ngx_http_get_module_ctx(r, ngx_http_userid_filter_module);
 
@@ -339,9 +338,9 @@ ngx_http_userid_get_uid(ngx_http_request_t *r, ngx_http_userid_conf_t *conf)
         ngx_http_set_ctx(r, ctx, ngx_http_userid_filter_module);
     }
 
-    n = ngx_http_parse_multi_header_lines(&r->headers_in.cookies, &conf->name,
-                                          &ctx->cookie);
-    if (n == NGX_DECLINED) {
+    cookie = ngx_http_parse_multi_header_lines(r, r->headers_in.cookie,
+                                               &conf->name, &ctx->cookie);
+    if (cookie == NULL) {
         return ctx;
     }
 
@@ -349,10 +348,9 @@ ngx_http_userid_get_uid(ngx_http_request_t *r, ngx_http_userid_conf_t *conf)
                    "uid cookie: \"%V\"", &ctx->cookie);
 
     if (ctx->cookie.len < 22) {
-        cookies = r->headers_in.cookies.elts;
         ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
                       "client sent too short userid cookie \"%V\"",
-                      &cookies[n]->value);
+                      &cookie->value);
         return ctx;
     }
 
@@ -370,10 +368,9 @@ ngx_http_userid_get_uid(ngx_http_request_t *r, ngx_http_userid_conf_t *conf)
     dst.data = (u_char *) ctx->uid_got;
 
     if (ngx_decode_base64(&dst, &src) == NGX_ERROR) {
-        cookies = r->headers_in.cookies.elts;
         ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
                       "client sent invalid userid cookie \"%V\"",
-                      &cookies[n]->value);
+                      &cookie->value);
         return ctx;
     }
 
diff --git a/src/http/modules/ngx_http_uwsgi_module.c b/src/http/modules/ngx_http_uwsgi_module.c
index d46741a..c1731ff 100644
--- a/src/http/modules/ngx_http_uwsgi_module.c
+++ b/src/http/modules/ngx_http_uwsgi_module.c
@@ -96,6 +96,8 @@ static char *ngx_http_uwsgi_ssl_password_file(ngx_conf_t *cf,
     ngx_command_t *cmd, void *conf);
 static char *ngx_http_uwsgi_ssl_conf_command_check(ngx_conf_t *cf, void *post,
     void *data);
+static ngx_int_t ngx_http_uwsgi_merge_ssl(ngx_conf_t *cf,
+    ngx_http_uwsgi_loc_conf_t *conf, ngx_http_uwsgi_loc_conf_t *prev);
 static ngx_int_t ngx_http_uwsgi_set_ssl(ngx_conf_t *cf,
     ngx_http_uwsgi_loc_conf_t *uwcf);
 #endif
@@ -668,7 +670,7 @@ ngx_http_uwsgi_handler(ngx_http_request_t *r)
     if (uwcf->uwsgi_lengths == NULL) {
 
 #if (NGX_HTTP_SSL)
-        u->ssl = (uwcf->upstream.ssl != NULL);
+        u->ssl = uwcf->ssl;
 
         if (u->ssl) {
             ngx_str_set(&u->schema, "suwsgi://");
@@ -845,13 +847,13 @@ ngx_http_uwsgi_create_key(ngx_http_request_t *r)
 static ngx_int_t
 ngx_http_uwsgi_create_request(ngx_http_request_t *r)
 {
-    u_char                        ch, *lowcase_key;
+    u_char                        ch, sep, *lowcase_key;
     size_t                        key_len, val_len, len, allocated;
     ngx_uint_t                    i, n, hash, skip_empty, header_params;
     ngx_buf_t                    *b;
     ngx_chain_t                  *cl, *body;
     ngx_list_part_t              *part;
-    ngx_table_elt_t              *header, **ignored;
+    ngx_table_elt_t              *header, *hn, **ignored;
     ngx_http_uwsgi_params_t      *params;
     ngx_http_script_code_pt       code;
     ngx_http_script_engine_t      e, le;
@@ -905,7 +907,11 @@ ngx_http_uwsgi_create_request(ngx_http_request_t *r)
         allocated = 0;
         lowcase_key = NULL;
 
-        if (params->number) {
+        if (ngx_http_link_multi_headers(r) != NGX_OK) {
+            return NGX_ERROR;
+        }
+
+        if (params->number || r->headers_in.multi) {
             n = 0;
             part = &r->headers_in.headers.part;
 
@@ -935,6 +941,12 @@ ngx_http_uwsgi_create_request(ngx_http_request_t *r)
                 i = 0;
             }
 
+            for (n = 0; n < header_params; n++) {
+                if (&header[i] == ignored[n]) {
+                    goto next_length;
+                }
+            }
+
             if (params->number) {
                 if (allocated < header[i].key.len) {
                     allocated = header[i].key.len + 16;
@@ -968,6 +980,15 @@ ngx_http_uwsgi_create_request(ngx_http_request_t *r)
 
             len += 2 + sizeof("HTTP_") - 1 + header[i].key.len
                  + 2 + header[i].value.len;
+
+            for (hn = header[i].next; hn; hn = hn->next) {
+                len += hn->value.len + 2;
+                ignored[header_params++] = hn;
+            }
+
+        next_length:
+
+            continue;
         }
     }
 
@@ -1086,7 +1107,7 @@ ngx_http_uwsgi_create_request(ngx_http_request_t *r)
 
             for (n = 0; n < header_params; n++) {
                 if (&header[i] == ignored[n]) {
-                    goto next;
+                    goto next_value;
                 }
             }
 
@@ -1109,15 +1130,41 @@ ngx_http_uwsgi_create_request(ngx_http_request_t *r)
             }
 
             val_len = header[i].value.len;
+
+            for (hn = header[i].next; hn; hn = hn->next) {
+                val_len += hn->value.len + 2;
+            }
+
             *b->last++ = (u_char) (val_len & 0xff);
             *b->last++ = (u_char) ((val_len >> 8) & 0xff);
-            b->last = ngx_copy(b->last, header[i].value.data, val_len);
+            b->last = ngx_copy(b->last, header[i].value.data,
+                               header[i].value.len);
+
+            if (header[i].next) {
+
+                if (header[i].key.len == sizeof("Cookie") - 1
+                    && ngx_strncasecmp(header[i].key.data, (u_char *) "Cookie",
+                                       sizeof("Cookie") - 1)
+                       == 0)
+                {
+                    sep = ';';
+
+                } else {
+                    sep = ',';
+                }
+
+                for (hn = header[i].next; hn; hn = hn->next) {
+                    *b->last++ = sep;
+                    *b->last++ = ' ';
+                    b->last = ngx_copy(b->last, hn->value.data, hn->value.len);
+                }
+            }
 
             ngx_log_debug4(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                            "uwsgi param: \"%*s: %*s\"",
                            key_len, b->last - (key_len + 2 + val_len),
                            val_len, b->last - val_len);
-        next:
+        next_value:
 
             continue;
         }
@@ -1295,8 +1342,12 @@ ngx_http_uwsgi_process_header(ngx_http_request_t *r)
             hh = ngx_hash_find(&umcf->headers_in_hash, h->hash,
                                h->lowcase_key, h->key.len);
 
-            if (hh && hh->handler(r, h, hh->offset) != NGX_OK) {
-                return NGX_ERROR;
+            if (hh) {
+                rc = hh->handler(r, h, hh->offset);
+
+                if (rc != NGX_OK) {
+                    return rc;
+                }
             }
 
             ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
@@ -1330,7 +1381,10 @@ ngx_http_uwsgi_process_header(ngx_http_request_t *r)
                 }
 
                 u->headers_in.status_n = status;
-                u->headers_in.status_line = *status_line;
+
+                if (status_line->len > 3) {
+                    u->headers_in.status_line = *status_line;
+                }
 
             } else if (u->headers_in.location) {
                 u->headers_in.status_n = 302;
@@ -1816,12 +1870,17 @@ ngx_http_uwsgi_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child)
 
 #if (NGX_HTTP_SSL)
 
+    if (ngx_http_uwsgi_merge_ssl(cf, conf, prev) != NGX_OK) {
+        return NGX_CONF_ERROR;
+    }
+
     ngx_conf_merge_value(conf->upstream.ssl_session_reuse,
                               prev->upstream.ssl_session_reuse, 1);
 
     ngx_conf_merge_bitmask_value(conf->ssl_protocols, prev->ssl_protocols,
-                                 (NGX_CONF_BITMASK_SET|NGX_SSL_TLSv1
-                                  |NGX_SSL_TLSv1_1|NGX_SSL_TLSv1_2));
+                                 (NGX_CONF_BITMASK_SET
+                                  |NGX_SSL_TLSv1|NGX_SSL_TLSv1_1
+                                  |NGX_SSL_TLSv1_2|NGX_SSL_TLSv1_3));
 
     ngx_conf_merge_str_value(conf->ssl_ciphers, prev->ssl_ciphers,
                              "DEFAULT");
@@ -1878,7 +1937,7 @@ ngx_http_uwsgi_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child)
         conf->uwsgi_values = prev->uwsgi_values;
 
 #if (NGX_HTTP_SSL)
-        conf->upstream.ssl = prev->upstream.ssl;
+        conf->ssl = prev->ssl;
 #endif
     }
 
@@ -2406,16 +2465,62 @@ ngx_http_uwsgi_ssl_conf_command_check(ngx_conf_t *cf, void *post, void *data)
 
 
 static ngx_int_t
-ngx_http_uwsgi_set_ssl(ngx_conf_t *cf, ngx_http_uwsgi_loc_conf_t *uwcf)
+ngx_http_uwsgi_merge_ssl(ngx_conf_t *cf, ngx_http_uwsgi_loc_conf_t *conf,
+    ngx_http_uwsgi_loc_conf_t *prev)
 {
-    ngx_pool_cleanup_t  *cln;
+    ngx_uint_t  preserve;
+
+    if (conf->ssl_protocols == 0
+        && conf->ssl_ciphers.data == NULL
+        && conf->upstream.ssl_certificate == NGX_CONF_UNSET_PTR
+        && conf->upstream.ssl_certificate_key == NGX_CONF_UNSET_PTR
+        && conf->upstream.ssl_passwords == NGX_CONF_UNSET_PTR
+        && conf->upstream.ssl_verify == NGX_CONF_UNSET
+        && conf->ssl_verify_depth == NGX_CONF_UNSET_UINT
+        && conf->ssl_trusted_certificate.data == NULL
+        && conf->ssl_crl.data == NULL
+        && conf->upstream.ssl_session_reuse == NGX_CONF_UNSET
+        && conf->ssl_conf_commands == NGX_CONF_UNSET_PTR)
+    {
+        if (prev->upstream.ssl) {
+            conf->upstream.ssl = prev->upstream.ssl;
+            return NGX_OK;
+        }
 
-    uwcf->upstream.ssl = ngx_pcalloc(cf->pool, sizeof(ngx_ssl_t));
-    if (uwcf->upstream.ssl == NULL) {
+        preserve = 1;
+
+    } else {
+        preserve = 0;
+    }
+
+    conf->upstream.ssl = ngx_pcalloc(cf->pool, sizeof(ngx_ssl_t));
+    if (conf->upstream.ssl == NULL) {
         return NGX_ERROR;
     }
 
-    uwcf->upstream.ssl->log = cf->log;
+    conf->upstream.ssl->log = cf->log;
+
+    /*
+     * special handling to preserve conf->upstream.ssl
+     * in the "http" section to inherit it to all servers
+     */
+
+    if (preserve) {
+        prev->upstream.ssl = conf->upstream.ssl;
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_http_uwsgi_set_ssl(ngx_conf_t *cf, ngx_http_uwsgi_loc_conf_t *uwcf)
+{
+    ngx_pool_cleanup_t  *cln;
+
+    if (uwcf->upstream.ssl->ctx) {
+        return NGX_OK;
+    }
 
     if (ngx_ssl_create(uwcf->upstream.ssl, uwcf->ssl_protocols, NULL)
         != NGX_OK)
@@ -2438,8 +2543,9 @@ ngx_http_uwsgi_set_ssl(ngx_conf_t *cf, ngx_http_uwsgi_loc_conf_t *uwcf)
         return NGX_ERROR;
     }
 
-    if (uwcf->upstream.ssl_certificate) {
-
+    if (uwcf->upstream.ssl_certificate
+        && uwcf->upstream.ssl_certificate->value.len)
+    {
         if (uwcf->upstream.ssl_certificate_key == NULL) {
             ngx_log_error(NGX_LOG_EMERG, cf->log, 0,
                           "no \"uwsgi_ssl_certificate_key\" is defined "
diff --git a/src/http/modules/perl/nginx.xs b/src/http/modules/perl/nginx.xs
index caf7c08..fd59e29 100644
--- a/src/http/modules/perl/nginx.xs
+++ b/src/http/modules/perl/nginx.xs
@@ -269,10 +269,9 @@ header_in(r, key)
     u_char                     *p, *lowcase_key, *value, sep;
     STRLEN                      len;
     ssize_t                     size;
-    ngx_uint_t                  i, n, hash;
-    ngx_array_t                *a;
+    ngx_uint_t                  i, hash;
     ngx_list_part_t            *part;
-    ngx_table_elt_t            *h, **ph;
+    ngx_table_elt_t            *h, *header, **ph;
     ngx_http_header_t          *hh;
     ngx_http_core_main_conf_t  *cmcf;
 
@@ -302,78 +301,23 @@ header_in(r, key)
 
     if (hh) {
 
-        if (hh->offset == offsetof(ngx_http_headers_in_t, cookies)) {
+        if (hh->offset == offsetof(ngx_http_headers_in_t, cookie)) {
             sep = ';';
-            goto multi;
-        }
-#if (NGX_HTTP_X_FORWARDED_FOR)
-        if (hh->offset == offsetof(ngx_http_headers_in_t, x_forwarded_for)) {
+
+        } else {
             sep = ',';
-            goto multi;
         }
-#endif
 
         ph = (ngx_table_elt_t **) ((char *) &r->headers_in + hh->offset);
 
-        if (*ph) {
-            ngx_http_perl_set_targ((*ph)->value.data, (*ph)->value.len);
-
-            goto done;
-        }
-
-        XSRETURN_UNDEF;
-
-    multi:
-
-        /* Cookie, X-Forwarded-For */
-
-        a = (ngx_array_t *) ((char *) &r->headers_in + hh->offset);
-
-        n = a->nelts;
-
-        if (n == 0) {
-            XSRETURN_UNDEF;
-        }
-
-        ph = a->elts;
-
-        if (n == 1) {
-            ngx_http_perl_set_targ((*ph)->value.data, (*ph)->value.len);
-
-            goto done;
-        }
-
-        size = - (ssize_t) (sizeof("; ") - 1);
-
-        for (i = 0; i < n; i++) {
-            size += ph[i]->value.len + sizeof("; ") - 1;
-        }
-
-        value = ngx_pnalloc(r->pool, size);
-        if (value == NULL) {
-            ctx->error = 1;
-            croak("ngx_pnalloc() failed");
-        }
-
-        p = value;
-
-        for (i = 0; /* void */ ; i++) {
-            p = ngx_copy(p, ph[i]->value.data, ph[i]->value.len);
-
-            if (i == n - 1) {
-                break;
-            }
-
-            *p++ = sep; *p++ = ' ';
-        }
-
-        ngx_http_perl_set_targ(value, size);
-
-        goto done;
+        goto found;
     }
 
     /* iterate over all headers */
 
+    sep = ',';
+    ph = &header;
+
     part = &r->headers_in.headers.part;
     h = part->elts;
 
@@ -395,12 +339,49 @@ header_in(r, key)
             continue;
         }
 
-        ngx_http_perl_set_targ(h[i].value.data, h[i].value.len);
+        *ph = &h[i];
+        ph = &h[i].next;
+    }
+
+    *ph = NULL;
+    ph = &header;
+
+    found:
+
+    if (*ph == NULL) {
+        XSRETURN_UNDEF;
+    }
 
+    if ((*ph)->next == NULL) {
+        ngx_http_perl_set_targ((*ph)->value.data, (*ph)->value.len);
         goto done;
     }
 
-    XSRETURN_UNDEF;
+    size = - (ssize_t) (sizeof("; ") - 1);
+
+    for (h = *ph; h; h = h->next) {
+        size += h->value.len + sizeof("; ") - 1;
+    }
+
+    value = ngx_pnalloc(r->pool, size);
+    if (value == NULL) {
+        ctx->error = 1;
+        croak("ngx_pnalloc() failed");
+    }
+
+    p = value;
+
+    for (h = *ph; h; h = h->next) {
+        p = ngx_copy(p, h->value.data, h->value.len);
+
+        if (h->next == NULL) {
+            break;
+        }
+
+        *p++ = sep; *p++ = ' ';
+    }
+
+    ngx_http_perl_set_targ(value, size);
 
     done:
 
@@ -591,6 +572,7 @@ header_out(r, key, value)
     }
 
     header->hash = 1;
+    header->next = NULL;
 
     if (ngx_http_perl_sv2str(aTHX_ r, &header->key, key) != NGX_OK) {
         header->hash = 0;
diff --git a/src/http/ngx_http.c b/src/http/ngx_http.c
index 73c08d5..d835f89 100644
--- a/src/http/ngx_http.c
+++ b/src/http/ngx_http.c
@@ -1130,7 +1130,7 @@ ngx_http_create_locations_tree(ngx_conf_t *cf, ngx_queue_t *locations,
     node->auto_redirect = (u_char) ((lq->exact && lq->exact->auto_redirect)
                            || (lq->inclusive && lq->inclusive->auto_redirect));
 
-    node->len = (u_char) len;
+    node->len = (u_short) len;
     ngx_memcpy(node->name, &lq->name->data[prefix], len);
 
     ngx_queue_split(locations, q, &tail);
@@ -1200,7 +1200,10 @@ ngx_http_add_listen(ngx_conf_t *cf, ngx_http_core_srv_conf_t *cscf,
     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;
         }
 
@@ -1217,6 +1220,7 @@ ngx_http_add_listen(ngx_conf_t *cf, ngx_http_core_srv_conf_t *cscf,
     }
 
     port->family = sa->sa_family;
+    port->type = lsopt->type;
     port->port = p;
     port->addrs.elts = NULL;
 
@@ -1228,7 +1232,8 @@ static ngx_int_t
 ngx_http_add_addresses(ngx_conf_t *cf, ngx_http_core_srv_conf_t *cscf,
     ngx_http_conf_port_t *port, ngx_http_listen_opt_t *lsopt)
 {
-    ngx_uint_t             i, default_server, proxy_protocol;
+    ngx_uint_t             i, default_server, proxy_protocol,
+                           protocols, protocols_prev;
     ngx_http_conf_addr_t  *addr;
 #if (NGX_HTTP_SSL)
     ngx_uint_t             ssl;
@@ -1236,6 +1241,9 @@ ngx_http_add_addresses(ngx_conf_t *cf, ngx_http_core_srv_conf_t *cscf,
 #if (NGX_HTTP_V2)
     ngx_uint_t             http2;
 #endif
+#if (NGX_HTTP_V3)
+    ngx_uint_t             quic;
+#endif
 
     /*
      * we cannot compare whole sockaddr struct's as kernel
@@ -1264,12 +1272,21 @@ ngx_http_add_addresses(ngx_conf_t *cf, ngx_http_core_srv_conf_t *cscf,
         default_server = addr[i].opt.default_server;
 
         proxy_protocol = lsopt->proxy_protocol || addr[i].opt.proxy_protocol;
+        protocols = lsopt->proxy_protocol;
+        protocols_prev = addr[i].opt.proxy_protocol;
 
 #if (NGX_HTTP_SSL)
         ssl = lsopt->ssl || addr[i].opt.ssl;
+        protocols |= lsopt->ssl << 1;
+        protocols_prev |= addr[i].opt.ssl << 1;
 #endif
 #if (NGX_HTTP_V2)
         http2 = lsopt->http2 || addr[i].opt.http2;
+        protocols |= lsopt->http2 << 2;
+        protocols_prev |= addr[i].opt.http2 << 2;
+#endif
+#if (NGX_HTTP_V3)
+        quic = lsopt->quic || addr[i].opt.quic;
 #endif
 
         if (lsopt->set) {
@@ -1299,6 +1316,57 @@ ngx_http_add_addresses(ngx_conf_t *cf, ngx_http_core_srv_conf_t *cscf,
             addr[i].default_server = cscf;
         }
 
+        /* check for conflicting protocol options */
+
+        if ((protocols | protocols_prev) != protocols_prev) {
+
+            /* options added */
+
+            if ((addr[i].opt.set && !lsopt->set)
+                || addr[i].protocols_changed
+                || (protocols | protocols_prev) != protocols)
+            {
+                ngx_conf_log_error(NGX_LOG_WARN, cf, 0,
+                                   "protocol options redefined for %V",
+                                   &addr[i].opt.addr_text);
+            }
+
+            addr[i].protocols = protocols_prev;
+            addr[i].protocols_set = 1;
+            addr[i].protocols_changed = 1;
+
+        } else if ((protocols_prev | protocols) != protocols) {
+
+            /* options removed */
+
+            if (lsopt->set
+                || (addr[i].protocols_set && protocols != addr[i].protocols))
+            {
+                ngx_conf_log_error(NGX_LOG_WARN, cf, 0,
+                                   "protocol options redefined for %V",
+                                   &addr[i].opt.addr_text);
+            }
+
+            addr[i].protocols = protocols;
+            addr[i].protocols_set = 1;
+            addr[i].protocols_changed = 1;
+
+        } else {
+
+            /* the same options */
+
+            if ((lsopt->set && addr[i].protocols_changed)
+                || (addr[i].protocols_set && protocols != addr[i].protocols))
+            {
+                ngx_conf_log_error(NGX_LOG_WARN, cf, 0,
+                                   "protocol options redefined for %V",
+                                   &addr[i].opt.addr_text);
+            }
+
+            addr[i].protocols = protocols;
+            addr[i].protocols_set = 1;
+        }
+
         addr[i].opt.default_server = default_server;
         addr[i].opt.proxy_protocol = proxy_protocol;
 #if (NGX_HTTP_SSL)
@@ -1307,6 +1375,9 @@ ngx_http_add_addresses(ngx_conf_t *cf, ngx_http_core_srv_conf_t *cscf,
 #if (NGX_HTTP_V2)
         addr[i].opt.http2 = http2;
 #endif
+#if (NGX_HTTP_V3)
+        addr[i].opt.quic = quic;
+#endif
 
         return NGX_OK;
     }
@@ -1355,6 +1426,9 @@ ngx_http_add_address(ngx_conf_t *cf, ngx_http_core_srv_conf_t *cscf,
     }
 
     addr->opt = *lsopt;
+    addr->protocols = 0;
+    addr->protocols_set = 0;
+    addr->protocols_changed = 0;
     addr->hash.buckets = NULL;
     addr->hash.size = 0;
     addr->wc_head = NULL;
@@ -1770,6 +1844,7 @@ ngx_http_add_listening(ngx_conf_t *cf, ngx_http_conf_addr_t *addr)
     }
 #endif
 
+    ls->type = addr->opt.type;
     ls->backlog = addr->opt.backlog;
     ls->rcvbuf = addr->opt.rcvbuf;
     ls->sndbuf = addr->opt.sndbuf;
@@ -1805,6 +1880,12 @@ ngx_http_add_listening(ngx_conf_t *cf, ngx_http_conf_addr_t *addr)
     ls->reuseport = addr->opt.reuseport;
 #endif
 
+    ls->wildcard = addr->opt.wildcard;
+
+#if (NGX_HTTP_V3)
+    ls->quic = addr->opt.quic;
+#endif
+
     return ls;
 }
 
@@ -1836,6 +1917,9 @@ ngx_http_add_addrs(ngx_conf_t *cf, ngx_http_port_t *hport,
 #endif
 #if (NGX_HTTP_V2)
         addrs[i].conf.http2 = addr[i].opt.http2;
+#endif
+#if (NGX_HTTP_V3)
+        addrs[i].conf.quic = addr[i].opt.quic;
 #endif
         addrs[i].conf.proxy_protocol = addr[i].opt.proxy_protocol;
 
@@ -1901,6 +1985,9 @@ ngx_http_add_addrs6(ngx_conf_t *cf, ngx_http_port_t *hport,
 #endif
 #if (NGX_HTTP_V2)
         addrs6[i].conf.http2 = addr[i].opt.http2;
+#endif
+#if (NGX_HTTP_V3)
+        addrs6[i].conf.quic = addr[i].opt.quic;
 #endif
         addrs6[i].conf.proxy_protocol = addr[i].opt.proxy_protocol;
 
diff --git a/src/http/ngx_http.h b/src/http/ngx_http.h
index be8b7cd..e06464e 100644
--- a/src/http/ngx_http.h
+++ b/src/http/ngx_http.h
@@ -20,6 +20,8 @@ typedef struct ngx_http_file_cache_s  ngx_http_file_cache_t;
 typedef struct ngx_http_log_ctx_s     ngx_http_log_ctx_t;
 typedef struct ngx_http_chunked_s     ngx_http_chunked_t;
 typedef struct ngx_http_v2_stream_s   ngx_http_v2_stream_t;
+typedef struct ngx_http_v3_parse_s    ngx_http_v3_parse_t;
+typedef struct ngx_http_v3_session_s  ngx_http_v3_session_t;
 
 typedef ngx_int_t (*ngx_http_header_handler_pt)(ngx_http_request_t *r,
     ngx_table_elt_t *h, ngx_uint_t offset);
@@ -38,6 +40,9 @@ typedef u_char *(*ngx_http_log_handler_pt)(ngx_http_request_t *r,
 #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
@@ -103,10 +108,10 @@ ngx_int_t ngx_http_parse_unsafe_uri(ngx_http_request_t *r, ngx_str_t *uri,
     ngx_str_t *args, ngx_uint_t *flags);
 ngx_int_t ngx_http_parse_header_line(ngx_http_request_t *r, ngx_buf_t *b,
     ngx_uint_t allow_underscores);
-ngx_int_t ngx_http_parse_multi_header_lines(ngx_array_t *headers,
-    ngx_str_t *name, ngx_str_t *value);
-ngx_int_t ngx_http_parse_set_cookie_lines(ngx_array_t *headers,
-    ngx_str_t *name, ngx_str_t *value);
+ngx_table_elt_t *ngx_http_parse_multi_header_lines(ngx_http_request_t *r,
+    ngx_table_elt_t *headers, ngx_str_t *name, ngx_str_t *value);
+ngx_table_elt_t *ngx_http_parse_set_cookie_lines(ngx_http_request_t *r,
+    ngx_table_elt_t *headers, ngx_str_t *name, ngx_str_t *value);
 ngx_int_t ngx_http_arg(ngx_http_request_t *r, u_char *name, size_t len,
     ngx_str_t *value);
 void ngx_http_split_args(ngx_http_request_t *r, ngx_str_t *uri,
@@ -124,6 +129,11 @@ void ngx_http_handler(ngx_http_request_t *r);
 void ngx_http_run_posted_requests(ngx_connection_t *c);
 ngx_int_t ngx_http_post_request(ngx_http_request_t *r,
     ngx_http_posted_request_t *pr);
+ngx_int_t ngx_http_set_virtual_server(ngx_http_request_t *r,
+    ngx_str_t *host);
+ngx_int_t ngx_http_validate_host(ngx_str_t *host, ngx_pool_t *pool,
+    ngx_uint_t alloc);
+void ngx_http_close_request(ngx_http_request_t *r, ngx_int_t rc);
 void ngx_http_finalize_request(ngx_http_request_t *r, ngx_int_t rc);
 void ngx_http_free_request(ngx_http_request_t *r, ngx_int_t rc);
 
@@ -167,7 +177,7 @@ ngx_uint_t  ngx_http_degraded(ngx_http_request_t *);
 #endif
 
 
-#if (NGX_HTTP_V2)
+#if (NGX_HTTP_V2 || NGX_HTTP_V3)
 ngx_int_t ngx_http_huff_decode(u_char *state, u_char *src, size_t len,
     u_char **dst, ngx_uint_t last, ngx_log_t *log);
 size_t ngx_http_huff_encode(u_char *src, size_t len, u_char *dst,
diff --git a/src/http/ngx_http_copy_filter_module.c b/src/http/ngx_http_copy_filter_module.c
index bd3028b..8e5de2b 100644
--- a/src/http/ngx_http_copy_filter_module.c
+++ b/src/http/ngx_http_copy_filter_module.c
@@ -170,6 +170,8 @@ ngx_http_copy_aio_handler(ngx_output_chain_ctx_t *ctx, ngx_file_t *file)
     file->aio->data = r;
     file->aio->handler = ngx_http_copy_aio_event_handler;
 
+    ngx_add_timer(&file->aio->event, 60000);
+
     r->main->blocked++;
     r->aio = 1;
     ctx->aio = 1;
@@ -192,12 +194,32 @@ ngx_http_copy_aio_event_handler(ngx_event_t *ev)
     ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,
                    "http aio: \"%V?%V\"", &r->uri, &r->args);
 
+    if (ev->timedout) {
+        ngx_log_error(NGX_LOG_ALERT, c->log, 0,
+                      "aio operation took too long");
+        ev->timedout = 0;
+        return;
+    }
+
+    if (ev->timer_set) {
+        ngx_del_timer(ev);
+    }
+
     r->main->blocked--;
     r->aio = 0;
 
-    r->write_event_handler(r);
+    if (r->main->terminated) {
+        /*
+         * trigger connection event handler if the request was
+         * terminated
+         */
+
+        c->write->handler(c->write);
 
-    ngx_http_run_posted_requests(c);
+    } else {
+        r->write_event_handler(r);
+        ngx_http_run_posted_requests(c);
+    }
 }
 
 #endif
@@ -264,6 +286,8 @@ ngx_http_copy_thread_handler(ngx_thread_task_t *task, ngx_file_t *file)
         return NGX_ERROR;
     }
 
+    ngx_add_timer(&task->event, 60000);
+
     r->main->blocked++;
     r->aio = 1;
 
@@ -288,6 +312,17 @@ ngx_http_copy_thread_event_handler(ngx_event_t *ev)
     ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,
                    "http thread: \"%V?%V\"", &r->uri, &r->args);
 
+    if (ev->timedout) {
+        ngx_log_error(NGX_LOG_ALERT, c->log, 0,
+                      "thread operation took too long");
+        ev->timedout = 0;
+        return;
+    }
+
+    if (ev->timer_set) {
+        ngx_del_timer(ev);
+    }
+
     r->main->blocked--;
     r->aio = 0;
 
@@ -305,11 +340,11 @@ ngx_http_copy_thread_event_handler(ngx_event_t *ev)
 
 #endif
 
-    if (r->done) {
+    if (r->done || r->main->terminated) {
         /*
          * trigger connection event handler if the subrequest was
-         * already finalized; this can happen if the handler is used
-         * for sendfile() in threads
+         * already finalized (this can happen if the handler is used
+         * for sendfile() in threads), or if the request was terminated
          */
 
         c->write->handler(c->write);
diff --git a/src/http/ngx_http_core_module.c b/src/http/ngx_http_core_module.c
index c7463dc..033a3bf 100644
--- a/src/http/ngx_http_core_module.c
+++ b/src/http/ngx_http_core_module.c
@@ -1007,6 +1007,7 @@ ngx_http_core_find_config_phase(ngx_http_request_t *r,
         }
 
         r->headers_out.location->hash = 1;
+        r->headers_out.location->next = NULL;
         ngx_str_set(&r->headers_out.location->key, "Location");
 
         if (r->args.len == 0) {
@@ -1087,6 +1088,7 @@ ngx_int_t
 ngx_http_core_access_phase(ngx_http_request_t *r, ngx_http_phase_handler_t *ph)
 {
     ngx_int_t                  rc;
+    ngx_table_elt_t           *h;
     ngx_http_core_loc_conf_t  *clcf;
 
     if (r != r->main) {
@@ -1121,8 +1123,8 @@ ngx_http_core_access_phase(ngx_http_request_t *r, ngx_http_phase_handler_t *ph)
         if (rc == NGX_OK) {
             r->access_code = 0;
 
-            if (r->headers_out.www_authenticate) {
-                r->headers_out.www_authenticate->hash = 0;
+            for (h = r->headers_out.www_authenticate; h; h = h->next) {
+                h->hash = 0;
             }
 
             r->phase_handler = ph->next;
@@ -1687,6 +1689,7 @@ ngx_http_set_etag(ngx_http_request_t *r)
     }
 
     etag->hash = 1;
+    etag->next = NULL;
     ngx_str_set(&etag->key, "ETag");
 
     etag->value.data = ngx_pnalloc(r->pool, NGX_OFF_T_LEN + NGX_TIME_T_LEN + 3);
@@ -1781,6 +1784,7 @@ ngx_http_send_response(ngx_http_request_t *r, ngx_uint_t status,
         }
 
         r->headers_out.location->hash = 1;
+        r->headers_out.location->next = NULL;
         ngx_str_set(&r->headers_out.location->key, "Location");
         r->headers_out.location->value = val;
 
@@ -1799,10 +1803,6 @@ ngx_http_send_response(ngx_http_request_t *r, ngx_uint_t status,
         }
     }
 
-    if (r != r->main && val.len == 0) {
-        return ngx_http_send_header(r);
-    }
-
     b = ngx_calloc_buf(r->pool);
     if (b == NULL) {
         return NGX_HTTP_INTERNAL_SERVER_ERROR;
@@ -1813,6 +1813,7 @@ ngx_http_send_response(ngx_http_request_t *r, ngx_uint_t status,
     b->memory = val.len ? 1 : 0;
     b->last_buf = (r == r->main) ? 1 : 0;
     b->last_in_chain = 1;
+    b->sync = (b->last_buf || b->memory) ? 0 : 1;
 
     out.buf = b;
     out.next = NULL;
@@ -2024,8 +2025,7 @@ ngx_http_gzip_ok(ngx_http_request_t *r)
 {
     time_t                     date, expires;
     ngx_uint_t                 p;
-    ngx_array_t               *cc;
-    ngx_table_elt_t           *e, *d, *ae;
+    ngx_table_elt_t           *e, *d, *ae, *cc;
     ngx_http_core_loc_conf_t  *clcf;
 
     r->gzip_tested = 1;
@@ -2118,30 +2118,30 @@ ngx_http_gzip_ok(ngx_http_request_t *r)
         return NGX_DECLINED;
     }
 
-    cc = &r->headers_out.cache_control;
+    cc = r->headers_out.cache_control;
 
-    if (cc->elts) {
+    if (cc) {
 
         if ((p & NGX_HTTP_GZIP_PROXIED_NO_CACHE)
-            && ngx_http_parse_multi_header_lines(cc, &ngx_http_gzip_no_cache,
+            && ngx_http_parse_multi_header_lines(r, cc, &ngx_http_gzip_no_cache,
                                                  NULL)
-               >= 0)
+               != NULL)
         {
             goto ok;
         }
 
         if ((p & NGX_HTTP_GZIP_PROXIED_NO_STORE)
-            && ngx_http_parse_multi_header_lines(cc, &ngx_http_gzip_no_store,
+            && ngx_http_parse_multi_header_lines(r, cc, &ngx_http_gzip_no_store,
                                                  NULL)
-               >= 0)
+               != NULL)
         {
             goto ok;
         }
 
         if ((p & NGX_HTTP_GZIP_PROXIED_PRIVATE)
-            && ngx_http_parse_multi_header_lines(cc, &ngx_http_gzip_private,
+            && ngx_http_parse_multi_header_lines(r, cc, &ngx_http_gzip_private,
                                                  NULL)
-               >= 0)
+               != NULL)
         {
             goto ok;
         }
@@ -2712,12 +2712,12 @@ ngx_http_set_disable_symlinks(ngx_http_request_t *r,
 
 ngx_int_t
 ngx_http_get_forwarded_addr(ngx_http_request_t *r, ngx_addr_t *addr,
-    ngx_array_t *headers, ngx_str_t *value, ngx_array_t *proxies,
+    ngx_table_elt_t *headers, ngx_str_t *value, ngx_array_t *proxies,
     int recursive)
 {
-    ngx_int_t          rc;
-    ngx_uint_t         i, found;
-    ngx_table_elt_t  **h;
+    ngx_int_t         rc;
+    ngx_uint_t        found;
+    ngx_table_elt_t  *h, *next;
 
     if (headers == NULL) {
         return ngx_http_get_forwarded_addr_internal(r, addr, value->data,
@@ -2725,16 +2725,23 @@ ngx_http_get_forwarded_addr(ngx_http_request_t *r, ngx_addr_t *addr,
                                                     recursive);
     }
 
-    i = headers->nelts;
-    h = headers->elts;
+    /* revert headers order */
+
+    for (h = headers, headers = NULL; h; h = next) {
+        next = h->next;
+        h->next = headers;
+        headers = h;
+    }
+
+    /* iterate over all headers in reverse order */
 
     rc = NGX_DECLINED;
 
     found = 0;
 
-    while (i-- > 0) {
-        rc = ngx_http_get_forwarded_addr_internal(r, addr, h[i]->value.data,
-                                                  h[i]->value.len, proxies,
+    for (h = headers; h; h = h->next) {
+        rc = ngx_http_get_forwarded_addr_internal(r, addr, h->value.data,
+                                                  h->value.len, proxies,
                                                   recursive);
 
         if (!recursive) {
@@ -2753,6 +2760,14 @@ ngx_http_get_forwarded_addr(ngx_http_request_t *r, ngx_addr_t *addr,
         found = 1;
     }
 
+    /* restore headers order */
+
+    for (h = headers, headers = NULL; h; h = next) {
+        next = h->next;
+        h->next = headers;
+        headers = h;
+    }
+
     return rc;
 }
 
@@ -2802,6 +2817,80 @@ ngx_http_get_forwarded_addr_internal(ngx_http_request_t *r, ngx_addr_t *addr,
 }
 
 
+ngx_int_t
+ngx_http_link_multi_headers(ngx_http_request_t *r)
+{
+    ngx_uint_t        i, j;
+    ngx_list_part_t  *part, *ppart;
+    ngx_table_elt_t  *header, *pheader, **ph;
+
+    if (r->headers_in.multi_linked) {
+        return NGX_OK;
+    }
+
+    r->headers_in.multi_linked = 1;
+
+    part = &r->headers_in.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;
+        }
+
+        header[i].next = NULL;
+
+        /*
+         * search for previous headers with the same name;
+         * if there are any, link to them
+         */
+
+        ppart = &r->headers_in.headers.part;
+        pheader = ppart->elts;
+
+        for (j = 0; /* void */; j++) {
+
+            if (j >= ppart->nelts) {
+                if (ppart->next == NULL) {
+                    break;
+                }
+
+                ppart = ppart->next;
+                pheader = ppart->elts;
+                j = 0;
+            }
+
+            if (part == ppart && i == j) {
+                break;
+            }
+
+            if (header[i].key.len == pheader[j].key.len
+                && ngx_strncasecmp(header[i].key.data, pheader[j].key.data,
+                                   header[i].key.len)
+                   == 0)
+            {
+                ph = &pheader[j].next;
+                while (*ph) { ph = &(*ph)->next; }
+                *ph = &header[i];
+
+                r->headers_in.multi = 1;
+
+                break;
+            }
+        }
+    }
+
+    return NGX_OK;
+}
+
+
 static char *
 ngx_http_core_server(ngx_conf_t *cf, ngx_command_t *cmd, void *dummy)
 {
@@ -2916,6 +3005,7 @@ ngx_http_core_server(ngx_conf_t *cf, ngx_command_t *cmd, void *dummy)
         lsopt.socklen = sizeof(struct sockaddr_in);
 
         lsopt.backlog = NGX_LISTEN_BACKLOG;
+        lsopt.type = SOCK_STREAM;
         lsopt.rcvbuf = -1;
         lsopt.sndbuf = -1;
 #if (NGX_HAVE_SETFIB)
@@ -3871,7 +3961,7 @@ ngx_http_core_listen(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
 
     ngx_str_t              *value, size;
     ngx_url_t               u;
-    ngx_uint_t              n;
+    ngx_uint_t              n, i, backlog;
     ngx_http_listen_opt_t   lsopt;
 
     cscf->listen = 1;
@@ -3897,6 +3987,7 @@ ngx_http_core_listen(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
     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)
@@ -3909,6 +4000,8 @@ ngx_http_core_listen(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
     lsopt.ipv6only = 1;
 #endif
 
+    backlog = 0;
+
     for (n = 2; n < cf->args->nelts; n++) {
 
         if (ngx_strcmp(value[n].data, "default_server") == 0
@@ -3967,6 +4060,8 @@ ngx_http_core_listen(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
                 return NGX_CONF_ERROR;
             }
 
+            backlog = 1;
+
             continue;
         }
 
@@ -4085,6 +4180,11 @@ ngx_http_core_listen(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
 
         if (ngx_strcmp(value[n].data, "http2") == 0) {
 #if (NGX_HTTP_V2)
+            ngx_conf_log_error(NGX_LOG_WARN, cf, 0,
+                               "the \"listen ... http2\" directive "
+                               "is deprecated, use "
+                               "the \"http2\" directive instead");
+
             lsopt.http2 = 1;
             continue;
 #else
@@ -4095,6 +4195,19 @@ ngx_http_core_listen(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
 #endif
         }
 
+        if (ngx_strcmp(value[n].data, "quic") == 0) {
+#if (NGX_HTTP_V3)
+            lsopt.quic = 1;
+            lsopt.type = SOCK_DGRAM;
+            continue;
+#else
+            ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
+                               "the \"quic\" parameter requires "
+                               "ngx_http_v3_module");
+            return NGX_CONF_ERROR;
+#endif
+        }
+
         if (ngx_strncmp(value[n].data, "so_keepalive=", 13) == 0) {
 
             if (ngx_strcmp(&value[n].data[13], "on") == 0) {
@@ -4196,7 +4309,61 @@ ngx_http_core_listen(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
         return NGX_CONF_ERROR;
     }
 
+    if (lsopt.quic) {
+#if (NGX_HAVE_TCP_FASTOPEN)
+        if (lsopt.fastopen != -1) {
+            return "\"fastopen\" parameter is incompatible with \"quic\"";
+        }
+#endif
+
+        if (backlog) {
+            return "\"backlog\" parameter is incompatible with \"quic\"";
+        }
+
+#if (NGX_HAVE_DEFERRED_ACCEPT && defined SO_ACCEPTFILTER)
+        if (lsopt.accept_filter) {
+            return "\"accept_filter\" parameter is incompatible with \"quic\"";
+        }
+#endif
+
+#if (NGX_HAVE_DEFERRED_ACCEPT && defined TCP_DEFER_ACCEPT)
+        if (lsopt.deferred_accept) {
+            return "\"deferred\" parameter is incompatible with \"quic\"";
+        }
+#endif
+
+#if (NGX_HTTP_SSL)
+        if (lsopt.ssl) {
+            return "\"ssl\" parameter is incompatible with \"quic\"";
+        }
+#endif
+
+#if (NGX_HTTP_V2)
+        if (lsopt.http2) {
+            return "\"http2\" parameter is incompatible with \"quic\"";
+        }
+#endif
+
+        if (lsopt.so_keepalive) {
+            return "\"so_keepalive\" parameter is incompatible with \"quic\"";
+        }
+
+        if (lsopt.proxy_protocol) {
+            return "\"proxy_protocol\" parameter is incompatible with \"quic\"";
+        }
+    }
+
     for (n = 0; n < u.naddrs; n++) {
+
+        for (i = 0; i < n; i++) {
+            if (ngx_cmp_sockaddr(u.addrs[n].sockaddr, u.addrs[n].socklen,
+                                 u.addrs[i].sockaddr, u.addrs[i].socklen, 1)
+                == NGX_OK)
+            {
+                goto next;
+            }
+        }
+
         lsopt.sockaddr = u.addrs[n].sockaddr;
         lsopt.socklen = u.addrs[n].socklen;
         lsopt.addr_text = u.addrs[n].name;
@@ -4205,6 +4372,9 @@ ngx_http_core_listen(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
         if (ngx_http_add_listen(cf, cscf, &lsopt) != NGX_OK) {
             return NGX_CONF_ERROR;
         }
+
+    next:
+        continue;
     }
 
     return NGX_CONF_OK;
diff --git a/src/http/ngx_http_core_module.h b/src/http/ngx_http_core_module.h
index 004a98e..765e7ff 100644
--- a/src/http/ngx_http_core_module.h
+++ b/src/http/ngx_http_core_module.h
@@ -75,6 +75,7 @@ typedef struct {
     unsigned                   wildcard:1;
     unsigned                   ssl:1;
     unsigned                   http2:1;
+    unsigned                   quic:1;
 #if (NGX_HAVE_INET6)
     unsigned                   ipv6only:1;
 #endif
@@ -86,6 +87,7 @@ typedef struct {
     int                        backlog;
     int                        rcvbuf;
     int                        sndbuf;
+    int                        type;
 #if (NGX_HAVE_SETFIB)
     int                        setfib;
 #endif
@@ -237,6 +239,7 @@ struct ngx_http_addr_conf_s {
 
     unsigned                   ssl:1;
     unsigned                   http2:1;
+    unsigned                   quic:1;
     unsigned                   proxy_protocol:1;
 };
 
@@ -266,6 +269,7 @@ typedef struct {
 
 typedef struct {
     ngx_int_t                  family;
+    ngx_int_t                  type;
     in_port_t                  port;
     ngx_array_t                addrs;     /* array of ngx_http_conf_addr_t */
 } ngx_http_conf_port_t;
@@ -274,6 +278,10 @@ typedef struct {
 typedef struct {
     ngx_http_listen_opt_t      opt;
 
+    unsigned                   protocols:3;
+    unsigned                   protocols_set:1;
+    unsigned                   protocols_changed:1;
+
     ngx_hash_t                 hash;
     ngx_hash_wildcard_t       *wc_head;
     ngx_hash_wildcard_t       *wc_tail;
@@ -463,8 +471,8 @@ struct ngx_http_location_tree_node_s {
     ngx_http_core_loc_conf_t        *exact;
     ngx_http_core_loc_conf_t        *inclusive;
 
+    u_short                          len;
     u_char                           auto_redirect;
-    u_char                           len;
     u_char                           name[1];
 };
 
@@ -529,9 +537,11 @@ ngx_int_t ngx_http_set_disable_symlinks(ngx_http_request_t *r,
     ngx_http_core_loc_conf_t *clcf, ngx_str_t *path, ngx_open_file_info_t *of);
 
 ngx_int_t ngx_http_get_forwarded_addr(ngx_http_request_t *r, ngx_addr_t *addr,
-    ngx_array_t *headers, ngx_str_t *value, ngx_array_t *proxies,
+    ngx_table_elt_t *headers, ngx_str_t *value, ngx_array_t *proxies,
     int recursive);
 
+ngx_int_t ngx_http_link_multi_headers(ngx_http_request_t *r);
+
 
 extern ngx_module_t  ngx_http_core_module;
 
diff --git a/src/http/ngx_http_file_cache.c b/src/http/ngx_http_file_cache.c
index c40093b..5209f00 100644
--- a/src/http/ngx_http_file_cache.c
+++ b/src/http/ngx_http_file_cache.c
@@ -14,7 +14,7 @@
 static ngx_int_t ngx_http_file_cache_lock(ngx_http_request_t *r,
     ngx_http_cache_t *c);
 static void ngx_http_file_cache_lock_wait_handler(ngx_event_t *ev);
-static void ngx_http_file_cache_lock_wait(ngx_http_request_t *r,
+static ngx_int_t ngx_http_file_cache_lock_wait(ngx_http_request_t *r,
     ngx_http_cache_t *c);
 static ngx_int_t ngx_http_file_cache_read(ngx_http_request_t *r,
     ngx_http_cache_t *c);
@@ -463,6 +463,7 @@ ngx_http_file_cache_lock(ngx_http_request_t *r, ngx_http_cache_t *c)
 static void
 ngx_http_file_cache_lock_wait_handler(ngx_event_t *ev)
 {
+    ngx_int_t            rc;
     ngx_connection_t    *c;
     ngx_http_request_t  *r;
 
@@ -474,13 +475,31 @@ ngx_http_file_cache_lock_wait_handler(ngx_event_t *ev)
     ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,
                    "http file cache wait: \"%V?%V\"", &r->uri, &r->args);
 
-    ngx_http_file_cache_lock_wait(r, r->cache);
+    rc = ngx_http_file_cache_lock_wait(r, r->cache);
 
-    ngx_http_run_posted_requests(c);
+    if (rc == NGX_AGAIN) {
+        return;
+    }
+
+    r->cache->waiting = 0;
+    r->main->blocked--;
+
+    if (r->main->terminated) {
+        /*
+         * trigger connection event handler if the request was
+         * terminated
+         */
+
+        c->write->handler(c->write);
+
+    } else {
+        r->write_event_handler(r);
+        ngx_http_run_posted_requests(c);
+    }
 }
 
 
-static void
+static ngx_int_t
 ngx_http_file_cache_lock_wait(ngx_http_request_t *r, ngx_http_cache_t *c)
 {
     ngx_uint_t              wait;
@@ -495,7 +514,7 @@ ngx_http_file_cache_lock_wait(ngx_http_request_t *r, ngx_http_cache_t *c)
         ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
                       "cache lock timeout");
         c->lock_timeout = 0;
-        goto wakeup;
+        return NGX_OK;
     }
 
     cache = c->file_cache;
@@ -513,14 +532,10 @@ ngx_http_file_cache_lock_wait(ngx_http_request_t *r, ngx_http_cache_t *c)
 
     if (wait) {
         ngx_add_timer(&c->wait_event, (timer > 500) ? 500 : timer);
-        return;
+        return NGX_AGAIN;
     }
 
-wakeup:
-
-    c->waiting = 0;
-    r->main->blocked--;
-    r->write_event_handler(r);
+    return NGX_OK;
 }
 
 
@@ -690,6 +705,8 @@ ngx_http_file_cache_aio_read(ngx_http_request_t *r, ngx_http_cache_t *c)
         c->file.aio->data = r;
         c->file.aio->handler = ngx_http_cache_aio_event_handler;
 
+        ngx_add_timer(&c->file.aio->event, 60000);
+
         r->main->blocked++;
         r->aio = 1;
 
@@ -737,12 +754,32 @@ ngx_http_cache_aio_event_handler(ngx_event_t *ev)
     ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,
                    "http file cache aio: \"%V?%V\"", &r->uri, &r->args);
 
+    if (ev->timedout) {
+        ngx_log_error(NGX_LOG_ALERT, c->log, 0,
+                      "aio operation took too long");
+        ev->timedout = 0;
+        return;
+    }
+
+    if (ev->timer_set) {
+        ngx_del_timer(ev);
+    }
+
     r->main->blocked--;
     r->aio = 0;
 
-    r->write_event_handler(r);
+    if (r->main->terminated) {
+        /*
+         * trigger connection event handler if the request was
+         * terminated
+         */
+
+        c->write->handler(c->write);
 
-    ngx_http_run_posted_requests(c);
+    } else {
+        r->write_event_handler(r);
+        ngx_http_run_posted_requests(c);
+    }
 }
 
 #endif
@@ -786,6 +823,8 @@ ngx_http_cache_thread_handler(ngx_thread_task_t *task, ngx_file_t *file)
         return NGX_ERROR;
     }
 
+    ngx_add_timer(&task->event, 60000);
+
     r->main->blocked++;
     r->aio = 1;
 
@@ -807,12 +846,32 @@ ngx_http_cache_thread_event_handler(ngx_event_t *ev)
     ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,
                    "http file cache thread: \"%V?%V\"", &r->uri, &r->args);
 
+    if (ev->timedout) {
+        ngx_log_error(NGX_LOG_ALERT, c->log, 0,
+                      "thread operation took too long");
+        ev->timedout = 0;
+        return;
+    }
+
+    if (ev->timer_set) {
+        ngx_del_timer(ev);
+    }
+
     r->main->blocked--;
     r->aio = 0;
 
-    r->write_event_handler(r);
+    if (r->main->terminated) {
+        /*
+         * trigger connection event handler if the request was
+         * terminated
+         */
+
+        c->write->handler(c->write);
 
-    ngx_http_run_posted_requests(c);
+    } else {
+        r->write_event_handler(r);
+        ngx_http_run_posted_requests(c);
+    }
 }
 
 #endif
@@ -1575,10 +1634,6 @@ ngx_http_cache_send(ngx_http_request_t *r)
     ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                    "http file cache send: %s", c->file.name.data);
 
-    if (r != r->main && c->length - c->body_start == 0) {
-        return ngx_http_send_header(r);
-    }
-
     /* we need to allocate all before the header would be sent */
 
     b = ngx_calloc_buf(r->pool);
@@ -1600,9 +1655,10 @@ ngx_http_cache_send(ngx_http_request_t *r)
     b->file_pos = c->body_start;
     b->file_last = c->length;
 
-    b->in_file = (c->length - c->body_start) ? 1: 0;
-    b->last_buf = (r == r->main) ? 1: 0;
+    b->in_file = (c->length - c->body_start) ? 1 : 0;
+    b->last_buf = (r == r->main) ? 1 : 0;
     b->last_in_chain = 1;
+    b->sync = (b->last_buf || b->in_file) ? 0 : 1;
 
     b->file->fd = c->file.fd;
     b->file->name = c->file.name;
@@ -1756,6 +1812,11 @@ ngx_http_file_cache_forced_expire(ngx_http_file_cache_t *cache)
             break;
         }
 
+        if (fcn->deleting) {
+            wait = 1;
+            break;
+        }
+
         p = ngx_hex_dump(key, (u_char *) &fcn->node.key,
                          sizeof(ngx_rbtree_key_t));
         len = NGX_HTTP_CACHE_KEY_LEN - sizeof(ngx_rbtree_key_t);
diff --git a/src/http/ngx_http_huff_decode.c b/src/http/ngx_http_huff_decode.c
index 14b7b78..a65c3c3 100644
--- a/src/http/ngx_http_huff_decode.c
+++ b/src/http/ngx_http_huff_decode.c
@@ -2657,7 +2657,7 @@ ngx_http_huff_decode(u_char *state, u_char *src, size_t len, u_char **dst,
             != NGX_OK)
         {
             ngx_log_debug2(NGX_LOG_DEBUG_HTTP, log, 0,
-                           "http2 huffman decoding error at state %d: "
+                           "http huffman decoding error at state %d: "
                            "bad code 0x%Xd", *state, ch >> 4);
 
             return NGX_ERROR;
@@ -2667,7 +2667,7 @@ ngx_http_huff_decode(u_char *state, u_char *src, size_t len, u_char **dst,
             != NGX_OK)
         {
             ngx_log_debug2(NGX_LOG_DEBUG_HTTP, log, 0,
-                           "http2 huffman decoding error at state %d: "
+                           "http huffman decoding error at state %d: "
                            "bad code 0x%Xd", *state, ch & 0xf);
 
             return NGX_ERROR;
@@ -2677,7 +2677,7 @@ ngx_http_huff_decode(u_char *state, u_char *src, size_t len, u_char **dst,
     if (last) {
         if (!ending) {
             ngx_log_debug1(NGX_LOG_DEBUG_HTTP, log, 0,
-                           "http2 huffman decoding error: "
+                           "http huffman decoding error: "
                            "incomplete code 0x%Xd", ch);
 
             return NGX_ERROR;
diff --git a/src/http/ngx_http_parse.c b/src/http/ngx_http_parse.c
index 6460da2..f7e5038 100644
--- a/src/http/ngx_http_parse.c
+++ b/src/http/ngx_http_parse.c
@@ -451,19 +451,16 @@ ngx_http_parse_request_line(ngx_http_request_t *r, ngx_buf_t *b)
 
             switch (ch) {
             case '/':
-                r->port_end = p;
                 r->uri_start = p;
                 state = sw_after_slash_in_uri;
                 break;
             case '?':
-                r->port_end = p;
                 r->uri_start = p;
                 r->args_start = p + 1;
                 r->empty_path_in_uri = 1;
                 state = sw_uri;
                 break;
             case ' ':
-                r->port_end = p;
                 /*
                  * use single "/" from request line to preserve pointers,
                  * if request line will be copied to large client buffer
@@ -1960,27 +1957,24 @@ unsafe:
 }
 
 
-ngx_int_t
-ngx_http_parse_multi_header_lines(ngx_array_t *headers, ngx_str_t *name,
-    ngx_str_t *value)
+ngx_table_elt_t *
+ngx_http_parse_multi_header_lines(ngx_http_request_t *r,
+    ngx_table_elt_t *headers, ngx_str_t *name, ngx_str_t *value)
 {
-    ngx_uint_t         i;
-    u_char            *start, *last, *end, ch;
-    ngx_table_elt_t  **h;
-
-    h = headers->elts;
+    u_char           *start, *last, *end, ch;
+    ngx_table_elt_t  *h;
 
-    for (i = 0; i < headers->nelts; i++) {
+    for (h = headers; h; h = h->next) {
 
-        ngx_log_debug2(NGX_LOG_DEBUG_HTTP, headers->pool->log, 0,
-                       "parse header: \"%V: %V\"", &h[i]->key, &h[i]->value);
+        ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
+                       "parse header: \"%V: %V\"", &h->key, &h->value);
 
-        if (name->len > h[i]->value.len) {
+        if (name->len > h->value.len) {
             continue;
         }
 
-        start = h[i]->value.data;
-        end = h[i]->value.data + h[i]->value.len;
+        start = h->value.data;
+        end = h->value.data + h->value.len;
 
         while (start < end) {
 
@@ -1994,7 +1988,7 @@ ngx_http_parse_multi_header_lines(ngx_array_t *headers, ngx_str_t *name,
 
             if (value == NULL) {
                 if (start == end || *start == ',') {
-                    return i;
+                    return h;
                 }
 
                 goto skip;
@@ -2014,7 +2008,7 @@ ngx_http_parse_multi_header_lines(ngx_array_t *headers, ngx_str_t *name,
             value->len = last - start;
             value->data = start;
 
-            return i;
+            return h;
 
         skip:
 
@@ -2029,31 +2023,28 @@ ngx_http_parse_multi_header_lines(ngx_array_t *headers, ngx_str_t *name,
         }
     }
 
-    return NGX_DECLINED;
+    return NULL;
 }
 
 
-ngx_int_t
-ngx_http_parse_set_cookie_lines(ngx_array_t *headers, ngx_str_t *name,
-    ngx_str_t *value)
+ngx_table_elt_t *
+ngx_http_parse_set_cookie_lines(ngx_http_request_t *r,
+    ngx_table_elt_t *headers, ngx_str_t *name, ngx_str_t *value)
 {
-    ngx_uint_t         i;
-    u_char            *start, *last, *end;
-    ngx_table_elt_t  **h;
-
-    h = headers->elts;
+    u_char           *start, *last, *end;
+    ngx_table_elt_t  *h;
 
-    for (i = 0; i < headers->nelts; i++) {
+    for (h = headers; h; h = h->next) {
 
-        ngx_log_debug2(NGX_LOG_DEBUG_HTTP, headers->pool->log, 0,
-                       "parse header: \"%V: %V\"", &h[i]->key, &h[i]->value);
+        ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
+                       "parse header: \"%V: %V\"", &h->key, &h->value);
 
-        if (name->len >= h[i]->value.len) {
+        if (name->len >= h->value.len) {
             continue;
         }
 
-        start = h[i]->value.data;
-        end = h[i]->value.data + h[i]->value.len;
+        start = h->value.data;
+        end = h->value.data + h->value.len;
 
         if (ngx_strncasecmp(start, name->data, name->len) != 0) {
             continue;
@@ -2077,10 +2068,10 @@ ngx_http_parse_set_cookie_lines(ngx_array_t *headers, ngx_str_t *name,
         value->len = last - start;
         value->data = start;
 
-        return i;
+        return h;
     }
 
-    return NGX_DECLINED;
+    return NULL;
 }
 
 
diff --git a/src/http/ngx_http_request.c b/src/http/ngx_http_request.c
index 013b715..9593b7f 100644
--- a/src/http/ngx_http_request.c
+++ b/src/http/ngx_http_request.c
@@ -22,8 +22,6 @@ static ngx_int_t ngx_http_process_header_line(ngx_http_request_t *r,
     ngx_table_elt_t *h, ngx_uint_t offset);
 static ngx_int_t ngx_http_process_unique_header_line(ngx_http_request_t *r,
     ngx_table_elt_t *h, ngx_uint_t offset);
-static ngx_int_t ngx_http_process_multi_header_lines(ngx_http_request_t *r,
-    ngx_table_elt_t *h, ngx_uint_t offset);
 static ngx_int_t ngx_http_process_host(ngx_http_request_t *r,
     ngx_table_elt_t *h, ngx_uint_t offset);
 static ngx_int_t ngx_http_process_connection(ngx_http_request_t *r,
@@ -31,10 +29,6 @@ static ngx_int_t ngx_http_process_connection(ngx_http_request_t *r,
 static ngx_int_t ngx_http_process_user_agent(ngx_http_request_t *r,
     ngx_table_elt_t *h, ngx_uint_t offset);
 
-static ngx_int_t ngx_http_validate_host(ngx_str_t *host, ngx_pool_t *pool,
-    ngx_uint_t alloc);
-static ngx_int_t ngx_http_set_virtual_server(ngx_http_request_t *r,
-    ngx_str_t *host);
 static ngx_int_t ngx_http_find_virtual_server(ngx_connection_t *c,
     ngx_http_virtual_names_t *virtual_names, ngx_str_t *host,
     ngx_http_request_t *r, ngx_http_core_srv_conf_t **cscfp);
@@ -52,7 +46,6 @@ static void ngx_http_keepalive_handler(ngx_event_t *ev);
 static void ngx_http_set_lingering_close(ngx_connection_t *c);
 static void ngx_http_lingering_close_handler(ngx_event_t *ev);
 static ngx_int_t ngx_http_post_action(ngx_http_request_t *r);
-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);
@@ -164,7 +157,7 @@ ngx_http_header_t  ngx_http_headers_in[] = {
 #if (NGX_HTTP_X_FORWARDED_FOR)
     { ngx_string("X-Forwarded-For"),
                  offsetof(ngx_http_headers_in_t, x_forwarded_for),
-                 ngx_http_process_multi_header_lines },
+                 ngx_http_process_header_line },
 #endif
 
 #if (NGX_HTTP_REALIP)
@@ -196,8 +189,8 @@ ngx_http_header_t  ngx_http_headers_in[] = {
                  ngx_http_process_header_line },
 #endif
 
-    { ngx_string("Cookie"), offsetof(ngx_http_headers_in_t, cookies),
-                 ngx_http_process_multi_header_lines },
+    { ngx_string("Cookie"), offsetof(ngx_http_headers_in_t, cookie),
+                 ngx_http_process_header_line },
 
     { ngx_null_string, 0, NULL }
 };
@@ -325,24 +318,19 @@ ngx_http_init_connection(ngx_connection_t *c)
     rev->handler = ngx_http_wait_request_handler;
     c->write->handler = ngx_http_empty_handler;
 
-#if (NGX_HTTP_V2)
-    if (hc->addr_conf->http2) {
-        rev->handler = ngx_http_v2_init;
+#if (NGX_HTTP_V3)
+    if (hc->addr_conf->quic) {
+        ngx_http_v3_init_stream(c);
+        return;
     }
 #endif
 
 #if (NGX_HTTP_SSL)
-    {
-    ngx_http_ssl_srv_conf_t  *sscf;
-
-    sscf = ngx_http_get_module_srv_conf(hc->conf_ctx, ngx_http_ssl_module);
-
-    if (sscf->enable || hc->addr_conf->ssl) {
+    if (hc->addr_conf->ssl) {
         hc->ssl = 1;
         c->log->action = "SSL handshaking";
         rev->handler = ngx_http_ssl_handshake;
     }
-    }
 #endif
 
     if (hc->addr_conf->proxy_protocol) {
@@ -383,6 +371,9 @@ ngx_http_wait_request_handler(ngx_event_t *rev)
     ngx_buf_t                 *b;
     ngx_connection_t          *c;
     ngx_http_connection_t     *hc;
+#if (NGX_HTTP_V2)
+    ngx_http_v2_srv_conf_t    *h2scf;
+#endif
     ngx_http_core_srv_conf_t  *cscf;
 
     c = rev->data;
@@ -429,6 +420,8 @@ ngx_http_wait_request_handler(ngx_event_t *rev)
         b->end = b->last + size;
     }
 
+    size = b->end - b->last;
+
     n = c->recv(c, b->last, size);
 
     if (n == NGX_AGAIN) {
@@ -443,12 +436,16 @@ ngx_http_wait_request_handler(ngx_event_t *rev)
             return;
         }
 
-        /*
-         * We are trying to not hold c->buffer's memory for an idle connection.
-         */
+        if (b->pos == b->last) {
 
-        if (ngx_pfree(c->pool, b->start) == NGX_OK) {
-            b->start = NULL;
+            /*
+             * We are trying to not hold c->buffer's memory for an
+             * idle connection.
+             */
+
+            if (ngx_pfree(c->pool, b->start) == NGX_OK) {
+                b->start = NULL;
+            }
         }
 
         return;
@@ -489,6 +486,29 @@ ngx_http_wait_request_handler(ngx_event_t *rev)
         }
     }
 
+#if (NGX_HTTP_V2)
+
+    h2scf = ngx_http_get_module_srv_conf(hc->conf_ctx, ngx_http_v2_module);
+
+    if (!hc->ssl && (h2scf->enable || hc->addr_conf->http2)) {
+
+        size = ngx_min(sizeof(NGX_HTTP_V2_PREFACE) - 1,
+                       (size_t) (b->last - b->pos));
+
+        if (ngx_memcmp(b->pos, NGX_HTTP_V2_PREFACE, size) == 0) {
+
+            if (size == sizeof(NGX_HTTP_V2_PREFACE) - 1) {
+                ngx_http_v2_init(rev);
+                return;
+            }
+
+            ngx_post_event(rev, &ngx_posted_events);
+            return;
+        }
+    }
+
+#endif
+
     c->log->action = "reading client request line";
 
     ngx_reusable_connection(c, 0);
@@ -808,13 +828,16 @@ ngx_http_ssl_handshake_handler(ngx_connection_t *c)
 #if (NGX_HTTP_V2                                                              \
      && defined TLSEXT_TYPE_application_layer_protocol_negotiation)
         {
-        unsigned int            len;
-        const unsigned char    *data;
-        ngx_http_connection_t  *hc;
+        unsigned int             len;
+        const unsigned char     *data;
+        ngx_http_connection_t   *hc;
+        ngx_http_v2_srv_conf_t  *h2scf;
 
         hc = c->data;
 
-        if (hc->addr_conf->http2) {
+        h2scf = ngx_http_get_module_srv_conf(hc->conf_ctx, ngx_http_v2_module);
+
+        if (h2scf->enable || hc->addr_conf->http2) {
 
             SSL_get0_alpn_selected(c->ssl->connection, &data, &len);
 
@@ -909,6 +932,31 @@ ngx_http_ssl_servername(ngx_ssl_conn_t *ssl_conn, int *ad, void *arg)
         goto done;
     }
 
+    sscf = ngx_http_get_module_srv_conf(cscf->ctx, ngx_http_ssl_module);
+
+#if (defined TLS1_3_VERSION                                                   \
+     && !defined LIBRESSL_VERSION_NUMBER && !defined OPENSSL_IS_BORINGSSL)
+
+    /*
+     * SSL_SESSION_get0_hostname() is only available in OpenSSL 1.1.1+,
+     * but servername being negotiated in every TLSv1.3 handshake
+     * is only returned in OpenSSL 1.1.1+ as well
+     */
+
+    if (sscf->verify) {
+        const char  *hostname;
+
+        hostname = SSL_SESSION_get0_hostname(SSL_get0_session(ssl_conn));
+
+        if (hostname != NULL && ngx_strcmp(hostname, servername) != 0) {
+            c->ssl->handshake_rejected = 1;
+            *ad = SSL_AD_ACCESS_DENIED;
+            return SSL_TLSEXT_ERR_ALERT_FATAL;
+        }
+    }
+
+#endif
+
     hc->ssl_servername = ngx_palloc(c->pool, sizeof(ngx_str_t));
     if (hc->ssl_servername == NULL) {
         goto error;
@@ -922,8 +970,6 @@ ngx_http_ssl_servername(ngx_ssl_conn_t *ssl_conn, int *ad, void *arg)
 
     ngx_set_connection_log(c, clcf->error_log);
 
-    sscf = ngx_http_get_module_srv_conf(hc->conf_ctx, ngx_http_ssl_module);
-
     c->ssl->buffer_size = sscf->buffer_size;
 
     if (sscf->ssl.ctx) {
@@ -951,6 +997,14 @@ ngx_http_ssl_servername(ngx_ssl_conn_t *ssl_conn, int *ad, void *arg)
 
 #ifdef SSL_OP_NO_RENEGOTIATION
         SSL_set_options(ssl_conn, SSL_OP_NO_RENEGOTIATION);
+#endif
+
+#ifdef SSL_OP_ENABLE_MIDDLEBOX_COMPAT
+#if (NGX_HTTP_V3)
+        if (c->listening->quic) {
+            SSL_clear_options(ssl_conn, SSL_OP_ENABLE_MIDDLEBOX_COMPAT);
+        }
+#endif
 #endif
     }
 
@@ -1687,14 +1741,23 @@ ngx_http_alloc_large_header_buffer(ngx_http_request_t *r,
             r->request_end = new + (r->request_end - old);
         }
 
-        r->method_end = new + (r->method_end - old);
+        if (r->method_end) {
+            r->method_end = new + (r->method_end - old);
+        }
+
+        if (r->uri_start) {
+            r->uri_start = new + (r->uri_start - old);
+        }
 
-        r->uri_start = new + (r->uri_start - old);
-        r->uri_end = new + (r->uri_end - old);
+        if (r->uri_end) {
+            r->uri_end = new + (r->uri_end - old);
+        }
 
         if (r->schema_start) {
             r->schema_start = new + (r->schema_start - old);
-            r->schema_end = new + (r->schema_end - old);
+            if (r->schema_end) {
+                r->schema_end = new + (r->schema_end - old);
+            }
         }
 
         if (r->host_start) {
@@ -1704,11 +1767,6 @@ ngx_http_alloc_large_header_buffer(ngx_http_request_t *r,
             }
         }
 
-        if (r->port_start) {
-            r->port_start = new + (r->port_start - old);
-            r->port_end = new + (r->port_end - old);
-        }
-
         if (r->uri_ext) {
             r->uri_ext = new + (r->uri_ext - old);
         }
@@ -1723,9 +1781,18 @@ ngx_http_alloc_large_header_buffer(ngx_http_request_t *r,
 
     } else {
         r->header_name_start = new;
-        r->header_name_end = new + (r->header_name_end - old);
-        r->header_start = new + (r->header_start - old);
-        r->header_end = new + (r->header_end - old);
+
+        if (r->header_name_end) {
+            r->header_name_end = new + (r->header_name_end - old);
+        }
+
+        if (r->header_start) {
+            r->header_start = new + (r->header_start - old);
+        }
+
+        if (r->header_end) {
+            r->header_end = new + (r->header_end - old);
+        }
     }
 
     r->header_in = b;
@@ -1742,9 +1809,10 @@ ngx_http_process_header_line(ngx_http_request_t *r, ngx_table_elt_t *h,
 
     ph = (ngx_table_elt_t **) ((char *) &r->headers_in + offset);
 
-    if (*ph == NULL) {
-        *ph = h;
-    }
+    while (*ph) { ph = &(*ph)->next; }
+
+    *ph = h;
+    h->next = NULL;
 
     return NGX_OK;
 }
@@ -1760,6 +1828,7 @@ ngx_http_process_unique_header_line(ngx_http_request_t *r, ngx_table_elt_t *h,
 
     if (*ph == NULL) {
         *ph = h;
+        h->next = NULL;
         return NGX_OK;
     }
 
@@ -1792,6 +1861,7 @@ ngx_http_process_host(ngx_http_request_t *r, ngx_table_elt_t *h,
     }
 
     r->headers_in.host = h;
+    h->next = NULL;
 
     host = h->value;
 
@@ -1827,6 +1897,10 @@ static ngx_int_t
 ngx_http_process_connection(ngx_http_request_t *r, ngx_table_elt_t *h,
     ngx_uint_t offset)
 {
+    if (ngx_http_process_header_line(r, h, offset) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
     if (ngx_strcasestrn(h->value.data, "close", 5 - 1)) {
         r->headers_in.connection_type = NGX_HTTP_CONNECTION_CLOSE;
 
@@ -1844,12 +1918,10 @@ ngx_http_process_user_agent(ngx_http_request_t *r, ngx_table_elt_t *h,
 {
     u_char  *user_agent, *msie;
 
-    if (r->headers_in.user_agent) {
-        return NGX_OK;
+    if (ngx_http_process_header_line(r, h, offset) != NGX_OK) {
+        return NGX_ERROR;
     }
 
-    r->headers_in.user_agent = h;
-
     /* check some widespread browsers while the header is in CPU cache */
 
     user_agent = h->value.data;
@@ -1911,35 +1983,6 @@ ngx_http_process_user_agent(ngx_http_request_t *r, ngx_table_elt_t *h,
 }
 
 
-static ngx_int_t
-ngx_http_process_multi_header_lines(ngx_http_request_t *r, ngx_table_elt_t *h,
-    ngx_uint_t offset)
-{
-    ngx_array_t       *headers;
-    ngx_table_elt_t  **ph;
-
-    headers = (ngx_array_t *) ((char *) &r->headers_in + offset);
-
-    if (headers->elts == NULL) {
-        if (ngx_array_init(headers, r->pool, 1, sizeof(ngx_table_elt_t *))
-            != NGX_OK)
-        {
-            ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
-            return NGX_ERROR;
-        }
-    }
-
-    ph = ngx_array_push(headers);
-    if (ph == NULL) {
-        ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
-        return NGX_ERROR;
-    }
-
-    *ph = h;
-    return NGX_OK;
-}
-
-
 ngx_int_t
 ngx_http_process_request_header(ngx_http_request_t *r)
 {
@@ -2121,7 +2164,7 @@ ngx_http_process_request(ngx_http_request_t *r)
 }
 
 
-static ngx_int_t
+ngx_int_t
 ngx_http_validate_host(ngx_str_t *host, ngx_pool_t *pool, ngx_uint_t alloc)
 {
     u_char  *h, ch;
@@ -2213,7 +2256,7 @@ ngx_http_validate_host(ngx_str_t *host, ngx_pool_t *pool, ngx_uint_t alloc)
 }
 
 
-static ngx_int_t
+ngx_int_t
 ngx_http_set_virtual_server(ngx_http_request_t *r, ngx_str_t *host)
 {
     ngx_int_t                  rc;
@@ -2674,6 +2717,8 @@ ngx_http_terminate_request(ngx_http_request_t *r, ngx_int_t rc)
     ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                    "http terminate request count:%d", mr->count);
 
+    mr->terminated = 1;
+
     if (rc > 0 && (mr->headers_out.status == 0 || mr->connection->sent == 0)) {
         mr->headers_out.status = rc;
     }
@@ -2696,8 +2741,11 @@ ngx_http_terminate_request(ngx_http_request_t *r, ngx_int_t rc)
     if (mr->write_event_handler) {
 
         if (mr->blocked) {
+            r = r->connection->data;
+
             r->connection->error = 1;
             r->write_event_handler = ngx_http_request_finalizer;
+
             return;
         }
 
@@ -2736,6 +2784,13 @@ ngx_http_finalize_connection(ngx_http_request_t *r)
     }
 #endif
 
+#if (NGX_HTTP_V3)
+    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) {
@@ -2779,7 +2834,8 @@ ngx_http_finalize_connection(ngx_http_request_t *r)
         || (clcf->lingering_close == NGX_HTTP_LINGERING_ON
             && (r->lingering_close
                 || r->header_in->pos < r->header_in->last
-                || r->connection->read->ready)))
+                || r->connection->read->ready
+                || r->connection->pipeline)))
     {
         ngx_http_set_lingering_close(r->connection);
         return;
@@ -2950,6 +3006,20 @@ ngx_http_test_reading(ngx_http_request_t *r)
 
 #endif
 
+#if (NGX_HTTP_V3)
+
+    if (c->quic) {
+        if (rev->error) {
+            c->error = 1;
+            err = 0;
+            goto closed;
+        }
+
+        return;
+    }
+
+#endif
+
 #if (NGX_HAVE_KQUEUE)
 
     if (ngx_event_flags & NGX_USE_KQUEUE_EVENT) {
@@ -3149,6 +3219,7 @@ ngx_http_set_keepalive(ngx_http_request_t *r)
 
         c->sent = 0;
         c->destroyed = 0;
+        c->pipeline = 1;
 
         if (rev->timer_set) {
             ngx_del_timer(rev);
@@ -3614,7 +3685,7 @@ ngx_http_post_action(ngx_http_request_t *r)
 }
 
 
-static void
+void
 ngx_http_close_request(ngx_http_request_t *r, ngx_int_t rc)
 {
     ngx_connection_t  *c;
@@ -3701,7 +3772,12 @@ ngx_http_free_request(ngx_http_request_t *r, ngx_int_t rc)
 
     log->action = "closing request";
 
-    if (r->connection->timedout) {
+    if (r->connection->timedout
+#if (NGX_HTTP_V3)
+        && r->connection->quic == NULL
+#endif
+       )
+    {
         clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
 
         if (clcf->reset_timedout_connection) {
@@ -3774,6 +3850,12 @@ ngx_http_close_connection(ngx_connection_t *c)
 
 #endif
 
+#if (NGX_HTTP_V3)
+    if (c->quic) {
+        ngx_http_v3_reset_stream(c);
+    }
+#endif
+
 #if (NGX_STAT_STUB)
     (void) ngx_atomic_fetch_add(ngx_stat_active, -1);
 #endif
diff --git a/src/http/ngx_http_request.h b/src/http/ngx_http_request.h
index b1269d2..65c8333 100644
--- 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                   0x00000001
 #define NGX_HTTP_GET                       0x00000002
@@ -212,7 +213,7 @@ typedef struct {
     ngx_table_elt_t                  *keep_alive;
 
 #if (NGX_HTTP_X_FORWARDED_FOR)
-    ngx_array_t                       x_forwarded_for;
+    ngx_table_elt_t                  *x_forwarded_for;
 #endif
 
 #if (NGX_HTTP_REALIP)
@@ -231,17 +232,19 @@ typedef struct {
     ngx_table_elt_t                  *date;
 #endif
 
+    ngx_table_elt_t                  *cookie;
+
     ngx_str_t                         user;
     ngx_str_t                         passwd;
 
-    ngx_array_t                       cookies;
-
     ngx_str_t                         server;
     off_t                             content_length_n;
     time_t                            keep_alive_n;
 
     unsigned                          connection_type:2;
     unsigned                          chunked:1;
+    unsigned                          multi:1;
+    unsigned                          multi_linked:1;
     unsigned                          msie:1;
     unsigned                          msie6:1;
     unsigned                          opera:1;
@@ -272,6 +275,9 @@ typedef struct {
     ngx_table_elt_t                  *expires;
     ngx_table_elt_t                  *etag;
 
+    ngx_table_elt_t                  *cache_control;
+    ngx_table_elt_t                  *link;
+
     ngx_str_t                        *override_charset;
 
     size_t                            content_type_len;
@@ -280,9 +286,6 @@ typedef struct {
     u_char                           *content_type_lowcase;
     ngx_uint_t                        content_type_hash;
 
-    ngx_array_t                       cache_control;
-    ngx_array_t                       link;
-
     off_t                             content_length_n;
     off_t                             content_offset;
     time_t                            date_time;
@@ -449,6 +452,7 @@ struct ngx_http_request_s {
 
     ngx_http_connection_t            *http_connection;
     ngx_http_v2_stream_t             *stream;
+    ngx_http_v3_parse_t              *v3_parse;
 
     ngx_http_log_handler_pt           log_handler;
 
@@ -541,10 +545,12 @@ struct ngx_http_request_s {
     unsigned                          request_complete:1;
     unsigned                          request_output:1;
     unsigned                          header_sent:1;
+    unsigned                          response_sent:1;
     unsigned                          expect_tested:1;
     unsigned                          root_tested:1;
     unsigned                          done:1;
     unsigned                          logged:1;
+    unsigned                          terminated:1;
 
     unsigned                          buffered:4;
 
@@ -592,8 +598,6 @@ struct ngx_http_request_s {
     u_char                           *schema_end;
     u_char                           *host_start;
     u_char                           *host_end;
-    u_char                           *port_start;
-    u_char                           *port_end;
 
     unsigned                          http_minor:16;
     unsigned                          http_major:16;
diff --git a/src/http/ngx_http_request_body.c b/src/http/ngx_http_request_body.c
index ad3549f..afb0423 100644
--- a/src/http/ngx_http_request_body.c
+++ b/src/http/ngx_http_request_body.c
@@ -92,6 +92,13 @@ ngx_http_read_client_request_body(ngx_http_request_t *r,
     }
 #endif
 
+#if (NGX_HTTP_V3)
+    if (r->http_version == NGX_HTTP_VERSION_30) {
+        rc = ngx_http_v3_read_request_body(r);
+        goto done;
+    }
+#endif
+
     preread = r->header_in->last - r->header_in->pos;
 
     if (preread) {
@@ -238,6 +245,18 @@ ngx_http_read_unbuffered_request_body(ngx_http_request_t *r)
     }
 #endif
 
+#if (NGX_HTTP_V3)
+    if (r->http_version == NGX_HTTP_VERSION_30) {
+        rc = ngx_http_v3_read_unbuffered_request_body(r);
+
+        if (rc == NGX_OK) {
+            r->reading_body = 0;
+        }
+
+        return rc;
+    }
+#endif
+
     if (r->connection->read->timedout) {
         r->connection->timedout = 1;
         return NGX_HTTP_REQUEST_TIME_OUT;
@@ -625,6 +644,12 @@ ngx_http_discard_request_body(ngx_http_request_t *r)
     }
 #endif
 
+#if (NGX_HTTP_V3)
+    if (r->http_version == NGX_HTTP_VERSION_30) {
+        return NGX_OK;
+    }
+#endif
+
     if (ngx_http_test_expect(r) != NGX_OK) {
         return NGX_HTTP_INTERNAL_SERVER_ERROR;
     }
@@ -920,6 +945,9 @@ ngx_http_test_expect(ngx_http_request_t *r)
         || r->http_version < NGX_HTTP_VERSION_11
 #if (NGX_HTTP_V2)
         || r->stream != NULL
+#endif
+#if (NGX_HTTP_V3)
+        || r->connection->quic != NULL
 #endif
        )
     {
diff --git a/src/http/ngx_http_script.c b/src/http/ngx_http_script.c
index bebdbd9..a2b9f1b 100644
--- a/src/http/ngx_http_script.c
+++ b/src/http/ngx_http_script.c
@@ -1243,6 +1243,7 @@ ngx_http_script_regex_end_code(ngx_http_script_engine_t *e)
         }
 
         r->headers_out.location->hash = 1;
+        r->headers_out.location->next = NULL;
         ngx_str_set(&r->headers_out.location->key, "Location");
         r->headers_out.location->value = e->buf;
 
diff --git a/src/http/ngx_http_special_response.c b/src/http/ngx_http_special_response.c
index 72f56fd..eaf42e3 100644
--- a/src/http/ngx_http_special_response.c
+++ b/src/http/ngx_http_special_response.c
@@ -649,6 +649,7 @@ ngx_http_send_error_page(ngx_http_request_t *r, ngx_http_err_page_t *err_page)
     }
 
     location->hash = 1;
+    location->next = NULL;
     ngx_str_set(&location->key, "Location");
     location->value = uri;
 
diff --git a/src/http/ngx_http_upstream.c b/src/http/ngx_http_upstream.c
index ded833c..2ce9f21 100644
--- a/src/http/ngx_http_upstream.c
+++ b/src/http/ngx_http_upstream.c
@@ -101,6 +101,9 @@ static void ngx_http_upstream_finalize_request(ngx_http_request_t *r,
 
 static ngx_int_t ngx_http_upstream_process_header_line(ngx_http_request_t *r,
     ngx_table_elt_t *h, ngx_uint_t offset);
+static ngx_int_t
+    ngx_http_upstream_process_multi_header_lines(ngx_http_request_t *r,
+    ngx_table_elt_t *h, ngx_uint_t offset);
 static ngx_int_t ngx_http_upstream_process_content_length(ngx_http_request_t *r,
     ngx_table_elt_t *h, ngx_uint_t offset);
 static ngx_int_t ngx_http_upstream_process_last_modified(ngx_http_request_t *r,
@@ -147,11 +150,6 @@ static ngx_int_t ngx_http_upstream_rewrite_set_cookie(ngx_http_request_t *r,
 static ngx_int_t ngx_http_upstream_copy_allow_ranges(ngx_http_request_t *r,
     ngx_table_elt_t *h, ngx_uint_t offset);
 
-#if (NGX_HTTP_GZIP)
-static ngx_int_t ngx_http_upstream_copy_content_encoding(ngx_http_request_t *r,
-    ngx_table_elt_t *h, ngx_uint_t offset);
-#endif
-
 static ngx_int_t ngx_http_upstream_add_variables(ngx_conf_t *cf);
 static ngx_int_t ngx_http_upstream_addr_variable(ngx_http_request_t *r,
     ngx_http_variable_value_t *v, uintptr_t data);
@@ -231,7 +229,7 @@ static ngx_http_upstream_header_t  ngx_http_upstream_headers_in[] = {
                  offsetof(ngx_http_headers_out_t, server), 0 },
 
     { ngx_string("WWW-Authenticate"),
-                 ngx_http_upstream_process_header_line,
+                 ngx_http_upstream_process_multi_header_lines,
                  offsetof(ngx_http_upstream_headers_in_t, www_authenticate),
                  ngx_http_upstream_copy_header_line, 0, 0 },
 
@@ -241,12 +239,13 @@ static ngx_http_upstream_header_t  ngx_http_upstream_headers_in[] = {
                  ngx_http_upstream_rewrite_location, 0, 0 },
 
     { ngx_string("Refresh"),
-                 ngx_http_upstream_ignore_header_line, 0,
+                 ngx_http_upstream_process_header_line,
+                 offsetof(ngx_http_upstream_headers_in_t, refresh),
                  ngx_http_upstream_rewrite_refresh, 0, 0 },
 
     { ngx_string("Set-Cookie"),
                  ngx_http_upstream_process_set_cookie,
-                 offsetof(ngx_http_upstream_headers_in_t, cookies),
+                 offsetof(ngx_http_upstream_headers_in_t, set_cookie),
                  ngx_http_upstream_rewrite_set_cookie, 0, 1 },
 
     { ngx_string("Content-Disposition"),
@@ -264,8 +263,7 @@ static ngx_http_upstream_header_t  ngx_http_upstream_headers_in[] = {
                  offsetof(ngx_http_headers_out_t, expires), 1 },
 
     { ngx_string("Accept-Ranges"),
-                 ngx_http_upstream_process_header_line,
-                 offsetof(ngx_http_upstream_headers_in_t, accept_ranges),
+                 ngx_http_upstream_ignore_header_line, 0,
                  ngx_http_upstream_copy_allow_ranges,
                  offsetof(ngx_http_headers_out_t, accept_ranges), 1 },
 
@@ -316,12 +314,10 @@ static ngx_http_upstream_header_t  ngx_http_upstream_headers_in[] = {
                  ngx_http_upstream_process_transfer_encoding, 0,
                  ngx_http_upstream_ignore_header_line, 0, 0 },
 
-#if (NGX_HTTP_GZIP)
     { ngx_string("Content-Encoding"),
-                 ngx_http_upstream_process_header_line,
-                 offsetof(ngx_http_upstream_headers_in_t, content_encoding),
-                 ngx_http_upstream_copy_content_encoding, 0, 0 },
-#endif
+                 ngx_http_upstream_ignore_header_line, 0,
+                 ngx_http_upstream_copy_header_line,
+                 offsetof(ngx_http_headers_out_t, content_encoding), 0 },
 
     { ngx_null_string, NULL, 0, NULL, 0, 0 }
 };
@@ -525,6 +521,13 @@ ngx_http_upstream_init(ngx_http_request_t *r)
     }
 #endif
 
+#if (NGX_HTTP_V3)
+    if (c->quic) {
+        ngx_http_upstream_init_request(r);
+        return;
+    }
+#endif
+
     if (c->read->timer_set) {
         ngx_del_timer(c->read);
     }
@@ -1358,6 +1361,19 @@ ngx_http_upstream_check_broken_connection(ngx_http_request_t *r,
     }
 #endif
 
+#if (NGX_HTTP_V3)
+
+    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) {
@@ -1694,8 +1710,10 @@ ngx_http_upstream_ssl_init_connection(ngx_http_request_t *r,
         }
     }
 
-    if (u->conf->ssl_certificate && (u->conf->ssl_certificate->lengths
-                                     || u->conf->ssl_certificate_key->lengths))
+    if (u->conf->ssl_certificate
+        && u->conf->ssl_certificate->value.len
+        && (u->conf->ssl_certificate->lengths
+            || u->conf->ssl_certificate_key->lengths))
     {
         if (ngx_http_upstream_ssl_certificate(r, u, c) != NGX_OK) {
             ngx_http_upstream_finalize_request(r, u,
@@ -2651,7 +2669,7 @@ ngx_http_upstream_intercept_errors(ngx_http_request_t *r,
 {
     ngx_int_t                  status;
     ngx_uint_t                 i;
-    ngx_table_elt_t           *h;
+    ngx_table_elt_t           *h, *ho, **ph;
     ngx_http_err_page_t       *err_page;
     ngx_http_core_loc_conf_t  *clcf;
 
@@ -2680,23 +2698,36 @@ ngx_http_upstream_intercept_errors(ngx_http_request_t *r,
             if (status == NGX_HTTP_UNAUTHORIZED
                 && u->headers_in.www_authenticate)
             {
-                h = ngx_list_push(&r->headers_out.headers);
+                h = u->headers_in.www_authenticate;
+                ph = &r->headers_out.www_authenticate;
 
-                if (h == NULL) {
-                    ngx_http_upstream_finalize_request(r, u,
+                while (h) {
+                    ho = ngx_list_push(&r->headers_out.headers);
+
+                    if (ho == NULL) {
+                        ngx_http_upstream_finalize_request(r, u,
                                                NGX_HTTP_INTERNAL_SERVER_ERROR);
-                    return NGX_OK;
-                }
+                        return NGX_OK;
+                    }
+
+                    *ho = *h;
+                    ho->next = NULL;
 
-                *h = *u->headers_in.www_authenticate;
+                    *ph = ho;
+                    ph = &ho->next;
 
-                r->headers_out.www_authenticate = h;
+                    h = h->next;
+                }
             }
 
 #if (NGX_HTTP_CACHE)
 
             if (r->cache) {
 
+                if (u->headers_in.no_cache || u->headers_in.expired) {
+                    u->cacheable = 0;
+                }
+
                 if (u->cacheable) {
                     time_t  valid;
 
@@ -2791,6 +2822,10 @@ ngx_http_upstream_process_headers(ngx_http_request_t *r, ngx_http_upstream_t *u)
 
     umcf = ngx_http_get_module_main_conf(r, ngx_http_upstream_module);
 
+    if (u->headers_in.no_cache || u->headers_in.expired) {
+        u->cacheable = 0;
+    }
+
     if (u->headers_in.x_accel_redirect
         && !(u->conf->ignore_headers & NGX_HTTP_UPSTREAM_IGN_XA_REDIRECT))
     {
@@ -2811,6 +2846,10 @@ ngx_http_upstream_process_headers(ngx_http_request_t *r, ngx_http_upstream_t *u)
                 i = 0;
             }
 
+            if (h[i].hash == 0) {
+                continue;
+            }
+
             hh = ngx_hash_find(&umcf->headers_in_hash, h[i].hash,
                                h[i].lowcase_key, h[i].key.len);
 
@@ -2864,6 +2903,10 @@ ngx_http_upstream_process_headers(ngx_http_request_t *r, ngx_http_upstream_t *u)
             i = 0;
         }
 
+        if (h[i].hash == 0) {
+            continue;
+        }
+
         if (ngx_hash_find(&u->conf->hide_headers_hash, h[i].hash,
                           h[i].lowcase_key, h[i].key.len))
         {
@@ -3906,6 +3949,8 @@ ngx_http_upstream_thread_handler(ngx_thread_task_t *task, ngx_file_t *file)
     r->aio = 1;
     p->aio = 1;
 
+    ngx_add_timer(&task->event, 60000);
+
     return NGX_OK;
 }
 
@@ -3924,6 +3969,17 @@ ngx_http_upstream_thread_event_handler(ngx_event_t *ev)
     ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,
                    "http upstream thread: \"%V?%V\"", &r->uri, &r->args);
 
+    if (ev->timedout) {
+        ngx_log_error(NGX_LOG_ALERT, c->log, 0,
+                      "thread operation took too long");
+        ev->timedout = 0;
+        return;
+    }
+
+    if (ev->timer_set) {
+        ngx_del_timer(ev);
+    }
+
     r->main->blocked--;
     r->aio = 0;
 
@@ -3941,11 +3997,11 @@ ngx_http_upstream_thread_event_handler(ngx_event_t *ev)
 
 #endif
 
-    if (r->done) {
+    if (r->done || r->main->terminated) {
         /*
          * trigger connection event handler if the subrequest was
-         * already finalized; this can happen if the handler is used
-         * for sendfile() in threads
+         * already finalized (this can happen if the handler is used
+         * for sendfile() in threads), or if the request was terminated
          */
 
         c->write->handler(c->write);
@@ -4518,6 +4574,10 @@ ngx_http_upstream_finalize_request(ngx_http_request_t *r,
 
     u->peer.connection = NULL;
 
+    if (u->pipe) {
+        u->pipe->upstream = NULL;
+    }
+
     if (u->pipe && u->pipe->temp_file) {
         ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                        "http upstream temp fd: %d",
@@ -4615,10 +4675,36 @@ ngx_http_upstream_process_header_line(ngx_http_request_t *r, ngx_table_elt_t *h,
 
     ph = (ngx_table_elt_t **) ((char *) &r->upstream->headers_in + offset);
 
-    if (*ph == NULL) {
-        *ph = h;
+    if (*ph) {
+        ngx_log_error(NGX_LOG_WARN, r->connection->log, 0,
+                      "upstream sent duplicate header line: \"%V: %V\", "
+                      "previous value: \"%V: %V\", ignored",
+                      &h->key, &h->value,
+                      &(*ph)->key, &(*ph)->value);
+        h->hash = 0;
+        return NGX_OK;
     }
 
+    *ph = h;
+    h->next = NULL;
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_http_upstream_process_multi_header_lines(ngx_http_request_t *r,
+    ngx_table_elt_t *h, ngx_uint_t offset)
+{
+    ngx_table_elt_t  **ph;
+
+    ph = (ngx_table_elt_t **) ((char *) &r->upstream->headers_in + offset);
+
+    while (*ph) { ph = &(*ph)->next; }
+
+    *ph = h;
+    h->next = NULL;
+
     return NGX_OK;
 }
 
@@ -4639,9 +4725,34 @@ ngx_http_upstream_process_content_length(ngx_http_request_t *r,
 
     u = r->upstream;
 
+    if (u->headers_in.content_length) {
+        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
+                      "upstream sent duplicate header line: \"%V: %V\", "
+                      "previous value: \"%V: %V\"",
+                      &h->key, &h->value,
+                      &u->headers_in.content_length->key,
+                      &u->headers_in.content_length->value);
+        return NGX_HTTP_UPSTREAM_INVALID_HEADER;
+    }
+
+    if (u->headers_in.transfer_encoding) {
+        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
+                      "upstream sent \"Content-Length\" and "
+                      "\"Transfer-Encoding\" headers at the same time");
+        return NGX_HTTP_UPSTREAM_INVALID_HEADER;
+    }
+
+    h->next = NULL;
     u->headers_in.content_length = h;
     u->headers_in.content_length_n = ngx_atoof(h->value.data, h->value.len);
 
+    if (u->headers_in.content_length_n == NGX_ERROR) {
+        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
+                      "upstream sent invalid \"Content-Length\" header: "
+                      "\"%V: %V\"", &h->key, &h->value);
+        return NGX_HTTP_UPSTREAM_INVALID_HEADER;
+    }
+
     return NGX_OK;
 }
 
@@ -4654,6 +4765,18 @@ ngx_http_upstream_process_last_modified(ngx_http_request_t *r,
 
     u = r->upstream;
 
+    if (u->headers_in.last_modified) {
+        ngx_log_error(NGX_LOG_WARN, r->connection->log, 0,
+                      "upstream sent duplicate header line: \"%V: %V\", "
+                      "previous value: \"%V: %V\", ignored",
+                      &h->key, &h->value,
+                      &u->headers_in.last_modified->key,
+                      &u->headers_in.last_modified->value);
+        h->hash = 0;
+        return NGX_OK;
+    }
+
+    h->next = NULL;
     u->headers_in.last_modified = h;
     u->headers_in.last_modified_time = ngx_parse_http_time(h->value.data,
                                                            h->value.len);
@@ -4666,26 +4789,16 @@ static ngx_int_t
 ngx_http_upstream_process_set_cookie(ngx_http_request_t *r, ngx_table_elt_t *h,
     ngx_uint_t offset)
 {
-    ngx_array_t           *pa;
     ngx_table_elt_t      **ph;
     ngx_http_upstream_t   *u;
 
     u = r->upstream;
-    pa = &u->headers_in.cookies;
-
-    if (pa->elts == NULL) {
-        if (ngx_array_init(pa, r->pool, 1, sizeof(ngx_table_elt_t *)) != NGX_OK)
-        {
-            return NGX_ERROR;
-        }
-    }
+    ph = &u->headers_in.set_cookie;
 
-    ph = ngx_array_push(pa);
-    if (ph == NULL) {
-        return NGX_ERROR;
-    }
+    while (*ph) { ph = &(*ph)->next; }
 
     *ph = h;
+    h->next = NULL;
 
 #if (NGX_HTTP_CACHE)
     if (!(u->conf->ignore_headers & NGX_HTTP_UPSTREAM_IGN_SET_COOKIE)) {
@@ -4701,26 +4814,16 @@ static ngx_int_t
 ngx_http_upstream_process_cache_control(ngx_http_request_t *r,
     ngx_table_elt_t *h, ngx_uint_t offset)
 {
-    ngx_array_t          *pa;
-    ngx_table_elt_t     **ph;
-    ngx_http_upstream_t  *u;
+    ngx_table_elt_t      **ph;
+    ngx_http_upstream_t   *u;
 
     u = r->upstream;
-    pa = &u->headers_in.cache_control;
+    ph = &u->headers_in.cache_control;
 
-    if (pa->elts == NULL) {
-        if (ngx_array_init(pa, r->pool, 2, sizeof(ngx_table_elt_t *)) != NGX_OK)
-        {
-            return NGX_ERROR;
-        }
-    }
-
-    ph = ngx_array_push(pa);
-    if (ph == NULL) {
-        return NGX_ERROR;
-    }
+    while (*ph) { ph = &(*ph)->next; }
 
     *ph = h;
+    h->next = NULL;
 
 #if (NGX_HTTP_CACHE)
     {
@@ -4735,18 +4838,18 @@ ngx_http_upstream_process_cache_control(ngx_http_request_t *r,
         return NGX_OK;
     }
 
-    if (r->cache->valid_sec != 0 && u->headers_in.x_accel_expires != NULL) {
-        return NGX_OK;
-    }
-
     start = h->value.data;
     last = start + h->value.len;
 
+    if (r->cache->valid_sec != 0 && u->headers_in.x_accel_expires != NULL) {
+        goto extensions;
+    }
+
     if (ngx_strlcasestrn(start, last, (u_char *) "no-cache", 8 - 1) != NULL
         || ngx_strlcasestrn(start, last, (u_char *) "no-store", 8 - 1) != NULL
         || ngx_strlcasestrn(start, last, (u_char *) "private", 7 - 1) != NULL)
     {
-        u->cacheable = 0;
+        u->headers_in.no_cache = 1;
         return NGX_OK;
     }
 
@@ -4776,13 +4879,16 @@ ngx_http_upstream_process_cache_control(ngx_http_request_t *r,
         }
 
         if (n == 0) {
-            u->cacheable = 0;
+            u->headers_in.no_cache = 1;
             return NGX_OK;
         }
 
         r->cache->valid_sec = ngx_time() + n;
+        u->headers_in.expired = 0;
     }
 
+extensions:
+
     p = ngx_strlcasestrn(start, last, (u_char *) "stale-while-revalidate=",
                          23 - 1);
 
@@ -4842,7 +4948,20 @@ ngx_http_upstream_process_expires(ngx_http_request_t *r, ngx_table_elt_t *h,
     ngx_http_upstream_t  *u;
 
     u = r->upstream;
+
+    if (u->headers_in.expires) {
+        ngx_log_error(NGX_LOG_WARN, r->connection->log, 0,
+                      "upstream sent duplicate header line: \"%V: %V\", "
+                      "previous value: \"%V: %V\", ignored",
+                      &h->key, &h->value,
+                      &u->headers_in.expires->key,
+                      &u->headers_in.expires->value);
+        h->hash = 0;
+        return NGX_OK;
+    }
+
     u->headers_in.expires = h;
+    h->next = NULL;
 
 #if (NGX_HTTP_CACHE)
     {
@@ -4863,7 +4982,7 @@ ngx_http_upstream_process_expires(ngx_http_request_t *r, ngx_table_elt_t *h,
     expires = ngx_parse_http_time(h->value.data, h->value.len);
 
     if (expires == NGX_ERROR || expires < ngx_time()) {
-        u->cacheable = 0;
+        u->headers_in.expired = 1;
         return NGX_OK;
     }
 
@@ -4882,7 +5001,20 @@ ngx_http_upstream_process_accel_expires(ngx_http_request_t *r,
     ngx_http_upstream_t  *u;
 
     u = r->upstream;
+
+    if (u->headers_in.x_accel_expires) {
+        ngx_log_error(NGX_LOG_WARN, r->connection->log, 0,
+                      "upstream sent duplicate header line: \"%V: %V\", "
+                      "previous value: \"%V: %V\", ignored",
+                      &h->key, &h->value,
+                      &u->headers_in.x_accel_expires->key,
+                      &u->headers_in.x_accel_expires->value);
+        h->hash = 0;
+        return NGX_OK;
+    }
+
     u->headers_in.x_accel_expires = h;
+    h->next = NULL;
 
 #if (NGX_HTTP_CACHE)
     {
@@ -4914,6 +5046,8 @@ ngx_http_upstream_process_accel_expires(ngx_http_request_t *r,
 
         default:
             r->cache->valid_sec = ngx_time() + n;
+            u->headers_in.no_cache = 0;
+            u->headers_in.expired = 0;
             return NGX_OK;
         }
     }
@@ -4925,6 +5059,8 @@ ngx_http_upstream_process_accel_expires(ngx_http_request_t *r,
 
     if (n != NGX_ERROR) {
         r->cache->valid_sec = n;
+        u->headers_in.no_cache = 0;
+        u->headers_in.expired = 0;
     }
     }
 #endif
@@ -4941,7 +5077,20 @@ ngx_http_upstream_process_limit_rate(ngx_http_request_t *r, ngx_table_elt_t *h,
     ngx_http_upstream_t  *u;
 
     u = r->upstream;
+
+    if (u->headers_in.x_accel_limit_rate) {
+        ngx_log_error(NGX_LOG_WARN, r->connection->log, 0,
+                      "upstream sent duplicate header line: \"%V: %V\", "
+                      "previous value: \"%V: %V\", ignored",
+                      &h->key, &h->value,
+                      &u->headers_in.x_accel_limit_rate->key,
+                      &u->headers_in.x_accel_limit_rate->value);
+        h->hash = 0;
+        return NGX_OK;
+    }
+
     u->headers_in.x_accel_limit_rate = h;
+    h->next = NULL;
 
     if (u->conf->ignore_headers & NGX_HTTP_UPSTREAM_IGN_XA_LIMIT_RATE) {
         return NGX_OK;
@@ -5000,7 +5149,11 @@ static ngx_int_t
 ngx_http_upstream_process_charset(ngx_http_request_t *r, ngx_table_elt_t *h,
     ngx_uint_t offset)
 {
-    if (r->upstream->conf->ignore_headers & NGX_HTTP_UPSTREAM_IGN_XA_CHARSET) {
+    ngx_http_upstream_t  *u;
+
+    u = r->upstream;
+
+    if (u->conf->ignore_headers & NGX_HTTP_UPSTREAM_IGN_XA_CHARSET) {
         return NGX_OK;
     }
 
@@ -5014,13 +5167,22 @@ static ngx_int_t
 ngx_http_upstream_process_connection(ngx_http_request_t *r, ngx_table_elt_t *h,
     ngx_uint_t offset)
 {
-    r->upstream->headers_in.connection = h;
+    ngx_table_elt_t      **ph;
+    ngx_http_upstream_t   *u;
+
+    u = r->upstream;
+    ph = &u->headers_in.connection;
+
+    while (*ph) { ph = &(*ph)->next; }
+
+    *ph = h;
+    h->next = NULL;
 
     if (ngx_strlcasestrn(h->value.data, h->value.data + h->value.len,
                          (u_char *) "close", 5 - 1)
         != NULL)
     {
-        r->upstream->headers_in.connection_close = 1;
+        u->headers_in.connection_close = 1;
     }
 
     return NGX_OK;
@@ -5031,13 +5193,40 @@ static ngx_int_t
 ngx_http_upstream_process_transfer_encoding(ngx_http_request_t *r,
     ngx_table_elt_t *h, ngx_uint_t offset)
 {
-    r->upstream->headers_in.transfer_encoding = h;
+    ngx_http_upstream_t  *u;
 
-    if (ngx_strlcasestrn(h->value.data, h->value.data + h->value.len,
-                         (u_char *) "chunked", 7 - 1)
-        != NULL)
+    u = r->upstream;
+
+    if (u->headers_in.transfer_encoding) {
+        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
+                      "upstream sent duplicate header line: \"%V: %V\", "
+                      "previous value: \"%V: %V\"",
+                      &h->key, &h->value,
+                      &u->headers_in.transfer_encoding->key,
+                      &u->headers_in.transfer_encoding->value);
+        return NGX_HTTP_UPSTREAM_INVALID_HEADER;
+    }
+
+    if (u->headers_in.content_length) {
+        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
+                      "upstream sent \"Content-Length\" and "
+                      "\"Transfer-Encoding\" headers at the same time");
+        return NGX_HTTP_UPSTREAM_INVALID_HEADER;
+    }
+
+    u->headers_in.transfer_encoding = h;
+    h->next = NULL;
+
+    if (h->value.len == 7
+        && ngx_strncasecmp(h->value.data, (u_char *) "chunked", 7) == 0)
     {
-        r->upstream->headers_in.chunked = 1;
+        u->headers_in.chunked = 1;
+
+    } else {
+        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
+                      "upstream sent unknown \"Transfer-Encoding\": \"%V\"",
+                      &h->value);
+        return NGX_HTTP_UPSTREAM_INVALID_HEADER;
     }
 
     return NGX_OK;
@@ -5048,29 +5237,74 @@ static ngx_int_t
 ngx_http_upstream_process_vary(ngx_http_request_t *r,
     ngx_table_elt_t *h, ngx_uint_t offset)
 {
-    ngx_http_upstream_t  *u;
+    ngx_table_elt_t      **ph;
+    ngx_http_upstream_t   *u;
 
     u = r->upstream;
-    u->headers_in.vary = h;
+    ph = &u->headers_in.vary;
+
+    while (*ph) { ph = &(*ph)->next; }
+
+    *ph = h;
+    h->next = NULL;
 
 #if (NGX_HTTP_CACHE)
+    {
+    u_char     *p;
+    size_t      len;
+    ngx_str_t   vary;
 
     if (u->conf->ignore_headers & NGX_HTTP_UPSTREAM_IGN_VARY) {
         return NGX_OK;
     }
 
-    if (r->cache == NULL) {
+    if (r->cache == NULL || !u->cacheable) {
         return NGX_OK;
     }
 
-    if (h->value.len > NGX_HTTP_CACHE_VARY_LEN
-        || (h->value.len == 1 && h->value.data[0] == '*'))
-    {
+    if (h->value.len == 1 && h->value.data[0] == '*') {
         u->cacheable = 0;
+        return NGX_OK;
     }
 
-    r->cache->vary = h->value;
+    if (u->headers_in.vary->next) {
+
+        len = 0;
+
+        for (h = u->headers_in.vary; h; h = h->next) {
+            len += h->value.len + 2;
+        }
+
+        len -= 2;
 
+        p = ngx_pnalloc(r->pool, len);
+        if (p == NULL) {
+            return NGX_ERROR;
+        }
+
+        vary.len = len;
+        vary.data = p;
+
+        for (h = u->headers_in.vary; h; h = h->next) {
+            p = ngx_copy(p, h->value.data, h->value.len);
+
+            if (h->next == NULL) {
+                break;
+            }
+
+            *p++ = ','; *p++ = ' ';
+        }
+
+    } else {
+        vary = h->value;
+    }
+
+    if (vary.len > NGX_HTTP_CACHE_VARY_LEN) {
+        u->cacheable = 0;
+    }
+
+    r->cache->vary = vary;
+    }
 #endif
 
     return NGX_OK;
@@ -5093,6 +5327,7 @@ ngx_http_upstream_copy_header_line(ngx_http_request_t *r, ngx_table_elt_t *h,
     if (offset) {
         ph = (ngx_table_elt_t **) ((char *) &r->headers_out + offset);
         *ph = ho;
+        ho->next = NULL;
     }
 
     return NGX_OK;
@@ -5103,18 +5338,8 @@ static ngx_int_t
 ngx_http_upstream_copy_multi_header_lines(ngx_http_request_t *r,
     ngx_table_elt_t *h, ngx_uint_t offset)
 {
-    ngx_array_t      *pa;
     ngx_table_elt_t  *ho, **ph;
 
-    pa = (ngx_array_t *) ((char *) &r->headers_out + offset);
-
-    if (pa->elts == NULL) {
-        if (ngx_array_init(pa, r->pool, 2, sizeof(ngx_table_elt_t *)) != NGX_OK)
-        {
-            return NGX_ERROR;
-        }
-    }
-
     ho = ngx_list_push(&r->headers_out.headers);
     if (ho == NULL) {
         return NGX_ERROR;
@@ -5122,12 +5347,12 @@ ngx_http_upstream_copy_multi_header_lines(ngx_http_request_t *r,
 
     *ho = *h;
 
-    ph = ngx_array_push(pa);
-    if (ph == NULL) {
-        return NGX_ERROR;
-    }
+    ph = (ngx_table_elt_t **) ((char *) &r->headers_out + offset);
+
+    while (*ph) { ph = &(*ph)->next; }
 
     *ph = ho;
+    ho->next = NULL;
 
     return NGX_OK;
 }
@@ -5197,6 +5422,7 @@ ngx_http_upstream_copy_last_modified(ngx_http_request_t *r, ngx_table_elt_t *h,
     }
 
     *ho = *h;
+    ho->next = NULL;
 
     r->headers_out.last_modified = ho;
     r->headers_out.last_modified_time =
@@ -5219,6 +5445,7 @@ ngx_http_upstream_rewrite_location(ngx_http_request_t *r, ngx_table_elt_t *h,
     }
 
     *ho = *h;
+    ho->next = NULL;
 
     if (r->upstream->rewrite_redirect) {
         rc = r->upstream->rewrite_redirect(r, ho, 0);
@@ -5264,6 +5491,7 @@ ngx_http_upstream_rewrite_refresh(ngx_http_request_t *r, ngx_table_elt_t *h,
     }
 
     *ho = *h;
+    ho->next = NULL;
 
     if (r->upstream->rewrite_redirect) {
 
@@ -5309,6 +5537,7 @@ ngx_http_upstream_rewrite_set_cookie(ngx_http_request_t *r, ngx_table_elt_t *h,
     }
 
     *ho = *h;
+    ho->next = NULL;
 
     if (r->upstream->rewrite_cookie) {
         rc = r->upstream->rewrite_cookie(r, ho);
@@ -5362,6 +5591,7 @@ ngx_http_upstream_copy_allow_ranges(ngx_http_request_t *r,
     }
 
     *ho = *h;
+    ho->next = NULL;
 
     r->headers_out.accept_ranges = ho;
 
@@ -5369,29 +5599,6 @@ ngx_http_upstream_copy_allow_ranges(ngx_http_request_t *r,
 }
 
 
-#if (NGX_HTTP_GZIP)
-
-static ngx_int_t
-ngx_http_upstream_copy_content_encoding(ngx_http_request_t *r,
-    ngx_table_elt_t *h, ngx_uint_t offset)
-{
-    ngx_table_elt_t  *ho;
-
-    ho = ngx_list_push(&r->headers_out.headers);
-    if (ho == NULL) {
-        return NGX_ERROR;
-    }
-
-    *ho = *h;
-
-    r->headers_out.content_encoding = ho;
-
-    return NGX_OK;
-}
-
-#endif
-
-
 static ngx_int_t
 ngx_http_upstream_add_variables(ngx_conf_t *cf)
 {
@@ -5703,7 +5910,7 @@ ngx_http_upstream_header_variable(ngx_http_request_t *r,
         return NGX_OK;
     }
 
-    return ngx_http_variable_unknown_header(v, (ngx_str_t *) data,
+    return ngx_http_variable_unknown_header(r, v, (ngx_str_t *) data,
                                          &r->upstream->headers_in.headers.part,
                                          sizeof("upstream_http_") - 1);
 }
@@ -5718,7 +5925,7 @@ ngx_http_upstream_trailer_variable(ngx_http_request_t *r,
         return NGX_OK;
     }
 
-    return ngx_http_variable_unknown_header(v, (ngx_str_t *) data,
+    return ngx_http_variable_unknown_header(r, v, (ngx_str_t *) data,
                                         &r->upstream->headers_in.trailers.part,
                                         sizeof("upstream_trailer_") - 1);
 }
@@ -5740,9 +5947,9 @@ ngx_http_upstream_cookie_variable(ngx_http_request_t *r,
     s.len = name->len - (sizeof("upstream_cookie_") - 1);
     s.data = name->data + sizeof("upstream_cookie_") - 1;
 
-    if (ngx_http_parse_set_cookie_lines(&r->upstream->headers_in.cookies,
+    if (ngx_http_parse_set_cookie_lines(r, r->upstream->headers_in.set_cookie,
                                         &s, &cookie)
-        == NGX_DECLINED)
+        == NULL)
     {
         v->not_found = 1;
         return NGX_OK;
diff --git a/src/http/ngx_http_upstream.h b/src/http/ngx_http_upstream.h
index 3db7b06..9a17a03 100644
--- a/src/http/ngx_http_upstream.h
+++ b/src/http/ngx_http_upstream.h
@@ -280,23 +280,21 @@ typedef struct {
 
     ngx_table_elt_t                 *last_modified;
     ngx_table_elt_t                 *location;
-    ngx_table_elt_t                 *accept_ranges;
+    ngx_table_elt_t                 *refresh;
     ngx_table_elt_t                 *www_authenticate;
     ngx_table_elt_t                 *transfer_encoding;
     ngx_table_elt_t                 *vary;
 
-#if (NGX_HTTP_GZIP)
-    ngx_table_elt_t                 *content_encoding;
-#endif
-
-    ngx_array_t                      cache_control;
-    ngx_array_t                      cookies;
+    ngx_table_elt_t                 *cache_control;
+    ngx_table_elt_t                 *set_cookie;
 
     off_t                            content_length_n;
     time_t                           last_modified_time;
 
     unsigned                         connection_close:1;
     unsigned                         chunked:1;
+    unsigned                         no_cache:1;
+    unsigned                         expired:1;
 } ngx_http_upstream_headers_in_t;
 
 
diff --git a/src/http/ngx_http_variables.c b/src/http/ngx_http_variables.c
index 942dacd..4f0bd0e 100644
--- a/src/http/ngx_http_variables.c
+++ b/src/http/ngx_http_variables.c
@@ -27,8 +27,6 @@ static ngx_int_t ngx_http_variable_header(ngx_http_request_t *r,
 
 static ngx_int_t ngx_http_variable_cookies(ngx_http_request_t *r,
     ngx_http_variable_value_t *v, uintptr_t data);
-static ngx_int_t ngx_http_variable_headers(ngx_http_request_t *r,
-    ngx_http_variable_value_t *v, uintptr_t data);
 static ngx_int_t ngx_http_variable_headers_internal(ngx_http_request_t *r,
     ngx_http_variable_value_t *v, uintptr_t data, u_char sep);
 
@@ -63,6 +61,8 @@ static ngx_int_t ngx_http_variable_proxy_protocol_addr(ngx_http_request_t *r,
     ngx_http_variable_value_t *v, uintptr_t data);
 static ngx_int_t ngx_http_variable_proxy_protocol_port(ngx_http_request_t *r,
     ngx_http_variable_value_t *v, uintptr_t data);
+static ngx_int_t ngx_http_variable_proxy_protocol_tlv(ngx_http_request_t *r,
+    ngx_http_variable_value_t *v, uintptr_t data);
 static ngx_int_t ngx_http_variable_server_addr(ngx_http_request_t *r,
     ngx_http_variable_value_t *v, uintptr_t data);
 static ngx_int_t ngx_http_variable_server_port(ngx_http_request_t *r,
@@ -178,12 +178,12 @@ static ngx_http_variable_t  ngx_http_core_variables[] = {
 #endif
 
 #if (NGX_HTTP_X_FORWARDED_FOR)
-    { ngx_string("http_x_forwarded_for"), NULL, ngx_http_variable_headers,
+    { ngx_string("http_x_forwarded_for"), NULL, ngx_http_variable_header,
       offsetof(ngx_http_request_t, headers_in.x_forwarded_for), 0, 0 },
 #endif
 
     { ngx_string("http_cookie"), NULL, ngx_http_variable_cookies,
-      offsetof(ngx_http_request_t, headers_in.cookies), 0, 0 },
+      offsetof(ngx_http_request_t, headers_in.cookie), 0, 0 },
 
     { ngx_string("content_length"), NULL, ngx_http_variable_content_length,
       0, 0, 0 },
@@ -216,6 +216,10 @@ static ngx_http_variable_t  ngx_http_core_variables[] = {
       ngx_http_variable_proxy_protocol_port,
       offsetof(ngx_proxy_protocol_t, dst_port), 0, 0 },
 
+    { ngx_string("proxy_protocol_tlv_"), NULL,
+      ngx_http_variable_proxy_protocol_tlv,
+      0, NGX_HTTP_VAR_PREFIX, 0 },
+
     { ngx_string("server_addr"), NULL, ngx_http_variable_server_addr, 0, 0, 0 },
 
     { ngx_string("server_port"), NULL, ngx_http_variable_server_port, 0, 0, 0 },
@@ -327,10 +331,10 @@ static ngx_http_variable_t  ngx_http_core_variables[] = {
     { ngx_string("sent_http_transfer_encoding"), NULL,
       ngx_http_variable_sent_transfer_encoding, 0, 0, 0 },
 
-    { ngx_string("sent_http_cache_control"), NULL, ngx_http_variable_headers,
+    { ngx_string("sent_http_cache_control"), NULL, ngx_http_variable_header,
       offsetof(ngx_http_request_t, headers_out.cache_control), 0, 0 },
 
-    { ngx_string("sent_http_link"), NULL, ngx_http_variable_headers,
+    { ngx_string("sent_http_link"), NULL, ngx_http_variable_header,
       offsetof(ngx_http_request_t, headers_out.link), 0, 0 },
 
     { ngx_string("limit_rate"), ngx_http_variable_set_limit_rate,
@@ -807,22 +811,7 @@ static ngx_int_t
 ngx_http_variable_header(ngx_http_request_t *r, ngx_http_variable_value_t *v,
     uintptr_t data)
 {
-    ngx_table_elt_t  *h;
-
-    h = *(ngx_table_elt_t **) ((char *) r + data);
-
-    if (h) {
-        v->len = h->value.len;
-        v->valid = 1;
-        v->no_cacheable = 0;
-        v->not_found = 0;
-        v->data = h->value.data;
-
-    } else {
-        v->not_found = 1;
-    }
-
-    return NGX_OK;
+    return ngx_http_variable_headers_internal(r, v, data, ',');
 }
 
 
@@ -834,38 +823,25 @@ ngx_http_variable_cookies(ngx_http_request_t *r,
 }
 
 
-static ngx_int_t
-ngx_http_variable_headers(ngx_http_request_t *r,
-    ngx_http_variable_value_t *v, uintptr_t data)
-{
-    return ngx_http_variable_headers_internal(r, v, data, ',');
-}
-
-
 static ngx_int_t
 ngx_http_variable_headers_internal(ngx_http_request_t *r,
     ngx_http_variable_value_t *v, uintptr_t data, u_char sep)
 {
-    size_t             len;
-    u_char            *p, *end;
-    ngx_uint_t         i, n;
-    ngx_array_t       *a;
-    ngx_table_elt_t  **h;
-
-    a = (ngx_array_t *) ((char *) r + data);
+    size_t            len;
+    u_char           *p, *end;
+    ngx_table_elt_t  *h, *th;
 
-    n = a->nelts;
-    h = a->elts;
+    h = *(ngx_table_elt_t **) ((char *) r + data);
 
     len = 0;
 
-    for (i = 0; i < n; i++) {
+    for (th = h; th; th = th->next) {
 
-        if (h[i]->hash == 0) {
+        if (th->hash == 0) {
             continue;
         }
 
-        len += h[i]->value.len + 2;
+        len += th->value.len + 2;
     }
 
     if (len == 0) {
@@ -879,9 +855,9 @@ ngx_http_variable_headers_internal(ngx_http_request_t *r,
     v->no_cacheable = 0;
     v->not_found = 0;
 
-    if (n == 1) {
-        v->len = (*h)->value.len;
-        v->data = (*h)->value.data;
+    if (h->next == NULL) {
+        v->len = h->value.len;
+        v->data = h->value.data;
 
         return NGX_OK;
     }
@@ -896,13 +872,13 @@ ngx_http_variable_headers_internal(ngx_http_request_t *r,
 
     end = p + len;
 
-    for (i = 0; /* void */ ; i++) {
+    for (th = h; th; th = th->next) {
 
-        if (h[i]->hash == 0) {
+        if (th->hash == 0) {
             continue;
         }
 
-        p = ngx_copy(p, h[i]->value.data, h[i]->value.len);
+        p = ngx_copy(p, th->value.data, th->value.len);
 
         if (p == end) {
             break;
@@ -919,7 +895,7 @@ static ngx_int_t
 ngx_http_variable_unknown_header_in(ngx_http_request_t *r,
     ngx_http_variable_value_t *v, uintptr_t data)
 {
-    return ngx_http_variable_unknown_header(v, (ngx_str_t *) data,
+    return ngx_http_variable_unknown_header(r, v, (ngx_str_t *) data,
                                             &r->headers_in.headers.part,
                                             sizeof("http_") - 1);
 }
@@ -929,7 +905,7 @@ static ngx_int_t
 ngx_http_variable_unknown_header_out(ngx_http_request_t *r,
     ngx_http_variable_value_t *v, uintptr_t data)
 {
-    return ngx_http_variable_unknown_header(v, (ngx_str_t *) data,
+    return ngx_http_variable_unknown_header(r, v, (ngx_str_t *) data,
                                             &r->headers_out.headers.part,
                                             sizeof("sent_http_") - 1);
 }
@@ -939,19 +915,26 @@ static ngx_int_t
 ngx_http_variable_unknown_trailer_out(ngx_http_request_t *r,
     ngx_http_variable_value_t *v, uintptr_t data)
 {
-    return ngx_http_variable_unknown_header(v, (ngx_str_t *) data,
+    return ngx_http_variable_unknown_header(r, v, (ngx_str_t *) data,
                                             &r->headers_out.trailers.part,
                                             sizeof("sent_trailer_") - 1);
 }
 
 
 ngx_int_t
-ngx_http_variable_unknown_header(ngx_http_variable_value_t *v, ngx_str_t *var,
+ngx_http_variable_unknown_header(ngx_http_request_t *r,
+    ngx_http_variable_value_t *v, ngx_str_t *var,
     ngx_list_part_t *part, size_t prefix)
 {
-    u_char            ch;
+    u_char           *p, ch;
+    size_t            len;
     ngx_uint_t        i, n;
-    ngx_table_elt_t  *header;
+    ngx_table_elt_t  *header, *h, **ph;
+
+    ph = &h;
+#if (NGX_SUPPRESS_WARN)
+    len = 0;
+#endif
 
     header = part->elts;
 
@@ -971,7 +954,11 @@ ngx_http_variable_unknown_header(ngx_http_variable_value_t *v, ngx_str_t *var,
             continue;
         }
 
-        for (n = 0; n + prefix < var->len && n < header[i].key.len; n++) {
+        if (header[i].key.len != var->len - prefix) {
+            continue;
+        }
+
+        for (n = 0; n < var->len - prefix; n++) {
             ch = header[i].key.data[n];
 
             if (ch >= 'A' && ch <= 'Z') {
@@ -986,18 +973,59 @@ ngx_http_variable_unknown_header(ngx_http_variable_value_t *v, ngx_str_t *var,
             }
         }
 
-        if (n + prefix == var->len && n == header[i].key.len) {
-            v->len = header[i].value.len;
-            v->valid = 1;
-            v->no_cacheable = 0;
-            v->not_found = 0;
-            v->data = header[i].value.data;
-
-            return NGX_OK;
+        if (n != var->len - prefix) {
+            continue;
         }
+
+        len += header[i].value.len + 2;
+
+        *ph = &header[i];
+        ph = &header[i].next;
     }
 
-    v->not_found = 1;
+    *ph = NULL;
+
+    if (h == NULL) {
+        v->not_found = 1;
+        return NGX_OK;
+    }
+
+    len -= 2;
+
+    if (h->next == NULL) {
+
+        v->len = h->value.len;
+        v->valid = 1;
+        v->no_cacheable = 0;
+        v->not_found = 0;
+        v->data = h->value.data;
+
+        return NGX_OK;
+    }
+
+    p = ngx_pnalloc(r->pool, len);
+    if (p == NULL) {
+        return NGX_ERROR;
+    }
+
+    v->len = len;
+    v->valid = 1;
+    v->no_cacheable = 0;
+    v->not_found = 0;
+    v->data = p;
+
+    for ( ;; ) {
+
+        p = ngx_copy(p, h->value.data, h->value.len);
+
+        if (h->next == NULL) {
+            break;
+        }
+
+        *p++ = ','; *p++ = ' ';
+
+        h = h->next;
+    }
 
     return NGX_OK;
 }
@@ -1050,8 +1078,8 @@ ngx_http_variable_cookie(ngx_http_request_t *r, ngx_http_variable_value_t *v,
     s.len = name->len - (sizeof("cookie_") - 1);
     s.data = name->data + sizeof("cookie_") - 1;
 
-    if (ngx_http_parse_multi_header_lines(&r->headers_in.cookies, &s, &cookie)
-        == NGX_DECLINED)
+    if (ngx_http_parse_multi_header_lines(r, r->headers_in.cookie, &s, &cookie)
+        == NULL)
     {
         v->not_found = 1;
         return NGX_OK;
@@ -1366,6 +1394,39 @@ ngx_http_variable_proxy_protocol_port(ngx_http_request_t *r,
 }
 
 
+static ngx_int_t
+ngx_http_variable_proxy_protocol_tlv(ngx_http_request_t *r,
+    ngx_http_variable_value_t *v, uintptr_t data)
+{
+    ngx_str_t *name = (ngx_str_t *) data;
+
+    ngx_int_t  rc;
+    ngx_str_t  tlv, value;
+
+    tlv.len = name->len - (sizeof("proxy_protocol_tlv_") - 1);
+    tlv.data = name->data + sizeof("proxy_protocol_tlv_") - 1;
+
+    rc = ngx_proxy_protocol_get_tlv(r->connection, &tlv, &value);
+
+    if (rc == NGX_ERROR) {
+        return NGX_ERROR;
+    }
+
+    if (rc == NGX_DECLINED) {
+        v->not_found = 1;
+        return NGX_OK;
+    }
+
+    v->len = value.len;
+    v->valid = 1;
+    v->no_cacheable = 0;
+    v->not_found = 0;
+    v->data = value.data;
+
+    return NGX_OK;
+}
+
+
 static ngx_int_t
 ngx_http_variable_server_addr(ngx_http_request_t *r,
     ngx_http_variable_value_t *v, uintptr_t data)
@@ -1879,7 +1940,7 @@ ngx_http_variable_sent_location(ngx_http_request_t *r,
 
     ngx_str_set(&name, "sent_http_location");
 
-    return ngx_http_variable_unknown_header(v, &name,
+    return ngx_http_variable_unknown_header(r, v, &name,
                                             &r->headers_out.headers.part,
                                             sizeof("sent_http_") - 1);
 }
diff --git a/src/http/ngx_http_variables.h b/src/http/ngx_http_variables.h
index f3f7f3c..6a661a2 100644
--- a/src/http/ngx_http_variables.h
+++ b/src/http/ngx_http_variables.h
@@ -57,8 +57,9 @@ ngx_http_variable_value_t *ngx_http_get_flushed_variable(ngx_http_request_t *r,
 ngx_http_variable_value_t *ngx_http_get_variable(ngx_http_request_t *r,
     ngx_str_t *name, ngx_uint_t key);
 
-ngx_int_t ngx_http_variable_unknown_header(ngx_http_variable_value_t *v,
-    ngx_str_t *var, ngx_list_part_t *part, size_t prefix);
+ngx_int_t ngx_http_variable_unknown_header(ngx_http_request_t *r,
+    ngx_http_variable_value_t *v, ngx_str_t *var, ngx_list_part_t *part,
+    size_t prefix);
 
 
 #if (NGX_PCRE)
diff --git a/src/http/ngx_http_write_filter_module.c b/src/http/ngx_http_write_filter_module.c
index 932f26d..9188ee9 100644
--- a/src/http/ngx_http_write_filter_module.c
+++ b/src/http/ngx_http_write_filter_module.c
@@ -227,7 +227,8 @@ ngx_http_write_filter(ngx_http_request_t *r, ngx_chain_t *in)
 
     if (size == 0
         && !(c->buffered & NGX_LOWLEVEL_BUFFERED)
-        && !(last && c->need_last_buf))
+        && !(last && c->need_last_buf)
+        && !(flush && c->need_flush_buf))
     {
         if (last || flush || sync) {
             for (cl = r->out; cl; /* void */) {
@@ -239,6 +240,10 @@ ngx_http_write_filter(ngx_http_request_t *r, ngx_chain_t *in)
             r->out = NULL;
             c->buffered &= ~NGX_HTTP_WRITE_BUFFERED;
 
+            if (last) {
+                r->response_sent = 1;
+            }
+
             return NGX_OK;
         }
 
@@ -345,6 +350,10 @@ ngx_http_write_filter(ngx_http_request_t *r, ngx_chain_t *in)
 
     c->buffered &= ~NGX_HTTP_WRITE_BUFFERED;
 
+    if (last) {
+        r->response_sent = 1;
+    }
+
     if ((c->buffered & NGX_LOWLEVEL_BUFFERED) && r->postponed == NULL) {
         return NGX_AGAIN;
     }
diff --git a/src/http/v2/ngx_http_v2.c b/src/http/v2/ngx_http_v2.c
index 0e45a7b..0f5bd3d 100644
--- a/src/http/v2/ngx_http_v2.c
+++ b/src/http/v2/ngx_http_v2.c
@@ -11,14 +11,6 @@
 #include <ngx_http_v2_module.h>
 
 
-typedef struct {
-    ngx_str_t           name;
-    ngx_uint_t          offset;
-    ngx_uint_t          hash;
-    ngx_http_header_t  *hh;
-} ngx_http_v2_parse_header_t;
-
-
 /* errors */
 #define NGX_HTTP_V2_NO_ERROR                     0x0
 #define NGX_HTTP_V2_PROTOCOL_ERROR               0x1
@@ -63,8 +55,6 @@ static void ngx_http_v2_handle_connection(ngx_http_v2_connection_t *h2c);
 static void ngx_http_v2_lingering_close(ngx_connection_t *c);
 static void ngx_http_v2_lingering_close_handler(ngx_event_t *rev);
 
-static u_char *ngx_http_v2_state_proxy_protocol(ngx_http_v2_connection_t *h2c,
-    u_char *pos, u_char *end);
 static u_char *ngx_http_v2_state_preface(ngx_http_v2_connection_t *h2c,
     u_char *pos, u_char *end);
 static u_char *ngx_http_v2_state_preface_end(ngx_http_v2_connection_t *h2c,
@@ -128,7 +118,7 @@ static ngx_int_t ngx_http_v2_parse_int(ngx_http_v2_connection_t *h2c,
     u_char **pos, u_char *end, ngx_uint_t prefix);
 
 static ngx_http_v2_stream_t *ngx_http_v2_create_stream(
-    ngx_http_v2_connection_t *h2c, ngx_uint_t push);
+    ngx_http_v2_connection_t *h2c);
 static ngx_http_v2_node_t *ngx_http_v2_get_node_by_id(
     ngx_http_v2_connection_t *h2c, ngx_uint_t sid, ngx_uint_t alloc);
 static ngx_http_v2_node_t *ngx_http_v2_get_closed_node(
@@ -164,14 +154,11 @@ static ngx_int_t ngx_http_v2_parse_scheme(ngx_http_request_t *r,
     ngx_str_t *value);
 static ngx_int_t ngx_http_v2_parse_authority(ngx_http_request_t *r,
     ngx_str_t *value);
-static ngx_int_t ngx_http_v2_parse_header(ngx_http_request_t *r,
-    ngx_http_v2_parse_header_t *header, ngx_str_t *value);
 static ngx_int_t ngx_http_v2_construct_request_line(ngx_http_request_t *r);
 static ngx_int_t ngx_http_v2_cookie(ngx_http_request_t *r,
     ngx_http_v2_header_t *header);
 static ngx_int_t ngx_http_v2_construct_cookie_header(ngx_http_request_t *r);
 static void ngx_http_v2_run_request(ngx_http_request_t *r);
-static void ngx_http_v2_run_request_handler(ngx_event_t *ev);
 static ngx_int_t ngx_http_v2_process_request_body(ngx_http_request_t *r,
     u_char *pos, size_t size, ngx_uint_t last, ngx_uint_t flush);
 static ngx_int_t ngx_http_v2_filter_request_body(ngx_http_request_t *r);
@@ -212,26 +199,10 @@ static ngx_http_v2_handler_pt ngx_http_v2_frame_states[] = {
     (sizeof(ngx_http_v2_frame_states) / sizeof(ngx_http_v2_handler_pt))
 
 
-static ngx_http_v2_parse_header_t  ngx_http_v2_parse_headers[] = {
-    { ngx_string("host"),
-      offsetof(ngx_http_headers_in_t, host), 0, NULL },
-
-    { ngx_string("accept-encoding"),
-      offsetof(ngx_http_headers_in_t, accept_encoding), 0, NULL },
-
-    { ngx_string("accept-language"),
-      offsetof(ngx_http_headers_in_t, accept_language), 0, NULL },
-
-    { ngx_string("user-agent"),
-      offsetof(ngx_http_headers_in_t, user_agent), 0, NULL },
-
-    { ngx_null_string, 0, 0, NULL }
-};
-
-
 void
 ngx_http_v2_init(ngx_event_t *rev)
 {
+    u_char                    *p, *end;
     ngx_connection_t          *c;
     ngx_pool_cleanup_t        *cln;
     ngx_http_connection_t     *hc;
@@ -276,7 +247,6 @@ ngx_http_v2_init(ngx_event_t *rev)
 
     h2scf = ngx_http_get_module_srv_conf(hc->conf_ctx, ngx_http_v2_module);
 
-    h2c->concurrent_pushes = h2scf->concurrent_pushes;
     h2c->priority_limit = ngx_max(h2scf->concurrent_streams, 100);
 
     h2c->pool = ngx_create_pool(h2scf->pool_size, h2c->connection->log);
@@ -314,8 +284,7 @@ ngx_http_v2_init(ngx_event_t *rev)
         return;
     }
 
-    h2c->state.handler = hc->proxy_protocol ? ngx_http_v2_state_proxy_protocol
-                                            : ngx_http_v2_state_preface;
+    h2c->state.handler = ngx_http_v2_state_preface;
 
     ngx_queue_init(&h2c->waiting);
     ngx_queue_init(&h2c->dependencies);
@@ -335,6 +304,23 @@ ngx_http_v2_init(ngx_event_t *rev)
     c->idle = 1;
     ngx_reusable_connection(c, 0);
 
+    if (c->buffer) {
+        p = c->buffer->pos;
+        end = c->buffer->last;
+
+        do {
+            p = h2c->state.handler(h2c, p, end);
+
+            if (p == NULL) {
+                return;
+            }
+
+        } while (p != end);
+
+        h2c->total_bytes += p - c->buffer->pos;
+        c->buffer->pos = p;
+    }
+
     ngx_http_v2_read_handler(rev);
 }
 
@@ -361,6 +347,7 @@ ngx_http_v2_read_handler(ngx_event_t *rev)
     ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http2 read handler");
 
     h2c->blocked = 1;
+    h2c->new_streams = 0;
 
     if (c->close) {
         c->close = 0;
@@ -370,7 +357,7 @@ ngx_http_v2_read_handler(ngx_event_t *rev)
             return;
         }
 
-        if (!h2c->processing && !h2c->pushing) {
+        if (!h2c->processing) {
             ngx_http_v2_finalize_connection(h2c, NGX_HTTP_V2_NO_ERROR);
             return;
         }
@@ -399,13 +386,11 @@ ngx_http_v2_read_handler(ngx_event_t *rev)
     h2mcf = ngx_http_get_module_main_conf(h2c->http_connection->conf_ctx,
                                           ngx_http_v2_module);
 
-    available = h2mcf->recv_buffer_size - 2 * NGX_HTTP_V2_STATE_BUFFER_SIZE;
+    available = h2mcf->recv_buffer_size - NGX_HTTP_V2_STATE_BUFFER_SIZE;
 
     do {
         p = h2mcf->recv_buffer;
-
-        ngx_memcpy(p, h2c->state.buffer, NGX_HTTP_V2_STATE_BUFFER_SIZE);
-        end = p + h2c->state.buffer_used;
+        end = ngx_cpymem(p, h2c->state.buffer, h2c->state.buffer_used);
 
         n = c->recv(c, end, available);
 
@@ -413,9 +398,7 @@ ngx_http_v2_read_handler(ngx_event_t *rev)
             break;
         }
 
-        if (n == 0
-            && (h2c->state.incomplete || h2c->processing || h2c->pushing))
-        {
+        if (n == 0 && (h2c->state.incomplete || h2c->processing)) {
             ngx_log_error(NGX_LOG_INFO, c->log, 0,
                           "client prematurely closed connection");
         }
@@ -638,7 +621,7 @@ ngx_http_v2_handle_connection(ngx_http_v2_connection_t *h2c)
     ngx_connection_t          *c;
     ngx_http_core_loc_conf_t  *clcf;
 
-    if (h2c->last_out || h2c->processing || h2c->pushing) {
+    if (h2c->last_out || h2c->processing) {
         return;
     }
 
@@ -846,32 +829,11 @@ ngx_http_v2_lingering_close_handler(ngx_event_t *rev)
 }
 
 
-static u_char *
-ngx_http_v2_state_proxy_protocol(ngx_http_v2_connection_t *h2c, u_char *pos,
-    u_char *end)
-{
-    ngx_log_t  *log;
-
-    log = h2c->connection->log;
-    log->action = "reading PROXY protocol";
-
-    pos = ngx_proxy_protocol_read(h2c->connection, pos, end);
-
-    log->action = "processing HTTP/2 connection";
-
-    if (pos == NULL) {
-        return ngx_http_v2_connection_error(h2c, NGX_HTTP_V2_PROTOCOL_ERROR);
-    }
-
-    return ngx_http_v2_state_preface(h2c, pos, end);
-}
-
-
 static u_char *
 ngx_http_v2_state_preface(ngx_http_v2_connection_t *h2c, u_char *pos,
     u_char *end)
 {
-    static const u_char preface[] = "PRI * HTTP/2.0\r\n";
+    static const u_char preface[] = NGX_HTTP_V2_PREFACE_START;
 
     if ((size_t) (end - pos) < sizeof(preface) - 1) {
         return ngx_http_v2_state_save(h2c, pos, end, ngx_http_v2_state_preface);
@@ -892,7 +854,7 @@ static u_char *
 ngx_http_v2_state_preface_end(ngx_http_v2_connection_t *h2c, u_char *pos,
     u_char *end)
 {
-    static const u_char preface[] = "\r\nSM\r\n\r\n";
+    static const u_char preface[] = NGX_HTTP_V2_PREFACE_END;
 
     if ((size_t) (end - pos) < sizeof(preface) - 1) {
         return ngx_http_v2_state_save(h2c, pos, end,
@@ -1321,6 +1283,14 @@ ngx_http_v2_state_headers(ngx_http_v2_connection_t *h2c, u_char *pos,
         goto rst_stream;
     }
 
+    if (h2c->new_streams++ >= 2 * h2scf->concurrent_streams) {
+        ngx_log_error(NGX_LOG_INFO, h2c->connection->log, 0,
+                      "client sent too many streams at once");
+
+        status = NGX_HTTP_V2_REFUSED_STREAM;
+        goto rst_stream;
+    }
+
     if (!h2c->settings_ack
         && !(h2c->state.flags & NGX_HTTP_V2_END_STREAM_FLAG)
         && h2scf->preread_size < NGX_HTTP_V2_DEFAULT_WINDOW)
@@ -1344,7 +1314,7 @@ ngx_http_v2_state_headers(ngx_http_v2_connection_t *h2c, u_char *pos,
         h2c->closed_nodes--;
     }
 
-    stream = ngx_http_v2_create_stream(h2c, 0);
+    stream = ngx_http_v2_create_stream(h2c);
     if (stream == NULL) {
         return ngx_http_v2_connection_error(h2c, NGX_HTTP_V2_INTERNAL_ERROR);
     }
@@ -1386,6 +1356,12 @@ ngx_http_v2_state_headers(ngx_http_v2_connection_t *h2c, u_char *pos,
 
 rst_stream:
 
+    if (h2c->refused_streams++ > ngx_max(h2scf->concurrent_streams, 100)) {
+        ngx_log_error(NGX_LOG_INFO, h2c->connection->log, 0,
+                      "client sent too many refused streams");
+        return ngx_http_v2_connection_error(h2c, NGX_HTTP_V2_NO_ERROR);
+    }
+
     if (ngx_http_v2_send_rst_stream(h2c, h2c->state.sid, status) != NGX_OK) {
         return ngx_http_v2_connection_error(h2c, NGX_HTTP_V2_INTERNAL_ERROR);
     }
@@ -1730,6 +1706,7 @@ ngx_http_v2_state_process_header(ngx_http_v2_connection_t *h2c, u_char *pos,
     size_t                      len;
     ngx_int_t                   rc;
     ngx_table_elt_t            *h;
+    ngx_connection_t           *fc;
     ngx_http_header_t          *hh;
     ngx_http_request_t         *r;
     ngx_http_v2_header_t       *header;
@@ -1789,17 +1766,11 @@ ngx_http_v2_state_process_header(ngx_http_v2_connection_t *h2c, u_char *pos,
     }
 
     r = h2c->state.stream->request;
+    fc = r->connection;
 
     /* TODO Optimization: validate headers while parsing. */
     if (ngx_http_v2_validate_header(r, header) != NGX_OK) {
-        if (ngx_http_v2_terminate_stream(h2c, h2c->state.stream,
-                                         NGX_HTTP_V2_PROTOCOL_ERROR)
-            == NGX_ERROR)
-        {
-            return ngx_http_v2_connection_error(h2c,
-                                                NGX_HTTP_V2_INTERNAL_ERROR);
-        }
-
+        ngx_http_finalize_request(r, NGX_HTTP_BAD_REQUEST);
         goto error;
     }
 
@@ -1886,6 +1857,8 @@ error:
 
     h2c->state.stream = NULL;
 
+    ngx_http_run_posted_requests(fc);
+
     return ngx_http_v2_state_header_complete(h2c, pos, end);
 }
 
@@ -2136,11 +2109,6 @@ ngx_http_v2_state_rst_stream(ngx_http_v2_connection_t *h2c, u_char *pos,
                       "client canceled stream %ui", h2c->state.sid);
         break;
 
-    case NGX_HTTP_V2_REFUSED_STREAM:
-        ngx_log_error(NGX_LOG_INFO, fc->log, 0,
-                      "client refused stream %ui", h2c->state.sid);
-        break;
-
     case NGX_HTTP_V2_INTERNAL_ERROR:
         ngx_log_error(NGX_LOG_INFO, fc->log, 0,
                       "client terminated stream %ui due to internal error",
@@ -2208,7 +2176,6 @@ ngx_http_v2_state_settings_params(ngx_http_v2_connection_t *h2c, u_char *pos,
 {
     ssize_t                   window_delta;
     ngx_uint_t                id, value;
-    ngx_http_v2_srv_conf_t   *h2scf;
     ngx_http_v2_out_frame_t  *frame;
 
     window_delta = 0;
@@ -2270,14 +2237,6 @@ ngx_http_v2_state_settings_params(ngx_http_v2_connection_t *h2c, u_char *pos,
                                                     NGX_HTTP_V2_PROTOCOL_ERROR);
             }
 
-            h2c->push_disabled = !value;
-            break;
-
-        case NGX_HTTP_V2_MAX_STREAMS_SETTING:
-            h2scf = ngx_http_get_module_srv_conf(h2c->http_connection->conf_ctx,
-                                                 ngx_http_v2_module);
-
-            h2c->concurrent_pushes = ngx_min(value, h2scf->concurrent_pushes);
             break;
 
         case NGX_HTTP_V2_HEADER_TABLE_SIZE_SETTING:
@@ -2631,7 +2590,7 @@ ngx_http_v2_state_save(ngx_http_v2_connection_t *h2c, u_char *pos, u_char *end,
         return ngx_http_v2_connection_error(h2c, NGX_HTTP_V2_INTERNAL_ERROR);
     }
 
-    ngx_memcpy(h2c->state.buffer, pos, NGX_HTTP_V2_STATE_BUFFER_SIZE);
+    ngx_memcpy(h2c->state.buffer, pos, size);
 
     h2c->state.buffer_used = size;
     h2c->state.handler = handler;
@@ -2732,163 +2691,6 @@ ngx_http_v2_parse_int(ngx_http_v2_connection_t *h2c, u_char **pos, u_char *end,
 }
 
 
-ngx_http_v2_stream_t *
-ngx_http_v2_push_stream(ngx_http_v2_stream_t *parent, ngx_str_t *path)
-{
-    ngx_int_t                     rc;
-    ngx_str_t                     value;
-    ngx_pool_t                   *pool;
-    ngx_uint_t                    index;
-    ngx_table_elt_t             **h;
-    ngx_connection_t             *fc;
-    ngx_http_request_t           *r;
-    ngx_http_v2_node_t           *node;
-    ngx_http_v2_stream_t         *stream;
-    ngx_http_v2_srv_conf_t       *h2scf;
-    ngx_http_v2_connection_t     *h2c;
-    ngx_http_v2_parse_header_t   *header;
-
-    h2c = parent->connection;
-
-    pool = ngx_create_pool(1024, h2c->connection->log);
-    if (pool == NULL) {
-        goto rst_stream;
-    }
-
-    node = ngx_http_v2_get_node_by_id(h2c, h2c->last_push, 1);
-
-    if (node == NULL) {
-        ngx_destroy_pool(pool);
-        goto rst_stream;
-    }
-
-    stream = ngx_http_v2_create_stream(h2c, 1);
-    if (stream == NULL) {
-
-        if (node->parent == NULL) {
-            h2scf = ngx_http_get_module_srv_conf(h2c->http_connection->conf_ctx,
-                                                 ngx_http_v2_module);
-
-            index = ngx_http_v2_index(h2scf, h2c->last_push);
-            h2c->streams_index[index] = node->index;
-
-            ngx_queue_insert_tail(&h2c->closed, &node->reuse);
-            h2c->closed_nodes++;
-        }
-
-        ngx_destroy_pool(pool);
-        goto rst_stream;
-    }
-
-    if (node->parent) {
-        ngx_queue_remove(&node->reuse);
-        h2c->closed_nodes--;
-    }
-
-    stream->pool = pool;
-
-    r = stream->request;
-    fc = r->connection;
-
-    stream->in_closed = 1;
-    stream->node = node;
-
-    node->stream = stream;
-
-    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, h2c->connection->log, 0,
-                   "http2 push stream sid:%ui "
-                   "depends on %ui excl:0 weight:16",
-                   h2c->last_push, parent->node->id);
-
-    node->weight = NGX_HTTP_V2_DEFAULT_WEIGHT;
-    ngx_http_v2_set_dependency(h2c, node, parent->node->id, 0);
-
-    r->method_name = ngx_http_core_get_method;
-    r->method = NGX_HTTP_GET;
-
-    r->schema.data = ngx_pstrdup(pool, &parent->request->schema);
-    if (r->schema.data == NULL) {
-        goto close;
-    }
-
-    r->schema.len = parent->request->schema.len;
-
-    value.data = ngx_pstrdup(pool, path);
-    if (value.data == NULL) {
-        goto close;
-    }
-
-    value.len = path->len;
-
-    rc = ngx_http_v2_parse_path(r, &value);
-
-    if (rc != NGX_OK) {
-        goto error;
-    }
-
-    for (header = ngx_http_v2_parse_headers; header->name.len; header++) {
-        h = (ngx_table_elt_t **)
-                ((char *) &parent->request->headers_in + header->offset);
-
-        if (*h == NULL) {
-            continue;
-        }
-
-        value.len = (*h)->value.len;
-
-        value.data = ngx_pnalloc(pool, value.len + 1);
-        if (value.data == NULL) {
-            goto close;
-        }
-
-        ngx_memcpy(value.data, (*h)->value.data, value.len);
-        value.data[value.len] = '\0';
-
-        rc = ngx_http_v2_parse_header(r, header, &value);
-
-        if (rc != NGX_OK) {
-            goto error;
-        }
-    }
-
-    fc->write->handler = ngx_http_v2_run_request_handler;
-    ngx_post_event(fc->write, &ngx_posted_events);
-
-    return stream;
-
-error:
-
-    if (rc == NGX_ABORT) {
-        /* header handler has already finalized request */
-        ngx_http_run_posted_requests(fc);
-        return NULL;
-    }
-
-    if (rc == NGX_DECLINED) {
-        ngx_http_finalize_request(r, NGX_HTTP_BAD_REQUEST);
-        ngx_http_run_posted_requests(fc);
-        return NULL;
-    }
-
-close:
-
-    ngx_http_v2_close_stream(stream, NGX_HTTP_INTERNAL_SERVER_ERROR);
-
-    return NULL;
-
-rst_stream:
-
-    if (ngx_http_v2_send_rst_stream(h2c, h2c->last_push,
-                                    NGX_HTTP_INTERNAL_SERVER_ERROR)
-        != NGX_OK)
-    {
-        h2c->connection->error = 1;
-    }
-
-    return NULL;
-}
-
-
 static ngx_int_t
 ngx_http_v2_send_settings(ngx_http_v2_connection_t *h2c)
 {
@@ -3160,7 +2962,7 @@ ngx_http_v2_frame_handler(ngx_http_v2_connection_t *h2c,
 
 
 static ngx_http_v2_stream_t *
-ngx_http_v2_create_stream(ngx_http_v2_connection_t *h2c, ngx_uint_t push)
+ngx_http_v2_create_stream(ngx_http_v2_connection_t *h2c)
 {
     ngx_log_t                 *log;
     ngx_event_t               *rev, *wev;
@@ -3215,13 +3017,7 @@ ngx_http_v2_create_stream(ngx_http_v2_connection_t *h2c, ngx_uint_t push)
     ngx_memcpy(log, h2c->connection->log, sizeof(ngx_log_t));
 
     log->data = ctx;
-
-    if (push) {
-        log->action = "processing pushed request headers";
-
-    } else {
-        log->action = "reading client request headers";
-    }
+    log->action = "reading client request headers";
 
     ngx_memzero(rev, sizeof(ngx_event_t));
 
@@ -3293,12 +3089,7 @@ ngx_http_v2_create_stream(ngx_http_v2_connection_t *h2c, ngx_uint_t push)
     stream->send_window = h2c->init_window;
     stream->recv_window = h2scf->preread_size;
 
-    if (push) {
-        h2c->pushing++;
-
-    } else {
-        h2c->processing++;
-    }
+    h2c->processing++;
 
     h2c->priority_limit += h2scf->concurrent_streams;
 
@@ -3720,46 +3511,42 @@ ngx_http_v2_parse_scheme(ngx_http_request_t *r, ngx_str_t *value)
 
 static ngx_int_t
 ngx_http_v2_parse_authority(ngx_http_request_t *r, ngx_str_t *value)
-{
-    return ngx_http_v2_parse_header(r, &ngx_http_v2_parse_headers[0], value);
-}
-
-
-static ngx_int_t
-ngx_http_v2_parse_header(ngx_http_request_t *r,
-    ngx_http_v2_parse_header_t *header, ngx_str_t *value)
 {
     ngx_table_elt_t            *h;
+    ngx_http_header_t          *hh;
     ngx_http_core_main_conf_t  *cmcf;
 
+    static ngx_str_t host = ngx_string("host");
+
     h = ngx_list_push(&r->headers_in.headers);
     if (h == NULL) {
         return NGX_ERROR;
     }
 
-    h->key.len = header->name.len;
-    h->key.data = header->name.data;
-    h->lowcase_key = header->name.data;
+    h->hash = ngx_hash(ngx_hash(ngx_hash('h', 'o'), 's'), 't');
 
-    if (header->hh == NULL) {
-        header->hash = ngx_hash_key(header->name.data, header->name.len);
+    h->key.len = host.len;
+    h->key.data = host.data;
 
-        cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module);
+    h->value.len = value->len;
+    h->value.data = value->data;
 
-        header->hh = ngx_hash_find(&cmcf->headers_in_hash, header->hash,
-                                   h->lowcase_key, h->key.len);
-        if (header->hh == NULL) {
-            return NGX_ERROR;
-        }
-    }
+    h->lowcase_key = host.data;
 
-    h->hash = header->hash;
+    cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module);
 
-    h->value.len = value->len;
-    h->value.data = value->data;
+    hh = ngx_hash_find(&cmcf->headers_in_hash, h->hash,
+                       h->lowcase_key, h->key.len);
 
-    if (header->hh->handler(r, h, header->hh->offset) != NGX_OK) {
-        /* header handler has already finalized request */
+    if (hh == NULL) {
+        return NGX_ERROR;
+    }
+
+    if (hh->handler(r, h, hh->offset) != NGX_OK) {
+        /*
+         * request has been finalized already
+         * in ngx_http_process_host()
+         */
         return NGX_ABORT;
     }
 
@@ -3946,10 +3733,22 @@ static void
 ngx_http_v2_run_request(ngx_http_request_t *r)
 {
     ngx_connection_t          *fc;
+    ngx_http_v2_srv_conf_t    *h2scf;
     ngx_http_v2_connection_t  *h2c;
 
     fc = r->connection;
 
+    h2scf = ngx_http_get_module_srv_conf(r, ngx_http_v2_module);
+
+    if (!h2scf->enable && !r->http_connection->addr_conf->http2) {
+        ngx_log_error(NGX_LOG_INFO, fc->log, 0,
+                      "client attempted to request the server name "
+                      "for which the negotiated protocol is disabled");
+
+        ngx_http_finalize_request(r, NGX_HTTP_MISDIRECTED_REQUEST);
+        goto failed;
+    }
+
     if (ngx_http_v2_construct_request_line(r) != NGX_OK) {
         goto failed;
     }
@@ -3990,22 +3789,6 @@ failed:
 }
 
 
-static void
-ngx_http_v2_run_request_handler(ngx_event_t *ev)
-{
-    ngx_connection_t    *fc;
-    ngx_http_request_t  *r;
-
-    fc = ev->data;
-    r = fc->data;
-
-    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, fc->log, 0,
-                   "http2 run request handler");
-
-    ngx_http_v2_run_request(r);
-}
-
-
 ngx_int_t
 ngx_http_v2_read_request_body(ngx_http_request_t *r)
 {
@@ -4609,7 +4392,6 @@ void
 ngx_http_v2_close_stream(ngx_http_v2_stream_t *stream, ngx_int_t rc)
 {
     ngx_pool_t                *pool;
-    ngx_uint_t                 push;
     ngx_event_t               *ev;
     ngx_connection_t          *fc;
     ngx_http_v2_node_t        *node;
@@ -4618,10 +4400,9 @@ ngx_http_v2_close_stream(ngx_http_v2_stream_t *stream, ngx_int_t rc)
     h2c = stream->connection;
     node = stream->node;
 
-    ngx_log_debug4(NGX_LOG_DEBUG_HTTP, h2c->connection->log, 0,
-                   "http2 close stream %ui, queued %ui, "
-                   "processing %ui, pushing %ui",
-                   node->id, stream->queued, h2c->processing, h2c->pushing);
+    ngx_log_debug3(NGX_LOG_DEBUG_HTTP, h2c->connection->log, 0,
+                   "http2 close stream %ui, queued %ui, processing %ui",
+                   node->id, stream->queued, h2c->processing);
 
     fc = stream->request->connection;
 
@@ -4656,8 +4437,6 @@ ngx_http_v2_close_stream(ngx_http_v2_stream_t *stream, ngx_int_t rc)
         h2c->state.stream = NULL;
     }
 
-    push = stream->node->id % 2 == 0;
-
     node->stream = NULL;
 
     ngx_queue_insert_tail(&h2c->closed, &node->reuse);
@@ -4707,14 +4486,9 @@ ngx_http_v2_close_stream(ngx_http_v2_stream_t *stream, ngx_int_t rc)
     fc->data = h2c->free_fake_connections;
     h2c->free_fake_connections = fc;
 
-    if (push) {
-        h2c->pushing--;
-
-    } else {
-        h2c->processing--;
-    }
+    h2c->processing--;
 
-    if (h2c->processing || h2c->pushing || h2c->blocked) {
+    if (h2c->processing || h2c->blocked) {
         return;
     }
 
@@ -4890,7 +4664,7 @@ ngx_http_v2_finalize_connection(ngx_http_v2_connection_t *h2c,
         }
     }
 
-    if (!h2c->processing && !h2c->pushing) {
+    if (!h2c->processing) {
         goto done;
     }
 
@@ -4938,7 +4712,7 @@ ngx_http_v2_finalize_connection(ngx_http_v2_connection_t *h2c,
 
     h2c->blocked = 0;
 
-    if (h2c->processing || h2c->pushing) {
+    if (h2c->processing) {
         c->error = 1;
         return;
     }
diff --git a/src/http/v2/ngx_http_v2.h b/src/http/v2/ngx_http_v2.h
index 70ee287..6751b30 100644
--- a/src/http/v2/ngx_http_v2.h
+++ b/src/http/v2/ngx_http_v2.h
@@ -24,8 +24,6 @@
 #define NGX_HTTP_V2_MAX_FIELD                                                 \
     (127 + (1 << (NGX_HTTP_V2_INT_OCTETS - 1) * 7) - 1)
 
-#define NGX_HTTP_V2_STREAM_ID_SIZE       4
-
 #define NGX_HTTP_V2_FRAME_HEADER_SIZE    9
 
 /* frame types */
@@ -63,6 +61,15 @@ typedef u_char *(*ngx_http_v2_handler_pt) (ngx_http_v2_connection_t *h2c,
     u_char *pos, u_char *end);
 
 
+typedef struct {
+    ngx_flag_t                       enable;
+    size_t                           pool_size;
+    ngx_uint_t                       concurrent_streams;
+    size_t                           preread_size;
+    ngx_uint_t                       streams_index_mask;
+} ngx_http_v2_srv_conf_t;
+
+
 typedef struct {
     ngx_str_t                        name;
     ngx_str_t                        value;
@@ -124,11 +131,10 @@ struct ngx_http_v2_connection_s {
     ngx_uint_t                       processing;
     ngx_uint_t                       frames;
     ngx_uint_t                       idle;
+    ngx_uint_t                       new_streams;
+    ngx_uint_t                       refused_streams;
     ngx_uint_t                       priority_limit;
 
-    ngx_uint_t                       pushing;
-    ngx_uint_t                       concurrent_pushes;
-
     size_t                           send_window;
     size_t                           recv_window;
     size_t                           init_window;
@@ -153,17 +159,15 @@ struct ngx_http_v2_connection_s {
     ngx_queue_t                      dependencies;
     ngx_queue_t                      closed;
 
+    ngx_uint_t                       closed_nodes;
     ngx_uint_t                       last_sid;
-    ngx_uint_t                       last_push;
 
     time_t                           lingering_time;
 
-    unsigned                         closed_nodes:8;
     unsigned                         settings_ack:1;
     unsigned                         table_update:1;
     unsigned                         blocked:1;
     unsigned                         goaway:1;
-    unsigned                         push_disabled:1;
 };
 
 
@@ -293,9 +297,6 @@ void ngx_http_v2_init(ngx_event_t *rev);
 ngx_int_t ngx_http_v2_read_request_body(ngx_http_request_t *r);
 ngx_int_t ngx_http_v2_read_unbuffered_request_body(ngx_http_request_t *r);
 
-ngx_http_v2_stream_t *ngx_http_v2_push_stream(ngx_http_v2_stream_t *parent,
-    ngx_str_t *path);
-
 void ngx_http_v2_close_stream(ngx_http_v2_stream_t *stream, ngx_int_t rc);
 
 ngx_int_t ngx_http_v2_send_output_queue(ngx_http_v2_connection_t *h2c);
@@ -397,20 +398,25 @@ ngx_int_t ngx_http_v2_table_size(ngx_http_v2_connection_t *h2c, size_t size);
 #define NGX_HTTP_V2_STATUS_404_INDEX      13
 #define NGX_HTTP_V2_STATUS_500_INDEX      14
 
-#define NGX_HTTP_V2_ACCEPT_ENCODING_INDEX 16
-#define NGX_HTTP_V2_ACCEPT_LANGUAGE_INDEX 17
 #define NGX_HTTP_V2_CONTENT_LENGTH_INDEX  28
 #define NGX_HTTP_V2_CONTENT_TYPE_INDEX    31
 #define NGX_HTTP_V2_DATE_INDEX            33
 #define NGX_HTTP_V2_LAST_MODIFIED_INDEX   44
 #define NGX_HTTP_V2_LOCATION_INDEX        46
 #define NGX_HTTP_V2_SERVER_INDEX          54
-#define NGX_HTTP_V2_USER_AGENT_INDEX      58
 #define NGX_HTTP_V2_VARY_INDEX            59
 
+#define NGX_HTTP_V2_PREFACE_START         "PRI * HTTP/2.0\r\n"
+#define NGX_HTTP_V2_PREFACE_END           "\r\nSM\r\n\r\n"
+#define NGX_HTTP_V2_PREFACE               NGX_HTTP_V2_PREFACE_START           \
+                                          NGX_HTTP_V2_PREFACE_END
+
 
 u_char *ngx_http_v2_string_encode(u_char *dst, u_char *src, size_t len,
     u_char *tmp, ngx_uint_t lower);
 
 
+extern ngx_module_t  ngx_http_v2_module;
+
+
 #endif /* _NGX_HTTP_V2_H_INCLUDED_ */
diff --git a/src/http/v2/ngx_http_v2_filter_module.c b/src/http/v2/ngx_http_v2_filter_module.c
index 9ffb155..1e2cafa 100644
--- a/src/http/v2/ngx_http_v2_filter_module.c
+++ b/src/http/v2/ngx_http_v2_filter_module.c
@@ -27,39 +27,8 @@
 #define NGX_HTTP_V2_NO_TRAILERS           (ngx_http_v2_out_frame_t *) -1
 
 
-typedef struct {
-    ngx_str_t      name;
-    u_char         index;
-    ngx_uint_t     offset;
-} ngx_http_v2_push_header_t;
-
-
-static ngx_http_v2_push_header_t  ngx_http_v2_push_headers[] = {
-    { ngx_string(":authority"), NGX_HTTP_V2_AUTHORITY_INDEX,
-      offsetof(ngx_http_headers_in_t, host) },
-
-    { ngx_string("accept-encoding"), NGX_HTTP_V2_ACCEPT_ENCODING_INDEX,
-      offsetof(ngx_http_headers_in_t, accept_encoding) },
-
-    { ngx_string("accept-language"), NGX_HTTP_V2_ACCEPT_LANGUAGE_INDEX,
-      offsetof(ngx_http_headers_in_t, accept_language) },
-
-    { ngx_string("user-agent"), NGX_HTTP_V2_USER_AGENT_INDEX,
-      offsetof(ngx_http_headers_in_t, user_agent) },
-};
-
-#define NGX_HTTP_V2_PUSH_HEADERS                                              \
-    (sizeof(ngx_http_v2_push_headers) / sizeof(ngx_http_v2_push_header_t))
-
-
-static ngx_int_t ngx_http_v2_push_resources(ngx_http_request_t *r);
-static ngx_int_t ngx_http_v2_push_resource(ngx_http_request_t *r,
-    ngx_str_t *path, ngx_str_t *binary);
-
 static ngx_http_v2_out_frame_t *ngx_http_v2_create_headers_frame(
     ngx_http_request_t *r, u_char *pos, u_char *end, ngx_uint_t fin);
-static ngx_http_v2_out_frame_t *ngx_http_v2_create_push_frame(
-    ngx_http_request_t *r, u_char *pos, u_char *end);
 static ngx_http_v2_out_frame_t *ngx_http_v2_create_trailers_frame(
     ngx_http_request_t *r);
 
@@ -82,8 +51,6 @@ static ngx_inline ngx_int_t ngx_http_v2_filter_send(
 
 static ngx_int_t ngx_http_v2_headers_frame_handler(
     ngx_http_v2_connection_t *h2c, ngx_http_v2_out_frame_t *frame);
-static ngx_int_t ngx_http_v2_push_frame_handler(
-    ngx_http_v2_connection_t *h2c, ngx_http_v2_out_frame_t *frame);
 static ngx_int_t ngx_http_v2_data_frame_handler(
     ngx_http_v2_connection_t *h2c, ngx_http_v2_out_frame_t *frame);
 static ngx_inline void ngx_http_v2_handle_frame(
@@ -244,15 +211,6 @@ ngx_http_v2_header_filter(ngx_http_request_t *r)
 
     h2c = stream->connection;
 
-    if (!h2c->push_disabled && !h2c->goaway
-        && stream->node->id % 2 == 1
-        && r->method != NGX_HTTP_HEAD)
-    {
-        if (ngx_http_v2_push_resources(r) != NGX_OK) {
-            return NGX_ERROR;
-        }
-    }
-
     len = h2c->table_update ? 1 : 0;
 
     len += status ? 1 : 1 + ngx_http_v2_literal_size("418");
@@ -653,7 +611,7 @@ ngx_http_v2_header_filter(ngx_http_request_t *r)
 
     ngx_http_v2_queue_blocked_frame(h2c, frame);
 
-    stream->queued++;
+    stream->queued = 1;
 
     cln = ngx_http_cleanup_add(r, 0);
     if (cln == NULL) {
@@ -665,416 +623,12 @@ ngx_http_v2_header_filter(ngx_http_request_t *r)
 
     fc->send_chain = ngx_http_v2_send_chain;
     fc->need_last_buf = 1;
+    fc->need_flush_buf = 1;
 
     return ngx_http_v2_filter_send(fc, stream);
 }
 
 
-static ngx_int_t
-ngx_http_v2_push_resources(ngx_http_request_t *r)
-{
-    u_char                     *start, *end, *last;
-    ngx_int_t                   rc;
-    ngx_str_t                   path;
-    ngx_uint_t                  i, push;
-    ngx_table_elt_t           **h;
-    ngx_http_v2_loc_conf_t     *h2lcf;
-    ngx_http_complex_value_t   *pushes;
-    ngx_str_t                   binary[NGX_HTTP_V2_PUSH_HEADERS];
-
-    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
-                   "http2 push resources");
-
-    ngx_memzero(binary, NGX_HTTP_V2_PUSH_HEADERS * sizeof(ngx_str_t));
-
-    h2lcf = ngx_http_get_module_loc_conf(r, ngx_http_v2_module);
-
-    if (h2lcf->pushes) {
-        pushes = h2lcf->pushes->elts;
-
-        for (i = 0; i < h2lcf->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_v2_push_resource(r, &path, binary);
-
-            if (rc == NGX_ERROR) {
-                return NGX_ERROR;
-            }
-
-            if (rc == NGX_ABORT) {
-                return NGX_OK;
-            }
-
-            /* NGX_OK, NGX_DECLINED */
-        }
-    }
-
-    if (!h2lcf->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,
-                       "http2 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_v2_push_resource(r, &path, binary);
-
-            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_v2_push_resource(ngx_http_request_t *r, ngx_str_t *path,
-    ngx_str_t *binary)
-{
-    u_char                      *start, *pos, *tmp;
-    size_t                       len;
-    ngx_str_t                   *value;
-    ngx_uint_t                   i;
-    ngx_table_elt_t            **h;
-    ngx_connection_t            *fc;
-    ngx_http_v2_stream_t        *stream;
-    ngx_http_v2_out_frame_t     *frame;
-    ngx_http_v2_connection_t    *h2c;
-    ngx_http_v2_push_header_t   *ph;
-
-    fc = r->connection;
-
-    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, fc->log, 0, "http2 push resource");
-
-    stream = r->stream;
-    h2c = stream->connection;
-
-    if (!ngx_path_separator(path->data[0])) {
-        ngx_log_error(NGX_LOG_WARN, fc->log, 0,
-                      "non-absolute path \"%V\" not pushed", path);
-        return NGX_DECLINED;
-    }
-
-    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, h2c->connection->log, 0,
-                   "http2 pushing:%ui limit:%ui",
-                   h2c->pushing, h2c->concurrent_pushes);
-
-    if (h2c->pushing >= h2c->concurrent_pushes) {
-        return NGX_ABORT;
-    }
-
-    if (h2c->last_push == 0x7ffffffe) {
-        return NGX_ABORT;
-    }
-
-    if (path->len > NGX_HTTP_V2_MAX_FIELD) {
-        return NGX_DECLINED;
-    }
-
-    if (r->headers_in.host == NULL) {
-        return NGX_ABORT;
-    }
-
-    ph = ngx_http_v2_push_headers;
-
-    len = ngx_max(r->schema.len, path->len);
-
-    if (binary[0].len) {
-        tmp = ngx_palloc(r->pool, len);
-        if (tmp == NULL) {
-            return NGX_ERROR;
-        }
-
-    } else {
-        for (i = 0; i < NGX_HTTP_V2_PUSH_HEADERS; i++) {
-            h = (ngx_table_elt_t **) ((char *) &r->headers_in + ph[i].offset);
-
-            if (*h) {
-                len = ngx_max(len, (*h)->value.len);
-            }
-        }
-
-        tmp = ngx_palloc(r->pool, len);
-        if (tmp == NULL) {
-            return NGX_ERROR;
-        }
-
-        for (i = 0; i < NGX_HTTP_V2_PUSH_HEADERS; i++) {
-            h = (ngx_table_elt_t **) ((char *) &r->headers_in + ph[i].offset);
-
-            if (*h == NULL) {
-                continue;
-            }
-
-            value = &(*h)->value;
-
-            len = 1 + NGX_HTTP_V2_INT_OCTETS + value->len;
-
-            pos = ngx_pnalloc(r->pool, len);
-            if (pos == NULL) {
-                return NGX_ERROR;
-            }
-
-            binary[i].data = pos;
-
-            *pos++ = ngx_http_v2_inc_indexed(ph[i].index);
-            pos = ngx_http_v2_write_value(pos, value->data, value->len, tmp);
-
-            binary[i].len = pos - binary[i].data;
-        }
-    }
-
-    len = (h2c->table_update ? 1 : 0)
-          + 1
-          + 1 + NGX_HTTP_V2_INT_OCTETS + path->len
-          + 1 + NGX_HTTP_V2_INT_OCTETS + r->schema.len;
-
-    for (i = 0; i < NGX_HTTP_V2_PUSH_HEADERS; i++) {
-        len += binary[i].len;
-    }
-
-    pos = ngx_pnalloc(r->pool, len);
-    if (pos == NULL) {
-        return NGX_ERROR;
-    }
-
-    start = pos;
-
-    if (h2c->table_update) {
-        ngx_log_debug0(NGX_LOG_DEBUG_HTTP, fc->log, 0,
-                       "http2 table size update: 0");
-        *pos++ = (1 << 5) | 0;
-        h2c->table_update = 0;
-    }
-
-    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, fc->log, 0,
-                   "http2 push header: \":method: GET\"");
-
-    *pos++ = ngx_http_v2_indexed(NGX_HTTP_V2_METHOD_GET_INDEX);
-
-    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, fc->log, 0,
-                   "http2 push header: \":path: %V\"", path);
-
-    *pos++ = ngx_http_v2_inc_indexed(NGX_HTTP_V2_PATH_INDEX);
-    pos = ngx_http_v2_write_value(pos, path->data, path->len, tmp);
-
-    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, fc->log, 0,
-                   "http2 push header: \":scheme: %V\"", &r->schema);
-
-    if (r->schema.len == 5 && ngx_strncmp(r->schema.data, "https", 5) == 0) {
-        *pos++ = ngx_http_v2_indexed(NGX_HTTP_V2_SCHEME_HTTPS_INDEX);
-
-    } else if (r->schema.len == 4
-               && ngx_strncmp(r->schema.data, "http", 4) == 0)
-    {
-        *pos++ = ngx_http_v2_indexed(NGX_HTTP_V2_SCHEME_HTTP_INDEX);
-
-    } else {
-        *pos++ = ngx_http_v2_inc_indexed(NGX_HTTP_V2_SCHEME_HTTP_INDEX);
-        pos = ngx_http_v2_write_value(pos, r->schema.data, r->schema.len, tmp);
-    }
-
-    for (i = 0; i < NGX_HTTP_V2_PUSH_HEADERS; i++) {
-        h = (ngx_table_elt_t **) ((char *) &r->headers_in + ph[i].offset);
-
-        if (*h == NULL) {
-            continue;
-        }
-
-        ngx_log_debug2(NGX_LOG_DEBUG_HTTP, fc->log, 0,
-                       "http2 push header: \"%V: %V\"",
-                       &ph[i].name, &(*h)->value);
-
-        pos = ngx_cpymem(pos, binary[i].data, binary[i].len);
-    }
-
-    frame = ngx_http_v2_create_push_frame(r, start, pos);
-    if (frame == NULL) {
-        return NGX_ERROR;
-    }
-
-    ngx_http_v2_queue_blocked_frame(h2c, frame);
-
-    stream->queued++;
-
-    stream = ngx_http_v2_push_stream(stream, path);
-
-    if (stream) {
-        stream->request->request_length = pos - start;
-        return NGX_OK;
-    }
-
-    return NGX_ERROR;
-}
-
-
 static ngx_http_v2_out_frame_t *
 ngx_http_v2_create_headers_frame(ngx_http_request_t *r, u_char *pos,
     u_char *end, ngx_uint_t fin)
@@ -1180,125 +734,6 @@ ngx_http_v2_create_headers_frame(ngx_http_request_t *r, u_char *pos,
 }
 
 
-static ngx_http_v2_out_frame_t *
-ngx_http_v2_create_push_frame(ngx_http_request_t *r, u_char *pos, u_char *end)
-{
-    u_char                     type, flags;
-    size_t                     rest, frame_size, len;
-    ngx_buf_t                 *b;
-    ngx_chain_t               *cl, **ll;
-    ngx_http_v2_stream_t      *stream;
-    ngx_http_v2_out_frame_t   *frame;
-    ngx_http_v2_connection_t  *h2c;
-
-    stream = r->stream;
-    h2c = stream->connection;
-    rest = NGX_HTTP_V2_STREAM_ID_SIZE + (end - pos);
-
-    frame = ngx_palloc(r->pool, sizeof(ngx_http_v2_out_frame_t));
-    if (frame == NULL) {
-        return NULL;
-    }
-
-    frame->handler = ngx_http_v2_push_frame_handler;
-    frame->stream = stream;
-    frame->length = rest;
-    frame->blocked = 1;
-    frame->fin = 0;
-
-    ll = &frame->first;
-
-    type = NGX_HTTP_V2_PUSH_PROMISE_FRAME;
-    flags = NGX_HTTP_V2_NO_FLAG;
-    frame_size = h2c->frame_size;
-
-    for ( ;; ) {
-        if (rest <= frame_size) {
-            frame_size = rest;
-            flags |= NGX_HTTP_V2_END_HEADERS_FLAG;
-        }
-
-        b = ngx_create_temp_buf(r->pool,
-                                NGX_HTTP_V2_FRAME_HEADER_SIZE
-                                + ((type == NGX_HTTP_V2_PUSH_PROMISE_FRAME)
-                                   ? NGX_HTTP_V2_STREAM_ID_SIZE : 0));
-        if (b == NULL) {
-            return NULL;
-        }
-
-        b->last = ngx_http_v2_write_len_and_type(b->last, frame_size, type);
-        *b->last++ = flags;
-        b->last = ngx_http_v2_write_sid(b->last, stream->node->id);
-
-        b->tag = (ngx_buf_tag_t) &ngx_http_v2_module;
-
-        if (type == NGX_HTTP_V2_PUSH_PROMISE_FRAME) {
-            h2c->last_push += 2;
-
-            b->last = ngx_http_v2_write_sid(b->last, h2c->last_push);
-            len = frame_size - NGX_HTTP_V2_STREAM_ID_SIZE;
-
-        } else {
-            len = frame_size;
-        }
-
-        cl = ngx_alloc_chain_link(r->pool);
-        if (cl == NULL) {
-            return NULL;
-        }
-
-        cl->buf = b;
-
-        *ll = cl;
-        ll = &cl->next;
-
-        b = ngx_calloc_buf(r->pool);
-        if (b == NULL) {
-            return NULL;
-        }
-
-        b->pos = pos;
-
-        pos += len;
-
-        b->last = pos;
-        b->start = b->pos;
-        b->end = b->last;
-        b->temporary = 1;
-
-        cl = ngx_alloc_chain_link(r->pool);
-        if (cl == NULL) {
-            return NULL;
-        }
-
-        cl->buf = b;
-
-        *ll = cl;
-        ll = &cl->next;
-
-        rest -= frame_size;
-
-        if (rest) {
-            frame->length += NGX_HTTP_V2_FRAME_HEADER_SIZE;
-
-            type = NGX_HTTP_V2_CONTINUATION_FRAME;
-            continue;
-        }
-
-        cl->next = NULL;
-        frame->last = cl;
-
-        ngx_log_debug4(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
-                       "http2:%ui create PUSH_PROMISE frame %p: "
-                       "sid:%ui len:%uz",
-                       stream->node->id, frame, h2c->last_push,
-                       frame->length);
-
-        return frame;
-    }
-}
-
-
 static ngx_http_v2_out_frame_t *
 ngx_http_v2_create_trailers_frame(ngx_http_request_t *r)
 {
@@ -1815,7 +1250,11 @@ ngx_http_v2_waiting_queue(ngx_http_v2_connection_t *h2c,
 static ngx_inline ngx_int_t
 ngx_http_v2_filter_send(ngx_connection_t *fc, ngx_http_v2_stream_t *stream)
 {
-    if (stream->queued == 0) {
+    ngx_connection_t  *c;
+
+    c = stream->connection->connection;
+
+    if (stream->queued == 0 && !c->buffered) {
         fc->buffered &= ~NGX_HTTP_V2_BUFFERED;
         return NGX_OK;
     }
@@ -1898,62 +1337,6 @@ ngx_http_v2_headers_frame_handler(ngx_http_v2_connection_t *h2c,
 }
 
 
-static ngx_int_t
-ngx_http_v2_push_frame_handler(ngx_http_v2_connection_t *h2c,
-    ngx_http_v2_out_frame_t *frame)
-{
-    ngx_chain_t           *cl, *ln;
-    ngx_http_v2_stream_t  *stream;
-
-    stream = frame->stream;
-    cl = frame->first;
-
-    for ( ;; ) {
-        if (cl->buf->pos != cl->buf->last) {
-            frame->first = cl;
-
-            ngx_log_debug2(NGX_LOG_DEBUG_HTTP, h2c->connection->log, 0,
-                           "http2:%ui PUSH_PROMISE frame %p was sent partially",
-                           stream->node->id, frame);
-
-            return NGX_AGAIN;
-        }
-
-        ln = cl->next;
-
-        if (cl->buf->tag == (ngx_buf_tag_t) &ngx_http_v2_module) {
-            cl->next = stream->free_frame_headers;
-            stream->free_frame_headers = cl;
-
-        } else {
-            cl->next = stream->free_bufs;
-            stream->free_bufs = cl;
-        }
-
-        if (cl == frame->last) {
-            break;
-        }
-
-        cl = ln;
-    }
-
-    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, h2c->connection->log, 0,
-                   "http2:%ui PUSH_PROMISE frame %p was sent",
-                   stream->node->id, frame);
-
-    stream->request->header_size += NGX_HTTP_V2_FRAME_HEADER_SIZE
-                                    + frame->length;
-
-    h2c->payload_bytes += frame->length;
-
-    ngx_http_v2_handle_frame(stream, frame);
-
-    ngx_http_v2_handle_stream(h2c, stream);
-
-    return NGX_OK;
-}
-
-
 static ngx_int_t
 ngx_http_v2_data_frame_handler(ngx_http_v2_connection_t *h2c,
     ngx_http_v2_out_frame_t *frame)
diff --git a/src/http/v2/ngx_http_v2_module.c b/src/http/v2/ngx_http_v2_module.c
index 0050886..d64488c 100644
--- a/src/http/v2/ngx_http_v2_module.c
+++ b/src/http/v2/ngx_http_v2_module.c
@@ -27,8 +27,6 @@ static void *ngx_http_v2_create_loc_conf(ngx_conf_t *cf);
 static char *ngx_http_v2_merge_loc_conf(ngx_conf_t *cf, void *parent,
     void *child);
 
-static char *ngx_http_v2_push(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
-
 static char *ngx_http_v2_recv_buffer_size(ngx_conf_t *cf, void *post,
     void *data);
 static char *ngx_http_v2_pool_size(ngx_conf_t *cf, void *post, void *data);
@@ -75,6 +73,13 @@ static ngx_conf_post_t  ngx_http_v2_chunk_size_post =
 
 static ngx_command_t  ngx_http_v2_commands[] = {
 
+    { ngx_string("http2"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_FLAG,
+      ngx_conf_set_flag_slot,
+      NGX_HTTP_SRV_CONF_OFFSET,
+      offsetof(ngx_http_v2_srv_conf_t, enable),
+      NULL },
+
     { ngx_string("http2_recv_buffer_size"),
       NGX_HTTP_MAIN_CONF|NGX_CONF_TAKE1,
       ngx_conf_set_size_slot,
@@ -98,9 +103,9 @@ static ngx_command_t  ngx_http_v2_commands[] = {
 
     { ngx_string("http2_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_v2_srv_conf_t, concurrent_pushes),
+      ngx_http_v2_obsolete,
+      0,
+      0,
       NULL },
 
     { ngx_string("http2_max_requests"),
@@ -161,15 +166,15 @@ static ngx_command_t  ngx_http_v2_commands[] = {
 
     { ngx_string("http2_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_v2_loc_conf_t, push_preload),
+      ngx_http_v2_obsolete,
+      0,
+      0,
       NULL },
 
     { ngx_string("http2_push"),
       NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
-      ngx_http_v2_push,
-      NGX_HTTP_LOC_CONF_OFFSET,
+      ngx_http_v2_obsolete,
+      0,
       0,
       NULL },
 
@@ -314,10 +319,11 @@ ngx_http_v2_create_srv_conf(ngx_conf_t *cf)
         return NULL;
     }
 
+    h2scf->enable = NGX_CONF_UNSET;
+
     h2scf->pool_size = NGX_CONF_UNSET_SIZE;
 
     h2scf->concurrent_streams = NGX_CONF_UNSET_UINT;
-    h2scf->concurrent_pushes = NGX_CONF_UNSET_UINT;
 
     h2scf->preread_size = NGX_CONF_UNSET_SIZE;
 
@@ -333,12 +339,12 @@ ngx_http_v2_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child)
     ngx_http_v2_srv_conf_t *prev = parent;
     ngx_http_v2_srv_conf_t *conf = child;
 
+    ngx_conf_merge_value(conf->enable, prev->enable, 0);
+
     ngx_conf_merge_size_value(conf->pool_size, prev->pool_size, 4096);
 
     ngx_conf_merge_uint_value(conf->concurrent_streams,
                               prev->concurrent_streams, 128);
-    ngx_conf_merge_uint_value(conf->concurrent_pushes,
-                              prev->concurrent_pushes, 10);
 
     ngx_conf_merge_size_value(conf->preread_size, prev->preread_size, 65536);
 
@@ -359,17 +365,8 @@ ngx_http_v2_create_loc_conf(ngx_conf_t *cf)
         return NULL;
     }
 
-    /*
-     * set by ngx_pcalloc():
-     *
-     *     h2lcf->pushes = NULL;
-     */
-
     h2lcf->chunk_size = NGX_CONF_UNSET_SIZE;
 
-    h2lcf->push_preload = NGX_CONF_UNSET;
-    h2lcf->push = NGX_CONF_UNSET;
-
     return h2lcf;
 }
 
@@ -382,72 +379,6 @@ ngx_http_v2_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child)
 
     ngx_conf_merge_size_value(conf->chunk_size, prev->chunk_size, 8 * 1024);
 
-    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_v2_push(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
-{
-    ngx_http_v2_loc_conf_t *h2lcf = 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 (h2lcf->pushes) {
-            return "\"off\" parameter cannot be used with URI";
-        }
-
-        if (h2lcf->push == 0) {
-            return "is duplicate";
-        }
-
-        h2lcf->push = 0;
-        return NGX_CONF_OK;
-    }
-
-    if (h2lcf->push == 0) {
-        return "URI cannot be used with \"off\" parameter";
-    }
-
-    h2lcf->push = 1;
-
-    if (h2lcf->pushes == NULL) {
-        h2lcf->pushes = ngx_array_create(cf->pool, 1,
-                                         sizeof(ngx_http_complex_value_t));
-        if (h2lcf->pushes == NULL) {
-            return NGX_CONF_ERROR;
-        }
-    }
-
-    cv = ngx_array_push(h2lcf->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;
 }
 
@@ -457,7 +388,7 @@ ngx_http_v2_recv_buffer_size(ngx_conf_t *cf, void *post, void *data)
 {
     size_t *sp = data;
 
-    if (*sp <= 2 * NGX_HTTP_V2_STATE_BUFFER_SIZE) {
+    if (*sp <= NGX_HTTP_V2_STATE_BUFFER_SIZE) {
         return "value is too small";
     }
 
@@ -551,10 +482,17 @@ ngx_http_v2_obsolete(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
 {
     ngx_conf_deprecated_t  *d = cmd->post;
 
-    ngx_conf_log_error(NGX_LOG_WARN, cf, 0,
-                       "the \"%s\" directive is obsolete, "
-                       "use the \"%s\" directive instead",
-                       d->old_name, d->new_name);
+    if (d) {
+        ngx_conf_log_error(NGX_LOG_WARN, cf, 0,
+                           "the \"%s\" directive is obsolete, "
+                           "use the \"%s\" directive instead",
+                           d->old_name, d->new_name);
+
+    } else {
+        ngx_conf_log_error(NGX_LOG_WARN, cf, 0,
+                           "the \"%V\" directive is obsolete, ignored",
+                           &cmd->name);
+    }
 
     return NGX_CONF_OK;
 }
diff --git a/src/http/v2/ngx_http_v2_module.h b/src/http/v2/ngx_http_v2_module.h
index ca4a0bf..07a595c 100644
--- a/src/http/v2/ngx_http_v2_module.h
+++ b/src/http/v2/ngx_http_v2_module.h
@@ -20,26 +20,9 @@ typedef struct {
 } ngx_http_v2_main_conf_t;
 
 
-typedef struct {
-    size_t                          pool_size;
-    ngx_uint_t                      concurrent_streams;
-    ngx_uint_t                      concurrent_pushes;
-    size_t                          preread_size;
-    ngx_uint_t                      streams_index_mask;
-} ngx_http_v2_srv_conf_t;
-
-
 typedef struct {
     size_t                          chunk_size;
-
-    ngx_flag_t                      push_preload;
-
-    ngx_flag_t                      push;
-    ngx_array_t                    *pushes;
 } ngx_http_v2_loc_conf_t;
 
 
-extern ngx_module_t  ngx_http_v2_module;
-
-
 #endif /* _NGX_HTTP_V2_MODULE_H_INCLUDED_ */
diff --git a/src/http/v3/ngx_http_v3.c b/src/http/v3/ngx_http_v3.c
new file mode 100644
index 0000000..8db229b
--- /dev/null
+++ b/src/http/v3/ngx_http_v3.c
@@ -0,0 +1,111 @@
+
+/*
+ * Copyright (C) Roman Arutyunyan
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_http.h>
+
+
+static void ngx_http_v3_keepalive_handler(ngx_event_t *ev);
+static void ngx_http_v3_cleanup_session(void *data);
+
+
+ngx_int_t
+ngx_http_v3_init_session(ngx_connection_t *c)
+{
+    ngx_pool_cleanup_t     *cln;
+    ngx_http_connection_t  *hc;
+    ngx_http_v3_session_t  *h3c;
+
+    hc = c->data;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 init session");
+
+    h3c = ngx_pcalloc(c->pool, sizeof(ngx_http_v3_session_t));
+    if (h3c == NULL) {
+        goto failed;
+    }
+
+    h3c->http_connection = hc;
+
+    ngx_queue_init(&h3c->blocked);
+
+    h3c->keepalive.log = c->log;
+    h3c->keepalive.data = c;
+    h3c->keepalive.handler = ngx_http_v3_keepalive_handler;
+
+    h3c->table.send_insert_count.log = c->log;
+    h3c->table.send_insert_count.data = c;
+    h3c->table.send_insert_count.handler = ngx_http_v3_inc_insert_count_handler;
+
+    cln = ngx_pool_cleanup_add(c->pool, 0);
+    if (cln == NULL) {
+        goto failed;
+    }
+
+    cln->handler = ngx_http_v3_cleanup_session;
+    cln->data = h3c;
+
+    c->data = h3c;
+
+    return NGX_OK;
+
+failed:
+
+    ngx_log_error(NGX_LOG_ERR, c->log, 0, "failed to create http3 session");
+    return NGX_ERROR;
+}
+
+
+static void
+ngx_http_v3_keepalive_handler(ngx_event_t *ev)
+{
+    ngx_connection_t  *c;
+
+    c = ev->data;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 keepalive handler");
+
+    ngx_http_v3_finalize_connection(c, NGX_HTTP_V3_ERR_NO_ERROR,
+                                    "keepalive timeout");
+}
+
+
+static void
+ngx_http_v3_cleanup_session(void *data)
+{
+    ngx_http_v3_session_t  *h3c = data;
+
+    ngx_http_v3_cleanup_table(h3c);
+
+    if (h3c->keepalive.timer_set) {
+        ngx_del_timer(&h3c->keepalive);
+    }
+
+    if (h3c->table.send_insert_count.posted) {
+        ngx_delete_posted_event(&h3c->table.send_insert_count);
+    }
+}
+
+
+ngx_int_t
+ngx_http_v3_check_flood(ngx_connection_t *c)
+{
+    ngx_http_v3_session_t  *h3c;
+
+    h3c = ngx_http_v3_get_session(c);
+
+    if (h3c->total_bytes / 8 > h3c->payload_bytes + 1048576) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0, "http3 flood detected");
+
+        ngx_http_v3_finalize_connection(c, NGX_HTTP_V3_ERR_NO_ERROR,
+                                        "HTTP/3 flood detected");
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}
diff --git a/src/http/v3/ngx_http_v3.h b/src/http/v3/ngx_http_v3.h
new file mode 100644
index 0000000..9dcb5e6
--- /dev/null
+++ b/src/http/v3/ngx_http_v3.h
@@ -0,0 +1,160 @@
+
+/*
+ * 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>
+#include <ngx_http_v3_encode.h>
+#include <ngx_http_v3_uni.h>
+#include <ngx_http_v3_table.h>
+
+
+#define NGX_HTTP_V3_ALPN_PROTO                     "\x02h3"
+#define NGX_HTTP_V3_HQ_ALPN_PROTO                  "\x0Ahq-interop"
+#define NGX_HTTP_V3_HQ_PROTO                       "hq-interop"
+
+#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_FIELD_SECTION_SIZE   0x06
+#define NGX_HTTP_V3_PARAM_BLOCKED_STREAMS          0x07
+
+#define NGX_HTTP_V3_MAX_TABLE_CAPACITY             4096
+
+#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_MAX_UNI_STREAMS                3
+
+/* 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_session(c)                                            \
+    ((ngx_http_v3_session_t *) ((c)->quic ? (c)->quic->parent->data           \
+                                          : (c)->data))
+
+#define ngx_http_quic_get_connection(c)                                       \
+    (ngx_http_v3_get_session(c)->http_connection)
+
+#define ngx_http_v3_get_module_loc_conf(c, module)                            \
+    ngx_http_get_module_loc_conf(ngx_http_quic_get_connection(c)->conf_ctx,   \
+                                 module)
+
+#define ngx_http_v3_get_module_srv_conf(c, module)                            \
+    ngx_http_get_module_srv_conf(ngx_http_quic_get_connection(c)->conf_ctx,   \
+                                 module)
+
+#define ngx_http_v3_finalize_connection(c, code, reason)                      \
+    ngx_quic_finalize_connection((c)->quic ? (c)->quic->parent : (c),         \
+                                 code, reason)
+
+#define ngx_http_v3_shutdown_connection(c, code, reason)                      \
+    ngx_quic_shutdown_connection((c)->quic ? (c)->quic->parent : (c),         \
+                                 code, reason)
+
+
+typedef struct {
+    ngx_flag_t                    enable;
+    ngx_flag_t                    enable_hq;
+    size_t                        max_table_capacity;
+    ngx_uint_t                    max_blocked_streams;
+    ngx_uint_t                    max_concurrent_streams;
+    ngx_quic_conf_t               quic;
+} ngx_http_v3_srv_conf_t;
+
+
+struct ngx_http_v3_parse_s {
+    size_t                        header_limit;
+    ngx_http_v3_parse_headers_t   headers;
+    ngx_http_v3_parse_data_t      body;
+    ngx_array_t                  *cookies;
+};
+
+
+struct ngx_http_v3_session_s {
+    ngx_http_connection_t        *http_connection;
+
+    ngx_http_v3_dynamic_table_t   table;
+
+    ngx_event_t                   keepalive;
+    ngx_uint_t                    nrequests;
+
+    ngx_queue_t                   blocked;
+    ngx_uint_t                    nblocked;
+
+    uint64_t                      next_request_id;
+
+    off_t                         total_bytes;
+    off_t                         payload_bytes;
+
+    unsigned                      goaway:1;
+    unsigned                      hq:1;
+
+    ngx_connection_t             *known_streams[NGX_HTTP_V3_MAX_KNOWN_STREAM];
+};
+
+
+void ngx_http_v3_init_stream(ngx_connection_t *c);
+void ngx_http_v3_reset_stream(ngx_connection_t *c);
+ngx_int_t ngx_http_v3_init_session(ngx_connection_t *c);
+ngx_int_t ngx_http_v3_check_flood(ngx_connection_t *c);
+ngx_int_t ngx_http_v3_init(ngx_connection_t *c);
+void ngx_http_v3_shutdown(ngx_connection_t *c);
+
+ngx_int_t ngx_http_v3_read_request_body(ngx_http_request_t *r);
+ngx_int_t ngx_http_v3_read_unbuffered_request_body(ngx_http_request_t *r);
+
+
+extern ngx_module_t  ngx_http_v3_module;
+
+
+#endif /* _NGX_HTTP_V3_H_INCLUDED_ */
diff --git a/src/http/v3/ngx_http_v3_encode.c b/src/http/v3/ngx_http_v3_encode.c
new file mode 100644
index 0000000..fb089c4
--- /dev/null
+++ b/src/http/v3/ngx_http_v3_encode.c
@@ -0,0 +1,304 @@
+
+/*
+ * 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_field_section_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_field_ri(u_char *p, ngx_uint_t dynamic, ngx_uint_t index)
+{
+    /* Indexed Field Line */
+
+    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_field_lri(u_char *p, ngx_uint_t dynamic, ngx_uint_t index,
+    u_char *data, size_t len)
+{
+    size_t   hlen;
+    u_char  *p1, *p2;
+
+    /* Literal Field Line 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);
+
+    p1 = p;
+    *p = 0;
+    p = (u_char *) ngx_http_v3_encode_prefix_int(p, len, 7);
+
+    if (data) {
+        p2 = p;
+        hlen = ngx_http_huff_encode(data, len, p, 0);
+
+        if (hlen) {
+            p = p1;
+            *p = 0x80;
+            p = (u_char *) ngx_http_v3_encode_prefix_int(p, hlen, 7);
+
+            if (p != p2) {
+                ngx_memmove(p, p2, hlen);
+            }
+
+            p += hlen;
+
+        } else {
+            p = ngx_cpymem(p, data, len);
+        }
+    }
+
+    return (uintptr_t) p;
+}
+
+
+uintptr_t
+ngx_http_v3_encode_field_l(u_char *p, ngx_str_t *name, ngx_str_t *value)
+{
+    size_t   hlen;
+    u_char  *p1, *p2;
+
+    /* Literal Field Line With Literal Name */
+
+    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;
+    }
+
+    p1 = p;
+    *p = 0x20;
+    p = (u_char *) ngx_http_v3_encode_prefix_int(p, name->len, 3);
+
+    p2 = p;
+    hlen = ngx_http_huff_encode(name->data, name->len, p, 1);
+
+    if (hlen) {
+        p = p1;
+        *p = 0x28;
+        p = (u_char *) ngx_http_v3_encode_prefix_int(p, hlen, 3);
+
+        if (p != p2) {
+            ngx_memmove(p, p2, hlen);
+        }
+
+        p += hlen;
+
+    } else {
+        ngx_strlow(p, name->data, name->len);
+        p += name->len;
+    }
+
+    p1 = p;
+    *p = 0;
+    p = (u_char *) ngx_http_v3_encode_prefix_int(p, value->len, 7);
+
+    p2 = p;
+    hlen = ngx_http_huff_encode(value->data, value->len, p, 0);
+
+    if (hlen) {
+        p = p1;
+        *p = 0x80;
+        p = (u_char *) ngx_http_v3_encode_prefix_int(p, hlen, 7);
+
+        if (p != p2) {
+            ngx_memmove(p, p2, hlen);
+        }
+
+        p += hlen;
+
+    } else {
+        p = ngx_cpymem(p, value->data, value->len);
+    }
+
+    return (uintptr_t) p;
+}
+
+
+uintptr_t
+ngx_http_v3_encode_field_pbi(u_char *p, ngx_uint_t index)
+{
+    /* Indexed Field Line 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_field_lpbi(u_char *p, ngx_uint_t index, u_char *data,
+    size_t len)
+{
+    size_t   hlen;
+    u_char  *p1, *p2;
+
+    /* Literal Field Line 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);
+
+    p1 = p;
+    *p = 0;
+    p = (u_char *) ngx_http_v3_encode_prefix_int(p, len, 7);
+
+    if (data) {
+        p2 = p;
+        hlen = ngx_http_huff_encode(data, len, p, 0);
+
+        if (hlen) {
+            p = p1;
+            *p = 0x80;
+            p = (u_char *) ngx_http_v3_encode_prefix_int(p, hlen, 7);
+
+            if (p != p2) {
+                ngx_memmove(p, p2, hlen);
+            }
+
+            p += hlen;
+
+        } else {
+            p = ngx_cpymem(p, data, len);
+        }
+    }
+
+    return (uintptr_t) p;
+}
diff --git a/src/http/v3/ngx_http_v3_encode.h b/src/http/v3/ngx_http_v3_encode.h
new file mode 100644
index 0000000..fca376d
--- /dev/null
+++ b/src/http/v3/ngx_http_v3_encode.h
@@ -0,0 +1,34 @@
+
+/*
+ * Copyright (C) Roman Arutyunyan
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_HTTP_V3_ENCODE_H_INCLUDED_
+#define _NGX_HTTP_V3_ENCODE_H_INCLUDED_
+
+
+#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);
+uintptr_t ngx_http_v3_encode_prefix_int(u_char *p, uint64_t value,
+    ngx_uint_t prefix);
+
+uintptr_t ngx_http_v3_encode_field_section_prefix(u_char *p,
+    ngx_uint_t insert_count, ngx_uint_t sign, ngx_uint_t delta_base);
+uintptr_t ngx_http_v3_encode_field_ri(u_char *p, ngx_uint_t dynamic,
+    ngx_uint_t index);
+uintptr_t ngx_http_v3_encode_field_lri(u_char *p, ngx_uint_t dynamic,
+    ngx_uint_t index, u_char *data, size_t len);
+uintptr_t ngx_http_v3_encode_field_l(u_char *p, ngx_str_t *name,
+    ngx_str_t *value);
+uintptr_t ngx_http_v3_encode_field_pbi(u_char *p, ngx_uint_t index);
+uintptr_t ngx_http_v3_encode_field_lpbi(u_char *p, ngx_uint_t index,
+    u_char *data, size_t len);
+
+
+#endif /* _NGX_HTTP_V3_ENCODE_H_INCLUDED_ */
diff --git a/src/http/v3/ngx_http_v3_filter_module.c b/src/http/v3/ngx_http_v3_filter_module.c
new file mode 100644
index 0000000..4d2276d
--- /dev/null
+++ b/src/http/v3/ngx_http_v3_filter_module.c
@@ -0,0 +1,852 @@
+
+/*
+ * 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
+
+
+typedef struct {
+    ngx_chain_t         *free;
+    ngx_chain_t         *busy;
+} ngx_http_v3_filter_ctx_t;
+
+
+static ngx_int_t ngx_http_v3_header_filter(ngx_http_request_t *r);
+static ngx_int_t ngx_http_v3_body_filter(ngx_http_request_t *r,
+    ngx_chain_t *in);
+static ngx_chain_t *ngx_http_v3_create_trailers(ngx_http_request_t *r,
+    ngx_http_v3_filter_ctx_t *ctx);
+static ngx_int_t ngx_http_v3_filter_init(ngx_conf_t *cf);
+
+
+static ngx_http_module_t  ngx_http_v3_filter_module_ctx = {
+    NULL,                                  /* preconfiguration */
+    ngx_http_v3_filter_init,               /* postconfiguration */
+
+    NULL,                                  /* create main configuration */
+    NULL,                                  /* init main configuration */
+
+    NULL,                                  /* create server configuration */
+    NULL,                                  /* merge server configuration */
+
+    NULL,                                  /* create location configuration */
+    NULL                                   /* merge location configuration */
+};
+
+
+ngx_module_t  ngx_http_v3_filter_module = {
+    NGX_MODULE_V1,
+    &ngx_http_v3_filter_module_ctx,        /* module context */
+    NULL,                                  /* 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_output_header_filter_pt  ngx_http_next_header_filter;
+static ngx_http_output_body_filter_pt    ngx_http_next_body_filter;
+
+
+static ngx_int_t
+ngx_http_v3_header_filter(ngx_http_request_t *r)
+{
+    u_char                    *p;
+    size_t                     len, n;
+    ngx_buf_t                 *b;
+    ngx_str_t                  host, location;
+    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_v3_session_t     *h3c;
+    ngx_http_v3_filter_ctx_t  *ctx;
+    ngx_http_core_loc_conf_t  *clcf;
+    ngx_http_core_srv_conf_t  *cscf;
+    u_char                     addr[NGX_SOCKADDR_STRLEN];
+
+    if (r->http_version != NGX_HTTP_VERSION_30) {
+        return ngx_http_next_header_filter(r);
+    }
+
+    if (r->header_sent) {
+        return NGX_OK;
+    }
+
+    r->header_sent = 1;
+
+    if (r != r->main) {
+        return NGX_OK;
+    }
+
+    h3c = ngx_http_v3_get_session(r->connection);
+
+    if (r->method == NGX_HTTP_HEAD) {
+        r->header_only = 1;
+    }
+
+    if (r->headers_out.last_modified_time != -1) {
+        if (r->headers_out.status != NGX_HTTP_OK
+            && r->headers_out.status != NGX_HTTP_PARTIAL_CONTENT
+            && r->headers_out.status != NGX_HTTP_NOT_MODIFIED)
+        {
+            r->headers_out.last_modified_time = -1;
+            r->headers_out.last_modified = NULL;
+        }
+    }
+
+    if (r->headers_out.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;
+        r->headers_out.content_length = NULL;
+        r->headers_out.content_length_n = -1;
+    }
+
+    if (r->headers_out.status == NGX_HTTP_NOT_MODIFIED) {
+        r->header_only = 1;
+    }
+
+    c = r->connection;
+
+    out = NULL;
+    ll = &out;
+
+    len = ngx_http_v3_encode_field_section_prefix(NULL, 0, 0, 0);
+
+    if (r->headers_out.status == NGX_HTTP_OK) {
+        len += ngx_http_v3_encode_field_ri(NULL, 0,
+                                           NGX_HTTP_V3_HEADER_STATUS_200);
+
+    } else {
+        len += ngx_http_v3_encode_field_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_field_lri(NULL, 0,
+                                            NGX_HTTP_V3_HEADER_SERVER,
+                                            NULL, n);
+    }
+
+    if (r->headers_out.date == NULL) {
+        len += ngx_http_v3_encode_field_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_field_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_field_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_field_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_field_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) {
+
+        if (r->headers_out.location->value.data[0] == '/'
+            && clcf->absolute_redirect)
+        {
+            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 NGX_ERROR;
+                }
+            }
+
+            port = ngx_inet_get_port(c->local_sockaddr);
+
+            location.len = 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) {
+                location.len += sizeof(":65535") - 1;
+            }
+
+            location.data = ngx_pnalloc(r->pool, location.len);
+            if (location.data == NULL) {
+                return NGX_ERROR;
+            }
+
+            p = ngx_cpymem(location.data, "https://", sizeof("https://") - 1);
+            p = ngx_cpymem(p, host.data, host.len);
+
+            if (port) {
+                p = ngx_sprintf(p, ":%ui", port);
+            }
+
+            p = ngx_cpymem(p, 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 = p - location.data;
+            r->headers_out.location->value.data = location.data;
+            ngx_str_set(&r->headers_out.location->key, "Location");
+        }
+
+        r->headers_out.location->hash = 0;
+
+        len += ngx_http_v3_encode_field_lri(NULL, 0,
+                                           NGX_HTTP_V3_HEADER_LOCATION, NULL,
+                                           r->headers_out.location->value.len);
+    }
+
+#if (NGX_HTTP_GZIP)
+    if (r->gzip_vary) {
+        if (clcf->gzip_vary) {
+            len += ngx_http_v3_encode_field_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_field_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 NGX_ERROR;
+    }
+
+    b->last = (u_char *) ngx_http_v3_encode_field_section_prefix(b->last,
+                                                                 0, 0, 0);
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 output header: \":status: %03ui\"",
+                   r->headers_out.status);
+
+    if (r->headers_out.status == NGX_HTTP_OK) {
+        b->last = (u_char *) ngx_http_v3_encode_field_ri(b->last, 0,
+                                                NGX_HTTP_V3_HEADER_STATUS_200);
+
+    } else {
+        b->last = (u_char *) ngx_http_v3_encode_field_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;
+        }
+
+        ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 output header: \"server: %*s\"", n, p);
+
+        b->last = (u_char *) ngx_http_v3_encode_field_lri(b->last, 0,
+                                                     NGX_HTTP_V3_HEADER_SERVER,
+                                                     p, n);
+    }
+
+    if (r->headers_out.date == NULL) {
+        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 output header: \"date: %V\"",
+                       &ngx_cached_http_time);
+
+        b->last = (u_char *) ngx_http_v3_encode_field_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) {
+        if (r->headers_out.content_type_len == r->headers_out.content_type.len
+            && r->headers_out.charset.len)
+        {
+            n = r->headers_out.content_type.len + sizeof("; charset=") - 1
+                + r->headers_out.charset.len;
+
+            p = ngx_pnalloc(r->pool, n);
+            if (p == NULL) {
+                return NGX_ERROR;
+            }
+
+            p = ngx_cpymem(p, r->headers_out.content_type.data,
+                           r->headers_out.content_type.len);
+
+            p = ngx_cpymem(p, "; charset=", sizeof("; charset=") - 1);
+
+            p = ngx_cpymem(p, r->headers_out.charset.data,
+                           r->headers_out.charset.len);
+
+            /* updated r->headers_out.content_type is also needed for logging */
+
+            r->headers_out.content_type.len = n;
+            r->headers_out.content_type.data = p - n;
+        }
+
+        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 output header: \"content-type: %V\"",
+                       &r->headers_out.content_type);
+
+        b->last = (u_char *) ngx_http_v3_encode_field_lri(b->last, 0,
+                                    NGX_HTTP_V3_HEADER_CONTENT_TYPE_TEXT_PLAIN,
+                                    r->headers_out.content_type.data,
+                                    r->headers_out.content_type.len);
+    }
+
+    if (r->headers_out.content_length == NULL
+        && r->headers_out.content_length_n >= 0)
+    {
+        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 output header: \"content-length: %O\"",
+                       r->headers_out.content_length_n);
+
+        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_field_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 {
+            b->last = (u_char *) ngx_http_v3_encode_field_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)
+    {
+        n = sizeof("Mon, 28 Sep 1970 06:00:00 GMT") - 1;
+
+        p = ngx_pnalloc(r->pool, n);
+        if (p == NULL) {
+            return NGX_ERROR;
+        }
+
+        ngx_http_time(p, r->headers_out.last_modified_time);
+
+        ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 output header: \"last-modified: %*s\"", n, p);
+
+        b->last = (u_char *) ngx_http_v3_encode_field_lri(b->last, 0,
+                                              NGX_HTTP_V3_HEADER_LAST_MODIFIED,
+                                              p, n);
+    }
+
+    if (r->headers_out.location && r->headers_out.location->value.len) {
+        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 output header: \"location: %V\"",
+                       &r->headers_out.location->value);
+
+        b->last = (u_char *) ngx_http_v3_encode_field_lri(b->last, 0,
+                                           NGX_HTTP_V3_HEADER_LOCATION,
+                                           r->headers_out.location->value.data,
+                                           r->headers_out.location->value.len);
+    }
+
+#if (NGX_HTTP_GZIP)
+    if (r->gzip_vary) {
+        ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 output header: \"vary: Accept-Encoding\"");
+
+        b->last = (u_char *) ngx_http_v3_encode_field_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;
+        }
+
+        ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 output header: \"%V: %V\"",
+                       &header[i].key, &header[i].value);
+
+        b->last = (u_char *) ngx_http_v3_encode_field_l(b->last,
+                                                        &header[i].key,
+                                                        &header[i].value);
+    }
+
+    if (r->header_only) {
+        b->last_buf = 1;
+    }
+
+    cl = ngx_alloc_chain_link(r->pool);
+    if (cl == NULL) {
+        return NGX_ERROR;
+    }
+
+    cl->buf = b;
+    cl->next = NULL;
+
+    n = b->last - b->pos;
+
+    h3c->payload_bytes += n;
+
+    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(r->pool, len);
+    if (b == NULL) {
+        return NGX_ERROR;
+    }
+
+    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(r->pool);
+    if (hl == NULL) {
+        return NGX_ERROR;
+    }
+
+    hl->buf = b;
+    hl->next = cl;
+
+    *ll = hl;
+    ll = &cl->next;
+
+    if (r->headers_out.content_length_n >= 0
+        && !r->header_only && !r->expect_trailers)
+    {
+        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(r->pool, len);
+        if (b == NULL) {
+            return NGX_ERROR;
+        }
+
+        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);
+
+        h3c->payload_bytes += r->headers_out.content_length_n;
+        h3c->total_bytes += r->headers_out.content_length_n;
+
+        cl = ngx_alloc_chain_link(r->pool);
+        if (cl == NULL) {
+            return NGX_ERROR;
+        }
+
+        cl->buf = b;
+        cl->next = NULL;
+
+        *ll = cl;
+
+    } else {
+        ctx = ngx_pcalloc(r->pool, sizeof(ngx_http_v3_filter_ctx_t));
+        if (ctx == NULL) {
+            return NGX_ERROR;
+        }
+
+        ngx_http_set_ctx(r, ctx, ngx_http_v3_filter_module);
+    }
+
+    for (cl = out; cl; cl = cl->next) {
+        h3c->total_bytes += cl->buf->last - cl->buf->pos;
+        r->header_size += cl->buf->last - cl->buf->pos;
+    }
+
+    return ngx_http_write_filter(r, out);
+}
+
+
+static ngx_int_t
+ngx_http_v3_body_filter(ngx_http_request_t *r, ngx_chain_t *in)
+{
+    u_char                    *chunk;
+    off_t                      size;
+    ngx_int_t                  rc;
+    ngx_buf_t                 *b;
+    ngx_chain_t               *out, *cl, *tl, **ll;
+    ngx_http_v3_session_t     *h3c;
+    ngx_http_v3_filter_ctx_t  *ctx;
+
+    if (in == NULL) {
+        return ngx_http_next_body_filter(r, in);
+    }
+
+    ctx = ngx_http_get_module_ctx(r, ngx_http_v3_filter_module);
+    if (ctx == NULL) {
+        return ngx_http_next_body_filter(r, in);
+    }
+
+    h3c = ngx_http_v3_get_session(r->connection);
+
+    out = NULL;
+    ll = &out;
+
+    size = 0;
+    cl = in;
+
+    for ( ;; ) {
+        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
+                       "http3 chunk: %O", ngx_buf_size(cl->buf));
+
+        size += ngx_buf_size(cl->buf);
+
+        if (cl->buf->flush
+            || cl->buf->sync
+            || ngx_buf_in_memory(cl->buf)
+            || cl->buf->in_file)
+        {
+            tl = ngx_alloc_chain_link(r->pool);
+            if (tl == NULL) {
+                return NGX_ERROR;
+            }
+
+            tl->buf = cl->buf;
+            *ll = tl;
+            ll = &tl->next;
+        }
+
+        if (cl->next == NULL) {
+            break;
+        }
+
+        cl = cl->next;
+    }
+
+    if (size) {
+        tl = ngx_chain_get_free_buf(r->pool, &ctx->free);
+        if (tl == NULL) {
+            return NGX_ERROR;
+        }
+
+        b = tl->buf;
+        chunk = b->start;
+
+        if (chunk == NULL) {
+            chunk = ngx_palloc(r->pool, NGX_HTTP_V3_VARLEN_INT_LEN * 2);
+            if (chunk == NULL) {
+                return NGX_ERROR;
+            }
+
+            b->start = chunk;
+            b->end = chunk + NGX_HTTP_V3_VARLEN_INT_LEN * 2;
+        }
+
+        b->tag = (ngx_buf_tag_t) &ngx_http_v3_filter_module;
+        b->memory = 0;
+        b->temporary = 1;
+        b->pos = chunk;
+
+        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);
+
+        tl->next = out;
+        out = tl;
+
+        h3c->payload_bytes += size;
+    }
+
+    if (cl->buf->last_buf) {
+        tl = ngx_http_v3_create_trailers(r, ctx);
+        if (tl == NULL) {
+            return NGX_ERROR;
+        }
+
+        cl->buf->last_buf = 0;
+
+        *ll = tl;
+
+    } else {
+        *ll = NULL;
+    }
+
+    for (cl = out; cl; cl = cl->next) {
+        h3c->total_bytes += cl->buf->last - cl->buf->pos;
+    }
+
+    rc = ngx_http_next_body_filter(r, out);
+
+    ngx_chain_update_chains(r->pool, &ctx->free, &ctx->busy, &out,
+                            (ngx_buf_tag_t) &ngx_http_v3_filter_module);
+
+    return rc;
+}
+
+
+static ngx_chain_t *
+ngx_http_v3_create_trailers(ngx_http_request_t *r,
+    ngx_http_v3_filter_ctx_t *ctx)
+{
+    size_t                  len, n;
+    u_char                 *p;
+    ngx_buf_t              *b;
+    ngx_uint_t              i;
+    ngx_chain_t            *cl, *hl;
+    ngx_list_part_t        *part;
+    ngx_table_elt_t        *header;
+    ngx_http_v3_session_t  *h3c;
+
+    h3c = ngx_http_v3_get_session(r->connection);
+
+    len = 0;
+
+    part = &r->headers_out.trailers.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_field_l(NULL, &header[i].key,
+                                          &header[i].value);
+    }
+
+    cl = ngx_chain_get_free_buf(r->pool, &ctx->free);
+    if (cl == NULL) {
+        return NULL;
+    }
+
+    b = cl->buf;
+
+    b->tag = (ngx_buf_tag_t) &ngx_http_v3_filter_module;
+    b->memory = 0;
+    b->last_buf = 1;
+
+    if (len == 0) {
+        b->temporary = 0;
+        b->pos = b->last = NULL;
+        return cl;
+    }
+
+    b->temporary = 1;
+
+    len += ngx_http_v3_encode_field_section_prefix(NULL, 0, 0, 0);
+
+    b->pos = ngx_palloc(r->pool, len);
+    if (b->pos == NULL) {
+        return NULL;
+    }
+
+    b->last = (u_char *) ngx_http_v3_encode_field_section_prefix(b->pos,
+                                                                 0, 0, 0);
+
+    part = &r->headers_out.trailers.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;
+        }
+
+        ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
+                       "http3 output trailer: \"%V: %V\"",
+                       &header[i].key, &header[i].value);
+
+        b->last = (u_char *) ngx_http_v3_encode_field_l(b->last,
+                                                        &header[i].key,
+                                                        &header[i].value);
+    }
+
+    n = b->last - b->pos;
+
+    h3c->payload_bytes += n;
+
+    hl = ngx_chain_get_free_buf(r->pool, &ctx->free);
+    if (hl == NULL) {
+        return NULL;
+    }
+
+    b = hl->buf;
+    p = b->start;
+
+    if (p == NULL) {
+        p = ngx_palloc(r->pool, NGX_HTTP_V3_VARLEN_INT_LEN * 2);
+        if (p == NULL) {
+            return NULL;
+        }
+
+        b->start = p;
+        b->end = p + NGX_HTTP_V3_VARLEN_INT_LEN * 2;
+    }
+
+    b->tag = (ngx_buf_tag_t) &ngx_http_v3_filter_module;
+    b->memory = 0;
+    b->temporary = 1;
+    b->pos = p;
+
+    b->last = (u_char *) ngx_http_v3_encode_varlen_int(p,
+                                                    NGX_HTTP_V3_FRAME_HEADERS);
+    b->last = (u_char *) ngx_http_v3_encode_varlen_int(b->last, n);
+
+    hl->next = cl;
+
+    return hl;
+}
+
+
+static ngx_int_t
+ngx_http_v3_filter_init(ngx_conf_t *cf)
+{
+    ngx_http_next_header_filter = ngx_http_top_header_filter;
+    ngx_http_top_header_filter = ngx_http_v3_header_filter;
+
+    ngx_http_next_body_filter = ngx_http_top_body_filter;
+    ngx_http_top_body_filter = ngx_http_v3_body_filter;
+
+    return NGX_OK;
+}
diff --git a/src/http/v3/ngx_http_v3_module.c b/src/http/v3/ngx_http_v3_module.c
new file mode 100644
index 0000000..139bd65
--- /dev/null
+++ b/src/http/v3/ngx_http_v3_module.c
@@ -0,0 +1,393 @@
+
+/*
+ * 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_v3_variable(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 char *ngx_http_quic_host_key(ngx_conf_t *cf, ngx_command_t *cmd,
+    void *conf);
+
+
+static ngx_command_t  ngx_http_v3_commands[] = {
+
+    { ngx_string("http3"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_FLAG,
+      ngx_conf_set_flag_slot,
+      NGX_HTTP_SRV_CONF_OFFSET,
+      offsetof(ngx_http_v3_srv_conf_t, enable),
+      NULL },
+
+    { ngx_string("http3_hq"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_FLAG,
+      ngx_conf_set_flag_slot,
+      NGX_HTTP_SRV_CONF_OFFSET,
+      offsetof(ngx_http_v3_srv_conf_t, enable_hq),
+      NULL },
+
+    { ngx_string("http3_max_concurrent_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_concurrent_streams),
+      NULL },
+
+    { ngx_string("http3_stream_buffer_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, quic.stream_buffer_size),
+      NULL },
+
+    { 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_http_v3_srv_conf_t, quic.retry),
+      NULL },
+
+    { ngx_string("quic_gso"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_FLAG,
+      ngx_conf_set_flag_slot,
+      NGX_HTTP_SRV_CONF_OFFSET,
+      offsetof(ngx_http_v3_srv_conf_t, quic.gso_enabled),
+      NULL },
+
+    { ngx_string("quic_host_key"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1,
+      ngx_http_quic_host_key,
+      NGX_HTTP_SRV_CONF_OFFSET,
+      0,
+      NULL },
+
+    { ngx_string("quic_active_connection_id_limit"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1,
+      ngx_conf_set_num_slot,
+      NGX_HTTP_SRV_CONF_OFFSET,
+      offsetof(ngx_http_v3_srv_conf_t, quic.active_connection_id_limit),
+      NULL },
+
+      ngx_null_command
+};
+
+
+static ngx_http_module_t  ngx_http_v3_module_ctx = {
+    ngx_http_v3_add_variables,             /* preconfiguration */
+    NULL,                                  /* postconfiguration */
+
+    NULL,                                  /* create main configuration */
+    NULL,                                  /* init main configuration */
+
+    ngx_http_v3_create_srv_conf,           /* create server configuration */
+    ngx_http_v3_merge_srv_conf,            /* merge server configuration */
+
+    NULL,                                  /* create location configuration */
+    NULL                                   /* merge location configuration */
+};
+
+
+ngx_module_t  ngx_http_v3_module = {
+    NGX_MODULE_V1,
+    &ngx_http_v3_module_ctx,               /* module context */
+    ngx_http_v3_commands,                  /* module directives */
+    NGX_HTTP_MODULE,                       /* module type */
+    NULL,                                  /* init master */
+    NULL,                                  /* init module */
+    NULL,                                  /* init process */
+    NULL,                                  /* init thread */
+    NULL,                                  /* exit thread */
+    NULL,                                  /* exit process */
+    NULL,                                  /* exit master */
+    NGX_MODULE_V1_PADDING
+};
+
+
+static ngx_http_variable_t  ngx_http_v3_vars[] = {
+
+    { ngx_string("http3"), NULL, ngx_http_v3_variable, 0, 0, 0 },
+
+      ngx_http_null_variable
+};
+
+static ngx_str_t  ngx_http_quic_salt = ngx_string("ngx_quic");
+
+
+static ngx_int_t
+ngx_http_v3_variable(ngx_http_request_t *r,
+    ngx_http_variable_value_t *v, uintptr_t data)
+{
+    ngx_http_v3_session_t  *h3c;
+
+    if (r->connection->quic) {
+        h3c = ngx_http_v3_get_session(r->connection);
+
+        if (h3c->hq) {
+            v->len = sizeof("hq") - 1;
+            v->valid = 1;
+            v->no_cacheable = 0;
+            v->not_found = 0;
+            v->data = (u_char *) "hq";
+
+            return NGX_OK;
+        }
+
+        v->len = sizeof("h3") - 1;
+        v->valid = 1;
+        v->no_cacheable = 0;
+        v->not_found = 0;
+        v->data = (u_char *) "h3";
+
+        return NGX_OK;
+    }
+
+    *v = ngx_http_variable_null_value;
+
+    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;
+    }
+
+    /*
+     * set by ngx_pcalloc():
+     *
+     *     h3scf->quic.host_key = { 0, NULL }
+     *     h3scf->quic.stream_reject_code_uni = 0;
+     *     h3scf->quic.disable_active_migration = 0;
+     *     h3scf->quic.idle_timeout = 0;
+     *     h3scf->max_blocked_streams = 0;
+     */
+
+    h3scf->enable = NGX_CONF_UNSET;
+    h3scf->enable_hq = NGX_CONF_UNSET;
+    h3scf->max_table_capacity = NGX_HTTP_V3_MAX_TABLE_CAPACITY;
+    h3scf->max_concurrent_streams = NGX_CONF_UNSET_UINT;
+
+    h3scf->quic.stream_buffer_size = NGX_CONF_UNSET_SIZE;
+    h3scf->quic.max_concurrent_streams_bidi = NGX_CONF_UNSET_UINT;
+    h3scf->quic.max_concurrent_streams_uni = NGX_HTTP_V3_MAX_UNI_STREAMS;
+    h3scf->quic.retry = NGX_CONF_UNSET;
+    h3scf->quic.gso_enabled = NGX_CONF_UNSET;
+    h3scf->quic.stream_close_code = NGX_HTTP_V3_ERR_NO_ERROR;
+    h3scf->quic.stream_reject_code_bidi = NGX_HTTP_V3_ERR_REQUEST_REJECTED;
+    h3scf->quic.active_connection_id_limit = NGX_CONF_UNSET_UINT;
+
+    h3scf->quic.init = ngx_http_v3_init;
+    h3scf->quic.shutdown = ngx_http_v3_shutdown;
+
+    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_http_ssl_srv_conf_t   *sscf;
+    ngx_http_core_srv_conf_t  *cscf;
+
+    ngx_conf_merge_value(conf->enable, prev->enable, 1);
+
+    ngx_conf_merge_value(conf->enable_hq, prev->enable_hq, 0);
+
+    ngx_conf_merge_uint_value(conf->max_concurrent_streams,
+                              prev->max_concurrent_streams, 128);
+
+    conf->max_blocked_streams = conf->max_concurrent_streams;
+
+    ngx_conf_merge_size_value(conf->quic.stream_buffer_size,
+                              prev->quic.stream_buffer_size,
+                              65536);
+
+    conf->quic.max_concurrent_streams_bidi = conf->max_concurrent_streams;
+
+    ngx_conf_merge_value(conf->quic.retry, prev->quic.retry, 0);
+    ngx_conf_merge_value(conf->quic.gso_enabled, prev->quic.gso_enabled, 0);
+
+    ngx_conf_merge_str_value(conf->quic.host_key, prev->quic.host_key, "");
+
+    ngx_conf_merge_uint_value(conf->quic.active_connection_id_limit,
+                              prev->quic.active_connection_id_limit,
+                              2);
+
+    if (conf->quic.host_key.len == 0) {
+
+        conf->quic.host_key.len = NGX_QUIC_DEFAULT_HOST_KEY_LEN;
+        conf->quic.host_key.data = ngx_palloc(cf->pool,
+                                              conf->quic.host_key.len);
+        if (conf->quic.host_key.data == NULL) {
+            return NGX_CONF_ERROR;
+        }
+
+        if (RAND_bytes(conf->quic.host_key.data, NGX_QUIC_DEFAULT_HOST_KEY_LEN)
+            <= 0)
+        {
+            return NGX_CONF_ERROR;
+        }
+    }
+
+    if (ngx_quic_derive_key(cf->log, "av_token_key",
+                            &conf->quic.host_key, &ngx_http_quic_salt,
+                            conf->quic.av_token_key, NGX_QUIC_AV_KEY_LEN)
+        != NGX_OK)
+    {
+        return NGX_CONF_ERROR;
+    }
+
+    if (ngx_quic_derive_key(cf->log, "sr_token_key",
+                            &conf->quic.host_key, &ngx_http_quic_salt,
+                            conf->quic.sr_token_key, NGX_QUIC_SR_KEY_LEN)
+        != NGX_OK)
+    {
+        return NGX_CONF_ERROR;
+    }
+
+    cscf = ngx_http_conf_get_module_srv_conf(cf, ngx_http_core_module);
+    conf->quic.handshake_timeout = cscf->client_header_timeout;
+
+    sscf = ngx_http_conf_get_module_srv_conf(cf, ngx_http_ssl_module);
+    conf->quic.ssl = &sscf->ssl;
+
+    return NGX_CONF_OK;
+}
+
+
+static char *
+ngx_http_quic_host_key(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
+{
+    ngx_http_v3_srv_conf_t  *h3scf = conf;
+
+    u_char           *buf;
+    size_t            size;
+    ssize_t           n;
+    ngx_str_t        *value;
+    ngx_file_t        file;
+    ngx_file_info_t   fi;
+    ngx_quic_conf_t  *qcf;
+
+    qcf = &h3scf->quic;
+
+    if (qcf->host_key.len) {
+        return "is duplicate";
+    }
+
+    buf = NULL;
+#if (NGX_SUPPRESS_WARN)
+    size = 0;
+#endif
+
+    value = cf->args->elts;
+
+    if (ngx_conf_full_name(cf->cycle, &value[1], 1) != NGX_OK) {
+        return NGX_CONF_ERROR;
+    }
+
+    ngx_memzero(&file, sizeof(ngx_file_t));
+    file.name = value[1];
+    file.log = cf->log;
+
+    file.fd = ngx_open_file(file.name.data, NGX_FILE_RDONLY, NGX_FILE_OPEN, 0);
+
+    if (file.fd == NGX_INVALID_FILE) {
+        ngx_conf_log_error(NGX_LOG_EMERG, cf, ngx_errno,
+                           ngx_open_file_n " \"%V\" failed", &file.name);
+        return NGX_CONF_ERROR;
+    }
+
+    if (ngx_fd_info(file.fd, &fi) == NGX_FILE_ERROR) {
+        ngx_conf_log_error(NGX_LOG_CRIT, cf, ngx_errno,
+                           ngx_fd_info_n " \"%V\" failed", &file.name);
+        goto failed;
+    }
+
+    size = ngx_file_size(&fi);
+
+    if (size == 0) {
+        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
+                           "\"%V\" zero key size", &file.name);
+        goto failed;
+    }
+
+    buf = ngx_pnalloc(cf->pool, size);
+    if (buf == NULL) {
+        goto failed;
+    }
+
+    n = ngx_read_file(&file, buf, size, 0);
+
+    if (n == NGX_ERROR) {
+        ngx_conf_log_error(NGX_LOG_CRIT, cf, ngx_errno,
+                           ngx_read_file_n " \"%V\" failed", &file.name);
+        goto failed;
+    }
+
+    if ((size_t) n != size) {
+        ngx_conf_log_error(NGX_LOG_CRIT, cf, 0,
+                           ngx_read_file_n " \"%V\" returned only "
+                           "%z bytes instead of %uz", &file.name, n, size);
+        goto failed;
+    }
+
+    qcf->host_key.data = buf;
+    qcf->host_key.len = n;
+
+    if (ngx_close_file(file.fd) == NGX_FILE_ERROR) {
+        ngx_log_error(NGX_LOG_ALERT, cf->log, ngx_errno,
+                      ngx_close_file_n " \"%V\" failed", &file.name);
+    }
+
+    return NGX_CONF_OK;
+
+failed:
+
+    if (ngx_close_file(file.fd) == NGX_FILE_ERROR) {
+        ngx_log_error(NGX_LOG_ALERT, cf->log, ngx_errno,
+                      ngx_close_file_n " \"%V\" failed", &file.name);
+    }
+
+    if (buf) {
+        ngx_explicit_memzero(buf, size);
+    }
+
+    return NGX_CONF_ERROR;
+}
diff --git a/src/http/v3/ngx_http_v3_parse.c b/src/http/v3/ngx_http_v3_parse.c
new file mode 100644
index 0000000..436765c
--- /dev/null
+++ b/src/http/v3/ngx_http_v3_parse.c
@@ -0,0 +1,1936 @@
+
+/*
+ * 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 void ngx_http_v3_parse_start_local(ngx_buf_t *b, ngx_buf_t *loc,
+    ngx_uint_t n);
+static void ngx_http_v3_parse_end_local(ngx_buf_t *b, ngx_buf_t *loc,
+    ngx_uint_t *n);
+static ngx_int_t ngx_http_v3_parse_skip(ngx_buf_t *b, ngx_uint_t *length);
+
+static ngx_int_t ngx_http_v3_parse_varlen_int(ngx_connection_t *c,
+    ngx_http_v3_parse_varlen_int_t *st, ngx_buf_t *b);
+static 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, ngx_buf_t *b);
+
+static ngx_int_t ngx_http_v3_parse_field_section_prefix(ngx_connection_t *c,
+    ngx_http_v3_parse_field_section_prefix_t *st, ngx_buf_t *b);
+static ngx_int_t ngx_http_v3_parse_field_rep(ngx_connection_t *c,
+    ngx_http_v3_parse_field_rep_t *st, ngx_uint_t base, ngx_buf_t *b);
+static ngx_int_t ngx_http_v3_parse_literal(ngx_connection_t *c,
+    ngx_http_v3_parse_literal_t *st, ngx_buf_t *b);
+static ngx_int_t ngx_http_v3_parse_field_ri(ngx_connection_t *c,
+    ngx_http_v3_parse_field_t *st, ngx_buf_t *b);
+static ngx_int_t ngx_http_v3_parse_field_lri(ngx_connection_t *c,
+    ngx_http_v3_parse_field_t *st, ngx_buf_t *b);
+static ngx_int_t ngx_http_v3_parse_field_l(ngx_connection_t *c,
+    ngx_http_v3_parse_field_t *st, ngx_buf_t *b);
+static ngx_int_t ngx_http_v3_parse_field_pbi(ngx_connection_t *c,
+    ngx_http_v3_parse_field_t *st, ngx_buf_t *b);
+static ngx_int_t ngx_http_v3_parse_field_lpbi(ngx_connection_t *c,
+    ngx_http_v3_parse_field_t *st, ngx_buf_t *b);
+
+static ngx_int_t ngx_http_v3_parse_control(ngx_connection_t *c,
+    ngx_http_v3_parse_control_t *st, ngx_buf_t *b);
+static ngx_int_t ngx_http_v3_parse_settings(ngx_connection_t *c,
+    ngx_http_v3_parse_settings_t *st, ngx_buf_t *b);
+
+static ngx_int_t ngx_http_v3_parse_encoder(ngx_connection_t *c,
+    ngx_http_v3_parse_encoder_t *st, ngx_buf_t *b);
+static ngx_int_t ngx_http_v3_parse_field_inr(ngx_connection_t *c,
+    ngx_http_v3_parse_field_t *st, ngx_buf_t *b);
+static ngx_int_t ngx_http_v3_parse_field_iln(ngx_connection_t *c,
+    ngx_http_v3_parse_field_t *st, ngx_buf_t *b);
+
+static ngx_int_t ngx_http_v3_parse_decoder(ngx_connection_t *c,
+    ngx_http_v3_parse_decoder_t *st, ngx_buf_t *b);
+
+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);
+
+
+static void
+ngx_http_v3_parse_start_local(ngx_buf_t *b, ngx_buf_t *loc, ngx_uint_t n)
+{
+    *loc = *b;
+
+    if ((size_t) (loc->last - loc->pos) > n) {
+        loc->last = loc->pos + n;
+    }
+}
+
+
+static void
+ngx_http_v3_parse_end_local(ngx_buf_t *b, ngx_buf_t *loc, ngx_uint_t *pn)
+{
+    *pn -= loc->pos - b->pos;
+    b->pos = loc->pos;
+}
+
+
+static ngx_int_t
+ngx_http_v3_parse_skip(ngx_buf_t *b, ngx_uint_t *length)
+{
+    if ((size_t) (b->last - b->pos) < *length) {
+        *length -= b->last - b->pos;
+        b->pos = b->last;
+        return NGX_AGAIN;
+    }
+
+    b->pos += *length;
+    return NGX_DONE;
+}
+
+
+static ngx_int_t
+ngx_http_v3_parse_varlen_int(ngx_connection_t *c,
+    ngx_http_v3_parse_varlen_int_t *st, ngx_buf_t *b)
+{
+    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
+    };
+
+    for ( ;; ) {
+
+        if (b->pos == b->last) {
+            return NGX_AGAIN;
+        }
+
+        ch = *b->pos++;
+
+        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;
+        }
+    }
+
+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;
+}
+
+
+static 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, ngx_buf_t *b)
+{
+    u_char      ch;
+    ngx_uint_t  mask;
+    enum {
+        sw_start = 0,
+        sw_value
+    };
+
+    for ( ;; ) {
+
+        if (b->pos == b->last) {
+            return NGX_AGAIN;
+        }
+
+        ch = *b->pos++;
+
+        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;
+        }
+    }
+
+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,
+    ngx_buf_t *b)
+{
+    ngx_buf_t  loc;
+    ngx_int_t  rc;
+    enum {
+        sw_start = 0,
+        sw_type,
+        sw_length,
+        sw_skip,
+        sw_prefix,
+        sw_verify,
+        sw_field_rep,
+        sw_done
+    };
+
+    for ( ;; ) {
+
+        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, b);
+            if (rc != NGX_DONE) {
+                return rc;
+            }
+
+            st->type = st->vlint.value;
+
+            if (ngx_http_v3_is_v2_frame(st->type)
+                || st->type == NGX_HTTP_V3_FRAME_DATA
+                || st->type == NGX_HTTP_V3_FRAME_GOAWAY
+                || st->type == NGX_HTTP_V3_FRAME_SETTINGS
+                || st->type == NGX_HTTP_V3_FRAME_MAX_PUSH_ID
+                || st->type == NGX_HTTP_V3_FRAME_CANCEL_PUSH
+                || st->type == NGX_HTTP_V3_FRAME_PUSH_PROMISE)
+            {
+                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, b);
+            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:
+
+            rc = ngx_http_v3_parse_skip(b, &st->length);
+            if (rc != NGX_DONE) {
+                return rc;
+            }
+
+            st->state = sw_type;
+            break;
+
+        case sw_prefix:
+
+            ngx_http_v3_parse_start_local(b, &loc, st->length);
+
+            rc = ngx_http_v3_parse_field_section_prefix(c, &st->prefix, &loc);
+
+            ngx_http_v3_parse_end_local(b, &loc, &st->length);
+
+            if (st->length == 0 && rc == NGX_AGAIN) {
+                return NGX_HTTP_V3_ERR_FRAME_ERROR;
+            }
+
+            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_field_rep;
+
+            /* fall through */
+
+        case sw_field_rep:
+
+            ngx_http_v3_parse_start_local(b, &loc, st->length);
+
+            rc = ngx_http_v3_parse_field_rep(c, &st->field_rep, st->prefix.base,
+                                             &loc);
+
+            ngx_http_v3_parse_end_local(b, &loc, &st->length);
+
+            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;
+        }
+    }
+
+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_send_ack_section(c, c->quic->id) != NGX_OK) {
+            return NGX_ERROR;
+        }
+
+        ngx_http_v3_ack_insert_count(c, st->prefix.insert_count);
+    }
+
+    st->state = sw_start;
+    return NGX_DONE;
+}
+
+
+static ngx_int_t
+ngx_http_v3_parse_field_section_prefix(ngx_connection_t *c,
+    ngx_http_v3_parse_field_section_prefix_t *st, ngx_buf_t *b)
+{
+    u_char     ch;
+    ngx_int_t  rc;
+    enum {
+        sw_start = 0,
+        sw_req_insert_count,
+        sw_delta_base,
+        sw_read_delta_base
+    };
+
+    for ( ;; ) {
+
+        switch (st->state) {
+
+        case sw_start:
+
+            ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                           "http3 parse field section 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, b);
+            if (rc != NGX_DONE) {
+                return rc;
+            }
+
+            st->insert_count = st->pint.value;
+            st->state = sw_delta_base;
+            break;
+
+        case sw_delta_base:
+
+            if (b->pos == b->last) {
+                return NGX_AGAIN;
+            }
+
+            ch = *b->pos;
+
+            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, b);
+            if (rc != NGX_DONE) {
+                return rc;
+            }
+
+            st->delta_base = st->pint.value;
+            goto done;
+        }
+    }
+
+done:
+
+    rc = ngx_http_v3_decode_insert_count(c, &st->insert_count);
+    if (rc != NGX_OK) {
+        return rc;
+    }
+
+    if (st->sign) {
+        if (st->insert_count <= st->delta_base) {
+            ngx_log_error(NGX_LOG_INFO, c->log, 0, "client sent negative base");
+            return NGX_HTTP_V3_ERR_DECOMPRESSION_FAILED;
+        }
+
+        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 field section 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;
+}
+
+
+static ngx_int_t
+ngx_http_v3_parse_field_rep(ngx_connection_t *c,
+    ngx_http_v3_parse_field_rep_t *st, ngx_uint_t base, ngx_buf_t *b)
+{
+    u_char     ch;
+    ngx_int_t  rc;
+    enum {
+        sw_start = 0,
+        sw_field_ri,
+        sw_field_lri,
+        sw_field_l,
+        sw_field_pbi,
+        sw_field_lpbi
+    };
+
+    if (st->state == sw_start) {
+
+        ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 parse field representation");
+
+        if (b->pos == b->last) {
+            return NGX_AGAIN;
+        }
+
+        ch = *b->pos;
+
+        ngx_memzero(&st->field, sizeof(ngx_http_v3_parse_field_t));
+
+        st->field.base = base;
+
+        if (ch & 0x80) {
+            /* Indexed Field Line */
+
+            st->state = sw_field_ri;
+
+        } else if (ch & 0x40) {
+            /* Literal Field Line With Name Reference */
+
+            st->state = sw_field_lri;
+
+        } else if (ch & 0x20) {
+            /* Literal Field Line With Literal Name */
+
+            st->state = sw_field_l;
+
+        } else if (ch & 0x10) {
+            /* Indexed Field Line With Post-Base Index */
+
+            st->state = sw_field_pbi;
+
+        } else {
+            /* Literal Field Line With Post-Base Name Reference */
+
+            st->state = sw_field_lpbi;
+        }
+    }
+
+    switch (st->state) {
+
+    case sw_field_ri:
+        rc = ngx_http_v3_parse_field_ri(c, &st->field, b);
+        break;
+
+    case sw_field_lri:
+        rc = ngx_http_v3_parse_field_lri(c, &st->field, b);
+        break;
+
+    case sw_field_l:
+        rc = ngx_http_v3_parse_field_l(c, &st->field, b);
+        break;
+
+    case sw_field_pbi:
+        rc = ngx_http_v3_parse_field_pbi(c, &st->field, b);
+        break;
+
+    case sw_field_lpbi:
+        rc = ngx_http_v3_parse_field_lpbi(c, &st->field, b);
+        break;
+
+    default:
+        rc = NGX_OK;
+    }
+
+    if (rc != NGX_DONE) {
+        return rc;
+    }
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 parse field representation done");
+
+    st->state = sw_start;
+    return NGX_DONE;
+}
+
+
+static ngx_int_t
+ngx_http_v3_parse_literal(ngx_connection_t *c, ngx_http_v3_parse_literal_t *st,
+    ngx_buf_t *b)
+{
+    u_char                     ch;
+    ngx_uint_t                 n;
+    ngx_http_core_srv_conf_t  *cscf;
+    enum {
+        sw_start = 0,
+        sw_value
+    };
+
+    for ( ;; ) {
+
+        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;
+
+            cscf = ngx_http_v3_get_module_srv_conf(c, ngx_http_core_module);
+
+            if (n > cscf->large_client_header_buffers.size) {
+                ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                              "client sent too large field line");
+                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 (b->pos == b->last) {
+                return NGX_AGAIN;
+            }
+
+            ch = *b->pos++;
+
+            if (st->huffman) {
+                if (ngx_http_huff_decode(&st->huffstate, &ch, 1, &st->last,
+                                         st->length == 1, c->log)
+                    != NGX_OK)
+                {
+                    ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                                  "client sent invalid encoded field line");
+                    return NGX_ERROR;
+                }
+
+            } else {
+                *st->last++ = ch;
+            }
+
+            if (--st->length) {
+                break;
+            }
+
+            st->value.len = st->last - st->value.data;
+            *st->last = '\0';
+            goto done;
+        }
+    }
+
+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;
+}
+
+
+static ngx_int_t
+ngx_http_v3_parse_field_ri(ngx_connection_t *c, ngx_http_v3_parse_field_t *st,
+    ngx_buf_t *b)
+{
+    u_char     ch;
+    ngx_int_t  rc;
+    enum {
+        sw_start = 0,
+        sw_index
+    };
+
+    for ( ;; ) {
+
+        switch (st->state) {
+
+        case sw_start:
+
+            ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                           "http3 parse field ri");
+
+            if (b->pos == b->last) {
+                return NGX_AGAIN;
+            }
+
+            ch = *b->pos;
+
+            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, b);
+            if (rc != NGX_DONE) {
+                return rc;
+            }
+
+            st->index = st->pint.value;
+            goto done;
+        }
+    }
+
+done:
+
+    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 parse field 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;
+}
+
+
+static ngx_int_t
+ngx_http_v3_parse_field_lri(ngx_connection_t *c,
+    ngx_http_v3_parse_field_t *st, ngx_buf_t *b)
+{
+    u_char     ch;
+    ngx_int_t  rc;
+    enum {
+        sw_start = 0,
+        sw_index,
+        sw_value_len,
+        sw_read_value_len,
+        sw_value
+    };
+
+    for ( ;; ) {
+
+        switch (st->state) {
+
+        case sw_start:
+
+            ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                           "http3 parse field lri");
+
+            if (b->pos == b->last) {
+                return NGX_AGAIN;
+            }
+
+            ch = *b->pos;
+
+            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, b);
+            if (rc != NGX_DONE) {
+                return rc;
+            }
+
+            st->index = st->pint.value;
+            st->state = sw_value_len;
+            break;
+
+        case sw_value_len:
+
+            if (b->pos == b->last) {
+                return NGX_AGAIN;
+            }
+
+            ch = *b->pos;
+
+            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, b);
+            if (rc != NGX_DONE) {
+                return rc;
+            }
+
+            st->literal.length = st->pint.value;
+            if (st->literal.length == 0) {
+                st->value.data = (u_char *) "";
+                goto done;
+            }
+
+            st->state = sw_value;
+            break;
+
+        case sw_value:
+
+            rc = ngx_http_v3_parse_literal(c, &st->literal, b);
+            if (rc != NGX_DONE) {
+                return rc;
+            }
+
+            st->value = st->literal.value;
+            goto done;
+        }
+    }
+
+done:
+
+    ngx_log_debug3(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 parse field 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;
+}
+
+
+static ngx_int_t
+ngx_http_v3_parse_field_l(ngx_connection_t *c,
+    ngx_http_v3_parse_field_t *st, ngx_buf_t *b)
+{
+    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
+    };
+
+    for ( ;; ) {
+
+        switch (st->state) {
+
+        case sw_start:
+
+            ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                           "http3 parse field l");
+
+            if (b->pos == b->last) {
+                return NGX_AGAIN;
+            }
+
+            ch = *b->pos;
+
+            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, b);
+            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, b);
+            if (rc != NGX_DONE) {
+                return rc;
+            }
+
+            st->name = st->literal.value;
+            st->state = sw_value_len;
+            break;
+
+        case sw_value_len:
+
+            if (b->pos == b->last) {
+                return NGX_AGAIN;
+            }
+
+            ch = *b->pos;
+
+            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, b);
+            if (rc != NGX_DONE) {
+                return rc;
+            }
+
+            st->literal.length = st->pint.value;
+            if (st->literal.length == 0) {
+                st->value.data = (u_char *) "";
+                goto done;
+            }
+
+            st->state = sw_value;
+            break;
+
+        case sw_value:
+
+            rc = ngx_http_v3_parse_literal(c, &st->literal, b);
+            if (rc != NGX_DONE) {
+                return rc;
+            }
+
+            st->value = st->literal.value;
+            goto done;
+        }
+    }
+
+done:
+
+    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 parse field l done \"%V\" \"%V\"",
+                   &st->name, &st->value);
+
+    st->state = sw_start;
+    return NGX_DONE;
+}
+
+
+static ngx_int_t
+ngx_http_v3_parse_field_pbi(ngx_connection_t *c,
+    ngx_http_v3_parse_field_t *st, ngx_buf_t *b)
+{
+    ngx_int_t  rc;
+    enum {
+        sw_start = 0,
+        sw_index
+    };
+
+    for ( ;; ) {
+
+        switch (st->state) {
+
+        case sw_start:
+
+            ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                           "http3 parse field pbi");
+
+            st->state = sw_index;
+
+            /* fall through */
+
+        case sw_index:
+
+            rc = ngx_http_v3_parse_prefix_int(c, &st->pint, 4, b);
+            if (rc != NGX_DONE) {
+                return rc;
+            }
+
+            st->index = st->pint.value;
+            goto done;
+        }
+    }
+
+done:
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 parse field 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;
+}
+
+
+static ngx_int_t
+ngx_http_v3_parse_field_lpbi(ngx_connection_t *c,
+    ngx_http_v3_parse_field_t *st, ngx_buf_t *b)
+{
+    u_char     ch;
+    ngx_int_t  rc;
+    enum {
+        sw_start = 0,
+        sw_index,
+        sw_value_len,
+        sw_read_value_len,
+        sw_value
+    };
+
+    for ( ;; ) {
+
+        switch (st->state) {
+
+        case sw_start:
+
+            ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                           "http3 parse field lpbi");
+
+            st->state = sw_index;
+
+            /* fall through */
+
+        case sw_index:
+
+            rc = ngx_http_v3_parse_prefix_int(c, &st->pint, 3, b);
+            if (rc != NGX_DONE) {
+                return rc;
+            }
+
+            st->index = st->pint.value;
+            st->state = sw_value_len;
+            break;
+
+        case sw_value_len:
+
+            if (b->pos == b->last) {
+                return NGX_AGAIN;
+            }
+
+            ch = *b->pos;
+
+            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, b);
+            if (rc != NGX_DONE) {
+                return rc;
+            }
+
+            st->literal.length = st->pint.value;
+            if (st->literal.length == 0) {
+                st->value.data = (u_char *) "";
+                goto done;
+            }
+
+            st->state = sw_value;
+            break;
+
+        case sw_value:
+
+            rc = ngx_http_v3_parse_literal(c, &st->literal, b);
+            if (rc != NGX_DONE) {
+                return rc;
+            }
+
+            st->value = st->literal.value;
+            goto done;
+        }
+    }
+
+done:
+
+    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 parse field 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;
+}
+
+
+static ngx_int_t
+ngx_http_v3_parse_control(ngx_connection_t *c, ngx_http_v3_parse_control_t *st,
+    ngx_buf_t *b)
+{
+    ngx_buf_t  loc;
+    ngx_int_t  rc;
+    enum {
+        sw_start = 0,
+        sw_first_type,
+        sw_type,
+        sw_length,
+        sw_settings,
+        sw_skip
+    };
+
+    for ( ;; ) {
+
+        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, b);
+            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 (st->state == sw_first_type
+                && st->type != NGX_HTTP_V3_FRAME_SETTINGS)
+            {
+                return NGX_HTTP_V3_ERR_MISSING_SETTINGS;
+            }
+
+            if (st->state != sw_first_type
+                && st->type == NGX_HTTP_V3_FRAME_SETTINGS)
+            {
+                return NGX_HTTP_V3_ERR_FRAME_UNEXPECTED;
+            }
+
+            if (ngx_http_v3_is_v2_frame(st->type)
+                || st->type == NGX_HTTP_V3_FRAME_DATA
+                || st->type == NGX_HTTP_V3_FRAME_HEADERS
+                || st->type == NGX_HTTP_V3_FRAME_PUSH_PROMISE)
+            {
+                return NGX_HTTP_V3_ERR_FRAME_UNEXPECTED;
+            }
+
+            if (st->type == NGX_HTTP_V3_FRAME_CANCEL_PUSH) {
+                return NGX_HTTP_V3_ERR_ID_ERROR;
+            }
+
+            st->state = sw_length;
+            break;
+
+        case sw_length:
+
+            rc = ngx_http_v3_parse_varlen_int(c, &st->vlint, b);
+            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_SETTINGS:
+                st->state = sw_settings;
+                break;
+
+            default:
+                ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                               "http3 parse skip unknown frame");
+                st->state = sw_skip;
+            }
+
+            break;
+
+        case sw_settings:
+
+            ngx_http_v3_parse_start_local(b, &loc, st->length);
+
+            rc = ngx_http_v3_parse_settings(c, &st->settings, &loc);
+
+            ngx_http_v3_parse_end_local(b, &loc, &st->length);
+
+            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_skip:
+
+            rc = ngx_http_v3_parse_skip(b, &st->length);
+            if (rc != NGX_DONE) {
+                return rc;
+            }
+
+            st->state = sw_type;
+            break;
+        }
+    }
+}
+
+
+static ngx_int_t
+ngx_http_v3_parse_settings(ngx_connection_t *c,
+    ngx_http_v3_parse_settings_t *st, ngx_buf_t *b)
+{
+    ngx_int_t  rc;
+    enum {
+        sw_start = 0,
+        sw_id,
+        sw_value
+    };
+
+    for ( ;; ) {
+
+        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, b);
+            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, b);
+            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;
+        }
+    }
+
+done:
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 parse settings done");
+
+    st->state = sw_start;
+    return NGX_DONE;
+}
+
+
+static ngx_int_t
+ngx_http_v3_parse_encoder(ngx_connection_t *c, ngx_http_v3_parse_encoder_t *st,
+    ngx_buf_t *b)
+{
+    u_char     ch;
+    ngx_int_t  rc;
+    enum {
+        sw_start = 0,
+        sw_inr,
+        sw_iln,
+        sw_capacity,
+        sw_duplicate
+    };
+
+    for ( ;; ) {
+
+        if (st->state == sw_start) {
+
+            ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                           "http3 parse encoder instruction");
+
+            if (b->pos == b->last) {
+                return NGX_AGAIN;
+            }
+
+            ch = *b->pos;
+
+            if (ch & 0x80) {
+                /* Insert With Name Reference */
+
+                st->state = sw_inr;
+
+            } else if (ch & 0x40) {
+                /* Insert With Literal Name */
+
+                st->state = sw_iln;
+
+            } 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_field_inr(c, &st->field, b);
+            if (rc != NGX_DONE) {
+                return rc;
+            }
+
+            st->state = sw_start;
+            break;
+
+        case sw_iln:
+
+            rc = ngx_http_v3_parse_field_iln(c, &st->field, b);
+            if (rc != NGX_DONE) {
+                return rc;
+            }
+
+            st->state = sw_start;
+            break;
+
+        case sw_capacity:
+
+            rc = ngx_http_v3_parse_prefix_int(c, &st->pint, 5, b);
+            if (rc != NGX_DONE) {
+                return rc;
+            }
+
+            rc = ngx_http_v3_set_capacity(c, st->pint.value);
+            if (rc != NGX_OK) {
+                return rc;
+            }
+
+            st->state = sw_start;
+            break;
+
+        default: /* sw_duplicate */
+
+            rc = ngx_http_v3_parse_prefix_int(c, &st->pint, 5, b);
+            if (rc != NGX_DONE) {
+                return rc;
+            }
+
+            rc = ngx_http_v3_duplicate(c, st->pint.value);
+            if (rc != NGX_OK) {
+                return rc;
+            }
+
+            st->state = sw_start;
+            break;
+        }
+    }
+}
+
+
+static ngx_int_t
+ngx_http_v3_parse_field_inr(ngx_connection_t *c,
+    ngx_http_v3_parse_field_t *st, ngx_buf_t *b)
+{
+    u_char     ch;
+    ngx_int_t  rc;
+    enum {
+        sw_start = 0,
+        sw_name_index,
+        sw_value_len,
+        sw_read_value_len,
+        sw_value
+    };
+
+    for ( ;; ) {
+
+        switch (st->state) {
+
+        case sw_start:
+
+            ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                           "http3 parse field inr");
+
+            if (b->pos == b->last) {
+                return NGX_AGAIN;
+            }
+
+            ch = *b->pos;
+
+            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, b);
+            if (rc != NGX_DONE) {
+                return rc;
+            }
+
+            st->index = st->pint.value;
+            st->state = sw_value_len;
+            break;
+
+        case sw_value_len:
+
+            if (b->pos == b->last) {
+                return NGX_AGAIN;
+            }
+
+            ch = *b->pos;
+
+            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, b);
+            if (rc != NGX_DONE) {
+                return rc;
+            }
+
+            st->literal.length = st->pint.value;
+            if (st->literal.length == 0) {
+                st->value.len = 0;
+                goto done;
+            }
+
+            st->state = sw_value;
+            break;
+
+        case sw_value:
+
+            rc = ngx_http_v3_parse_literal(c, &st->literal, b);
+            if (rc != NGX_DONE) {
+                return rc;
+            }
+
+            st->value = st->literal.value;
+            goto done;
+        }
+    }
+
+done:
+
+    ngx_log_debug3(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 parse field 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;
+}
+
+
+static ngx_int_t
+ngx_http_v3_parse_field_iln(ngx_connection_t *c,
+    ngx_http_v3_parse_field_t *st, ngx_buf_t *b)
+{
+    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
+    };
+
+    for ( ;; ) {
+
+        switch (st->state) {
+
+        case sw_start:
+
+            ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                           "http3 parse field iln");
+
+            if (b->pos == b->last) {
+                return NGX_AGAIN;
+            }
+
+            ch = *b->pos;
+
+            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, b);
+            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, b);
+            if (rc != NGX_DONE) {
+                return rc;
+            }
+
+            st->name = st->literal.value;
+            st->state = sw_value_len;
+            break;
+
+        case sw_value_len:
+
+            if (b->pos == b->last) {
+                return NGX_AGAIN;
+            }
+
+            ch = *b->pos;
+
+            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, b);
+            if (rc != NGX_DONE) {
+                return rc;
+            }
+
+            st->literal.length = st->pint.value;
+            if (st->literal.length == 0) {
+                st->value.len = 0;
+                goto done;
+            }
+
+            st->state = sw_value;
+            break;
+
+        case sw_value:
+
+            rc = ngx_http_v3_parse_literal(c, &st->literal, b);
+            if (rc != NGX_DONE) {
+                return rc;
+            }
+
+            st->value = st->literal.value;
+            goto done;
+        }
+    }
+
+done:
+
+    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 parse field iln 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;
+}
+
+
+static ngx_int_t
+ngx_http_v3_parse_decoder(ngx_connection_t *c, ngx_http_v3_parse_decoder_t *st,
+    ngx_buf_t *b)
+{
+    u_char     ch;
+    ngx_int_t  rc;
+    enum {
+        sw_start = 0,
+        sw_ack_section,
+        sw_cancel_stream,
+        sw_inc_insert_count
+    };
+
+    for ( ;; ) {
+
+        if (st->state == sw_start) {
+
+            ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                           "http3 parse decoder instruction");
+
+            if (b->pos == b->last) {
+                return NGX_AGAIN;
+            }
+
+            ch = *b->pos;
+
+            if (ch & 0x80) {
+                /* Section Acknowledgment */
+
+                st->state = sw_ack_section;
+
+            } 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_section:
+
+            rc = ngx_http_v3_parse_prefix_int(c, &st->pint, 7, b);
+            if (rc != NGX_DONE) {
+                return rc;
+            }
+
+            rc = ngx_http_v3_ack_section(c, st->pint.value);
+            if (rc != NGX_OK) {
+                return rc;
+            }
+
+            st->state = sw_start;
+            break;
+
+        case sw_cancel_stream:
+
+            rc = ngx_http_v3_parse_prefix_int(c, &st->pint, 6, b);
+            if (rc != NGX_DONE) {
+                return rc;
+            }
+
+            rc = ngx_http_v3_cancel_stream(c, st->pint.value);
+            if (rc != NGX_OK) {
+                return rc;
+            }
+
+            st->state = sw_start;
+            break;
+
+        case sw_inc_insert_count:
+
+            rc = ngx_http_v3_parse_prefix_int(c, &st->pint, 6, b);
+            if (rc != NGX_DONE) {
+                return rc;
+            }
+
+            rc = ngx_http_v3_inc_insert_count(c, st->pint.value);
+            if (rc != NGX_OK) {
+                return rc;
+            }
+
+            st->state = sw_start;
+            break;
+        }
+    }
+}
+
+
+ngx_int_t
+ngx_http_v3_parse_data(ngx_connection_t *c, ngx_http_v3_parse_data_t *st,
+    ngx_buf_t *b)
+{
+    ngx_int_t  rc;
+    enum {
+        sw_start = 0,
+        sw_type,
+        sw_length,
+        sw_skip
+    };
+
+    for ( ;; ) {
+
+        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, b);
+            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)
+                || st->type == NGX_HTTP_V3_FRAME_GOAWAY
+                || st->type == NGX_HTTP_V3_FRAME_SETTINGS
+                || st->type == NGX_HTTP_V3_FRAME_MAX_PUSH_ID
+                || st->type == NGX_HTTP_V3_FRAME_CANCEL_PUSH
+                || st->type == NGX_HTTP_V3_FRAME_PUSH_PROMISE)
+            {
+                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, b);
+            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:
+
+            rc = ngx_http_v3_parse_skip(b, &st->length);
+            if (rc != NGX_DONE) {
+                return rc;
+            }
+
+            st->state = sw_type;
+            break;
+        }
+    }
+
+done:
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 parse data done");
+
+    st->state = sw_start;
+    return NGX_DONE;
+}
+
+
+ngx_int_t
+ngx_http_v3_parse_uni(ngx_connection_t *c, ngx_http_v3_parse_uni_t *st,
+    ngx_buf_t *b)
+{
+    ngx_int_t  rc;
+    enum {
+        sw_start = 0,
+        sw_type,
+        sw_control,
+        sw_encoder,
+        sw_decoder,
+        sw_unknown
+    };
+
+    for ( ;; ) {
+
+        switch (st->state) {
+        case sw_start:
+
+            ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 parse uni");
+
+            st->state = sw_type;
+
+            /* fall through */
+
+        case sw_type:
+
+            rc = ngx_http_v3_parse_varlen_int(c, &st->vlint, b);
+            if (rc != NGX_DONE) {
+                return rc;
+            }
+
+            rc = ngx_http_v3_register_uni_stream(c, st->vlint.value);
+            if (rc != NGX_OK) {
+                return rc;
+            }
+
+            switch (st->vlint.value) {
+            case NGX_HTTP_V3_STREAM_CONTROL:
+                st->state = sw_control;
+                break;
+
+            case NGX_HTTP_V3_STREAM_ENCODER:
+                st->state = sw_encoder;
+                break;
+
+            case NGX_HTTP_V3_STREAM_DECODER:
+                st->state = sw_decoder;
+                break;
+
+            default:
+                st->state = sw_unknown;
+            }
+
+            break;
+
+        case sw_control:
+
+            return ngx_http_v3_parse_control(c, &st->u.control, b);
+
+        case sw_encoder:
+
+            return ngx_http_v3_parse_encoder(c, &st->u.encoder, b);
+
+        case sw_decoder:
+
+            return ngx_http_v3_parse_decoder(c, &st->u.decoder, b);
+
+        case sw_unknown:
+
+            b->pos = b->last;
+            return NGX_AGAIN;
+        }
+    }
+}
diff --git a/src/http/v3/ngx_http_v3_parse.h b/src/http/v3/ngx_http_v3_parse.h
new file mode 100644
index 0000000..ba004db
--- /dev/null
+++ b/src/http/v3/ngx_http_v3_parse.h
@@ -0,0 +1,146 @@
+
+/*
+ * 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_field_section_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_field_t;
+
+
+typedef struct {
+    ngx_uint_t                      state;
+    ngx_http_v3_parse_field_t       field;
+} ngx_http_v3_parse_field_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_field_section_prefix_t  prefix;
+    ngx_http_v3_parse_field_rep_t   field_rep;
+} ngx_http_v3_parse_headers_t;
+
+
+typedef struct {
+    ngx_uint_t                      state;
+    ngx_http_v3_parse_field_t       field;
+    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_http_v3_parse_varlen_int_t  vlint;
+    union {
+        ngx_http_v3_parse_encoder_t  encoder;
+        ngx_http_v3_parse_decoder_t  decoder;
+        ngx_http_v3_parse_control_t  control;
+    } u;
+} ngx_http_v3_parse_uni_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_headers(ngx_connection_t *c,
+    ngx_http_v3_parse_headers_t *st, ngx_buf_t *b);
+ngx_int_t ngx_http_v3_parse_data(ngx_connection_t *c,
+    ngx_http_v3_parse_data_t *st, ngx_buf_t *b);
+ngx_int_t ngx_http_v3_parse_uni(ngx_connection_t *c,
+    ngx_http_v3_parse_uni_t *st, ngx_buf_t *b);
+
+
+#endif /* _NGX_HTTP_V3_PARSE_H_INCLUDED_ */
diff --git a/src/http/v3/ngx_http_v3_request.c b/src/http/v3/ngx_http_v3_request.c
new file mode 100644
index 0000000..0faddd2
--- /dev/null
+++ b/src/http/v3/ngx_http_v3_request.c
@@ -0,0 +1,1718 @@
+
+/*
+ * Copyright (C) Roman Arutyunyan
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_http.h>
+
+
+static void ngx_http_v3_init_request_stream(ngx_connection_t *c);
+static void ngx_http_v3_wait_request_handler(ngx_event_t *rev);
+static void ngx_http_v3_cleanup_connection(void *data);
+static void ngx_http_v3_cleanup_request(void *data);
+static void ngx_http_v3_process_request(ngx_event_t *rev);
+static ngx_int_t ngx_http_v3_process_header(ngx_http_request_t *r,
+    ngx_str_t *name, ngx_str_t *value);
+static ngx_int_t ngx_http_v3_validate_header(ngx_http_request_t *r,
+    ngx_str_t *name, ngx_str_t *value);
+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_init_pseudo_headers(ngx_http_request_t *r);
+static ngx_int_t ngx_http_v3_process_request_header(ngx_http_request_t *r);
+static ngx_int_t ngx_http_v3_cookie(ngx_http_request_t *r, ngx_str_t *value);
+static ngx_int_t ngx_http_v3_construct_cookie_header(ngx_http_request_t *r);
+static void ngx_http_v3_read_client_request_body_handler(ngx_http_request_t *r);
+static ngx_int_t ngx_http_v3_do_read_client_request_body(ngx_http_request_t *r);
+static ngx_int_t ngx_http_v3_request_body_filter(ngx_http_request_t *r,
+    ngx_chain_t *in);
+
+
+static const 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_string("CONNECT"),   NGX_HTTP_CONNECT }
+};
+
+
+void
+ngx_http_v3_init_stream(ngx_connection_t *c)
+{
+    ngx_http_connection_t     *hc, *phc;
+    ngx_http_v3_srv_conf_t    *h3scf;
+    ngx_http_core_loc_conf_t  *clcf;
+
+    hc = c->data;
+
+    hc->ssl = 1;
+
+    clcf = ngx_http_get_module_loc_conf(hc->conf_ctx, ngx_http_core_module);
+
+    if (c->quic == NULL) {
+        h3scf = ngx_http_get_module_srv_conf(hc->conf_ctx, ngx_http_v3_module);
+        h3scf->quic.idle_timeout = clcf->keepalive_timeout;
+
+        ngx_quic_run(c, &h3scf->quic);
+        return;
+    }
+
+    phc = ngx_http_quic_get_connection(c);
+
+    if (phc->ssl_servername) {
+        hc->ssl_servername = phc->ssl_servername;
+#if (NGX_PCRE)
+        hc->ssl_servername_regex = phc->ssl_servername_regex;
+#endif
+        hc->conf_ctx = phc->conf_ctx;
+
+        ngx_set_connection_log(c, clcf->error_log);
+    }
+
+    if (c->quic->id & NGX_QUIC_STREAM_UNIDIRECTIONAL) {
+        ngx_http_v3_init_uni_stream(c);
+
+    } else  {
+        ngx_http_v3_init_request_stream(c);
+    }
+}
+
+
+ngx_int_t
+ngx_http_v3_init(ngx_connection_t *c)
+{
+    unsigned int               len;
+    const unsigned char       *data;
+    ngx_http_v3_session_t     *h3c;
+    ngx_http_v3_srv_conf_t    *h3scf;
+    ngx_http_core_loc_conf_t  *clcf;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 init");
+
+    if (ngx_http_v3_init_session(c) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    h3c = ngx_http_v3_get_session(c);
+    clcf = ngx_http_v3_get_module_loc_conf(c, ngx_http_core_module);
+    ngx_add_timer(&h3c->keepalive, clcf->keepalive_timeout);
+
+    h3scf = ngx_http_v3_get_module_srv_conf(c, ngx_http_v3_module);
+
+    if (h3scf->enable_hq) {
+        if (!h3scf->enable) {
+            h3c->hq = 1;
+            return NGX_OK;
+        }
+
+        SSL_get0_alpn_selected(c->ssl->connection, &data, &len);
+
+        if (len == sizeof(NGX_HTTP_V3_HQ_PROTO) - 1
+            && ngx_strncmp(data, NGX_HTTP_V3_HQ_PROTO, len) == 0)
+        {
+            h3c->hq = 1;
+            return NGX_OK;
+        }
+    }
+
+    if (ngx_http_v3_send_settings(c) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    if (h3scf->max_table_capacity > 0) {
+        if (ngx_http_v3_get_uni_stream(c, NGX_HTTP_V3_STREAM_DECODER) == NULL) {
+            return NGX_ERROR;
+        }
+    }
+
+    return NGX_OK;
+}
+
+
+void
+ngx_http_v3_shutdown(ngx_connection_t *c)
+{
+    ngx_http_v3_session_t  *h3c;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 shutdown");
+
+    h3c = ngx_http_v3_get_session(c);
+
+    if (h3c == NULL) {
+        ngx_quic_finalize_connection(c, NGX_HTTP_V3_ERR_NO_ERROR,
+                                     "connection shutdown");
+        return;
+    }
+
+    if (!h3c->goaway) {
+        h3c->goaway = 1;
+
+        if (!h3c->hq) {
+            (void) ngx_http_v3_send_goaway(c, h3c->next_request_id);
+        }
+
+        ngx_http_v3_shutdown_connection(c, NGX_HTTP_V3_ERR_NO_ERROR,
+                                        "connection shutdown");
+    }
+}
+
+
+static void
+ngx_http_v3_init_request_stream(ngx_connection_t *c)
+{
+    uint64_t                   n;
+    ngx_event_t               *rev;
+    ngx_pool_cleanup_t        *cln;
+    ngx_http_connection_t     *hc;
+    ngx_http_v3_session_t     *h3c;
+    ngx_http_core_loc_conf_t  *clcf;
+    ngx_http_core_srv_conf_t  *cscf;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 init request stream");
+
+#if (NGX_STAT_STUB)
+    (void) ngx_atomic_fetch_add(ngx_stat_active, 1);
+#endif
+
+    hc = c->data;
+
+    clcf = ngx_http_get_module_loc_conf(hc->conf_ctx, ngx_http_core_module);
+
+    n = c->quic->id >> 2;
+
+    if (n >= clcf->keepalive_requests * 2) {
+        ngx_http_v3_finalize_connection(c, NGX_HTTP_V3_ERR_EXCESSIVE_LOAD,
+                                        "too many requests per connection");
+        ngx_http_close_connection(c);
+        return;
+    }
+
+    h3c = ngx_http_v3_get_session(c);
+
+    if (h3c->goaway) {
+        c->close = 1;
+        ngx_http_close_connection(c);
+        return;
+    }
+
+    h3c->next_request_id = c->quic->id + 0x04;
+
+    if (n + 1 == clcf->keepalive_requests
+        || ngx_current_msec - c->start_time > clcf->keepalive_time)
+    {
+        h3c->goaway = 1;
+
+        if (!h3c->hq) {
+            if (ngx_http_v3_send_goaway(c, h3c->next_request_id) != NGX_OK) {
+                ngx_http_close_connection(c);
+                return;
+            }
+        }
+
+        ngx_http_v3_shutdown_connection(c, NGX_HTTP_V3_ERR_NO_ERROR,
+                                        "reached maximum number of requests");
+    }
+
+    cln = ngx_pool_cleanup_add(c->pool, 0);
+    if (cln == NULL) {
+        ngx_http_close_connection(c);
+        return;
+    }
+
+    cln->handler = ngx_http_v3_cleanup_connection;
+    cln->data = c;
+
+    h3c->nrequests++;
+
+    if (h3c->keepalive.timer_set) {
+        ngx_del_timer(&h3c->keepalive);
+    }
+
+    rev = c->read;
+
+    if (!h3c->hq) {
+        rev->handler = ngx_http_v3_wait_request_handler;
+        c->write->handler = ngx_http_empty_handler;
+    }
+
+    if (rev->ready) {
+        rev->handler(rev);
+        return;
+    }
+
+    cscf = ngx_http_get_module_srv_conf(hc->conf_ctx, ngx_http_core_module);
+
+    ngx_add_timer(rev, cscf->client_header_timeout);
+    ngx_reusable_connection(c, 1);
+
+    if (ngx_handle_read_event(rev, 0) != NGX_OK) {
+        ngx_http_close_connection(c);
+        return;
+    }
+}
+
+
+static void
+ngx_http_v3_wait_request_handler(ngx_event_t *rev)
+{
+    size_t                     size;
+    ssize_t                    n;
+    ngx_buf_t                 *b;
+    ngx_connection_t          *c;
+    ngx_pool_cleanup_t        *cln;
+    ngx_http_request_t        *r;
+    ngx_http_connection_t     *hc;
+    ngx_http_core_srv_conf_t  *cscf;
+
+    c = rev->data;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 wait request handler");
+
+    if (rev->timedout) {
+        ngx_log_error(NGX_LOG_INFO, c->log, NGX_ETIMEDOUT, "client timed out");
+        c->timedout = 1;
+        ngx_http_close_connection(c);
+        return;
+    }
+
+    if (c->close) {
+        ngx_http_close_connection(c);
+        return;
+    }
+
+    hc = c->data;
+    cscf = ngx_http_get_module_srv_conf(hc->conf_ctx, ngx_http_core_module);
+
+    size = cscf->client_header_buffer_size;
+
+    b = c->buffer;
+
+    if (b == NULL) {
+        b = ngx_create_temp_buf(c->pool, size);
+        if (b == NULL) {
+            ngx_http_close_connection(c);
+            return;
+        }
+
+        c->buffer = b;
+
+    } else if (b->start == NULL) {
+
+        b->start = ngx_palloc(c->pool, size);
+        if (b->start == NULL) {
+            ngx_http_close_connection(c);
+            return;
+        }
+
+        b->pos = b->start;
+        b->last = b->start;
+        b->end = b->last + size;
+    }
+
+    n = c->recv(c, b->last, size);
+
+    if (n == NGX_AGAIN) {
+
+        if (!rev->timer_set) {
+            ngx_add_timer(rev, cscf->client_header_timeout);
+            ngx_reusable_connection(c, 1);
+        }
+
+        if (ngx_handle_read_event(rev, 0) != NGX_OK) {
+            ngx_http_close_connection(c);
+            return;
+        }
+
+        /*
+         * We are trying to not hold c->buffer's memory for an idle connection.
+         */
+
+        if (ngx_pfree(c->pool, b->start) == NGX_OK) {
+            b->start = NULL;
+        }
+
+        return;
+    }
+
+    if (n == NGX_ERROR) {
+        ngx_http_close_connection(c);
+        return;
+    }
+
+    if (n == 0) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "client closed connection");
+        ngx_http_close_connection(c);
+        return;
+    }
+
+    b->last += n;
+
+    c->log->action = "reading client request";
+
+    ngx_reusable_connection(c, 0);
+
+    r = ngx_http_create_request(c);
+    if (r == NULL) {
+        ngx_http_close_connection(c);
+        return;
+    }
+
+    r->http_version = NGX_HTTP_VERSION_30;
+
+    r->v3_parse = ngx_pcalloc(r->pool, sizeof(ngx_http_v3_parse_t));
+    if (r->v3_parse == NULL) {
+        ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
+        return;
+    }
+
+    r->v3_parse->header_limit = cscf->large_client_header_buffers.size
+                                * cscf->large_client_header_buffers.num;
+
+    c->data = r;
+    c->requests = (c->quic->id >> 2) + 1;
+
+    cln = ngx_pool_cleanup_add(r->pool, 0);
+    if (cln == NULL) {
+        ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
+        return;
+    }
+
+    cln->handler = ngx_http_v3_cleanup_request;
+    cln->data = r;
+
+    rev->handler = ngx_http_v3_process_request;
+    ngx_http_v3_process_request(rev);
+}
+
+
+void
+ngx_http_v3_reset_stream(ngx_connection_t *c)
+{
+    ngx_http_v3_session_t  *h3c;
+
+    h3c = ngx_http_v3_get_session(c);
+
+    if (!c->read->eof && !h3c->hq
+        && h3c->known_streams[NGX_HTTP_V3_STREAM_SERVER_DECODER]
+        && (c->quic->id & NGX_QUIC_STREAM_UNIDIRECTIONAL) == 0)
+    {
+        (void) ngx_http_v3_send_cancel_stream(c, c->quic->id);
+    }
+
+    if (c->timedout) {
+        ngx_quic_reset_stream(c, NGX_HTTP_V3_ERR_GENERAL_PROTOCOL_ERROR);
+
+    } else if (c->close) {
+        ngx_quic_reset_stream(c, NGX_HTTP_V3_ERR_REQUEST_REJECTED);
+
+    } else if (c->requests == 0 || c->error) {
+        ngx_quic_reset_stream(c, NGX_HTTP_V3_ERR_INTERNAL_ERROR);
+    }
+}
+
+
+static void
+ngx_http_v3_cleanup_connection(void *data)
+{
+    ngx_connection_t  *c = data;
+
+    ngx_http_v3_session_t     *h3c;
+    ngx_http_core_loc_conf_t  *clcf;
+
+    h3c = ngx_http_v3_get_session(c);
+
+    if (--h3c->nrequests == 0) {
+        clcf = ngx_http_v3_get_module_loc_conf(c, ngx_http_core_module);
+        ngx_add_timer(&h3c->keepalive, clcf->keepalive_timeout);
+    }
+}
+
+
+static void
+ngx_http_v3_cleanup_request(void *data)
+{
+    ngx_http_request_t  *r = data;
+
+    if (!r->response_sent) {
+        r->connection->error = 1;
+    }
+}
+
+
+static void
+ngx_http_v3_process_request(ngx_event_t *rev)
+{
+    u_char                       *p;
+    ssize_t                       n;
+    ngx_buf_t                    *b;
+    ngx_int_t                     rc;
+    ngx_connection_t             *c;
+    ngx_http_request_t           *r;
+    ngx_http_v3_session_t        *h3c;
+    ngx_http_core_srv_conf_t     *cscf;
+    ngx_http_v3_parse_headers_t  *st;
+
+    c = rev->data;
+    r = c->data;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, rev->log, 0, "http3 process request");
+
+    if (rev->timedout) {
+        ngx_log_error(NGX_LOG_INFO, c->log, NGX_ETIMEDOUT, "client timed out");
+        c->timedout = 1;
+        ngx_http_close_request(r, NGX_HTTP_REQUEST_TIME_OUT);
+        return;
+    }
+
+    h3c = ngx_http_v3_get_session(c);
+
+    st = &r->v3_parse->headers;
+
+    b = r->header_in;
+
+    for ( ;; ) {
+
+        if (b->pos == b->last) {
+
+            if (rev->ready) {
+                n = c->recv(c, b->start, b->end - b->start);
+
+            } else {
+                n = NGX_AGAIN;
+            }
+
+            if (n == NGX_AGAIN) {
+                if (!rev->timer_set) {
+                    cscf = ngx_http_get_module_srv_conf(r,
+                                                        ngx_http_core_module);
+                    ngx_add_timer(rev, cscf->client_header_timeout);
+                }
+
+                if (ngx_handle_read_event(rev, 0) != NGX_OK) {
+                    ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
+                }
+
+                break;
+            }
+
+            if (n == 0) {
+                ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                              "client prematurely closed connection");
+            }
+
+            if (n == 0 || n == NGX_ERROR) {
+                c->error = 1;
+                c->log->action = "reading client request";
+
+                ngx_http_finalize_request(r, NGX_HTTP_BAD_REQUEST);
+                break;
+            }
+
+            b->pos = b->start;
+            b->last = b->start + n;
+        }
+
+        p = b->pos;
+
+        rc = ngx_http_v3_parse_headers(c, st, b);
+
+        if (rc > 0) {
+            ngx_quic_reset_stream(c, rc);
+            ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
+                          "client sent invalid header");
+            ngx_http_finalize_request(r, NGX_HTTP_BAD_REQUEST);
+            break;
+        }
+
+        if (rc == NGX_ERROR) {
+            ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
+            break;
+        }
+
+        r->request_length += b->pos - p;
+        h3c->total_bytes += b->pos - p;
+
+        if (ngx_http_v3_check_flood(c) != NGX_OK) {
+            ngx_http_close_request(r, NGX_HTTP_CLOSE);
+            break;
+        }
+
+        if (rc == NGX_BUSY) {
+            if (rev->error) {
+                ngx_http_finalize_request(r, NGX_HTTP_BAD_REQUEST);
+                break;
+            }
+
+            if (!rev->timer_set) {
+                cscf = ngx_http_get_module_srv_conf(r,
+                                                    ngx_http_core_module);
+                ngx_add_timer(rev, cscf->client_header_timeout);
+            }
+
+            if (ngx_handle_read_event(rev, 0) != NGX_OK) {
+                ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
+            }
+
+            break;
+        }
+
+        if (rc == NGX_AGAIN) {
+            continue;
+        }
+
+        /* rc == NGX_OK || rc == NGX_DONE */
+
+        h3c->payload_bytes += ngx_http_v3_encode_field_l(NULL,
+                                                   &st->field_rep.field.name,
+                                                   &st->field_rep.field.value);
+
+        if (ngx_http_v3_process_header(r, &st->field_rep.field.name,
+                                       &st->field_rep.field.value)
+            != NGX_OK)
+        {
+            break;
+        }
+
+        if (rc == NGX_DONE) {
+            if (ngx_http_v3_process_request_header(r) != NGX_OK) {
+                break;
+            }
+
+            ngx_http_process_request(r);
+            break;
+        }
+    }
+
+    ngx_http_run_posted_requests(c);
+
+    return;
+}
+
+
+static ngx_int_t
+ngx_http_v3_process_header(ngx_http_request_t *r, ngx_str_t *name,
+    ngx_str_t *value)
+{
+    size_t                      len;
+    ngx_table_elt_t            *h;
+    ngx_http_header_t          *hh;
+    ngx_http_core_srv_conf_t   *cscf;
+    ngx_http_core_main_conf_t  *cmcf;
+
+    static ngx_str_t cookie = ngx_string("cookie");
+
+    len = name->len + value->len;
+
+    if (len > r->v3_parse->header_limit) {
+        ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
+                      "client sent too large header");
+        ngx_http_finalize_request(r, NGX_HTTP_REQUEST_HEADER_TOO_LARGE);
+        return NGX_ERROR;
+    }
+
+    r->v3_parse->header_limit -= len;
+
+    if (ngx_http_v3_validate_header(r, name, value) != NGX_OK) {
+        ngx_http_finalize_request(r, NGX_HTTP_BAD_REQUEST);
+        return NGX_ERROR;
+    }
+
+    if (r->invalid_header) {
+        cscf = ngx_http_get_module_srv_conf(r, ngx_http_core_module);
+
+        if (cscf->ignore_invalid_headers) {
+            ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
+                          "client sent invalid header: \"%V\"", name);
+
+            return NGX_OK;
+        }
+    }
+
+    if (name->len && name->data[0] == ':') {
+        return ngx_http_v3_process_pseudo_header(r, name, value);
+    }
+
+    if (ngx_http_v3_init_pseudo_headers(r) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    if (name->len == cookie.len
+        && ngx_memcmp(name->data, cookie.data, cookie.len) == 0)
+    {
+        if (ngx_http_v3_cookie(r, value) != NGX_OK) {
+            ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
+            return NGX_ERROR;
+        }
+
+    } else {
+        h = ngx_list_push(&r->headers_in.headers);
+        if (h == NULL) {
+            ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
+            return NGX_ERROR;
+        }
+
+        h->key = *name;
+        h->value = *value;
+        h->lowcase_key = h->key.data;
+        h->hash = ngx_hash_key(h->key.data, h->key.len);
+
+        cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module);
+
+        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;
+        }
+    }
+
+    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
+                   "http3 header: \"%V: %V\"", name, value);
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_http_v3_validate_header(ngx_http_request_t *r, ngx_str_t *name,
+    ngx_str_t *value)
+{
+    u_char                     ch;
+    ngx_uint_t                 i;
+    ngx_http_core_srv_conf_t  *cscf;
+
+    r->invalid_header = 0;
+
+    cscf = ngx_http_get_module_srv_conf(r, ngx_http_core_module);
+
+    for (i = (name->data[0] == ':'); i != name->len; i++) {
+        ch = name->data[i];
+
+        if ((ch >= 'a' && ch <= 'z')
+            || (ch == '-')
+            || (ch >= '0' && ch <= '9')
+            || (ch == '_' && cscf->underscores_in_headers))
+        {
+            continue;
+        }
+
+        if (ch <= 0x20 || ch == 0x7f || ch == ':'
+            || (ch >= 'A' && ch <= 'Z'))
+        {
+            ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
+                          "client sent invalid header name: \"%V\"", name);
+
+            return NGX_ERROR;
+        }
+
+        r->invalid_header = 1;
+    }
+
+    for (i = 0; i != value->len; i++) {
+        ch = value->data[i];
+
+        if (ch == '\0' || ch == LF || ch == CR) {
+            ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
+                          "client sent header \"%V\" with "
+                          "invalid value: \"%V\"", name, value);
+
+            return NGX_ERROR;
+        }
+    }
+
+    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)
+{
+    u_char      ch, c;
+    ngx_uint_t  i;
+
+    if (r->request_line.len) {
+        ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
+                      "client sent out of order pseudo-headers");
+        goto failed;
+    }
+
+    if (name->len == 7 && ngx_strncmp(name->data, ":method", 7) == 0) {
+
+        if (r->method_name.len) {
+            ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
+                          "client sent duplicate \":method\" header");
+            goto failed;
+        }
+
+        if (value->len == 0) {
+            ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
+                          "client sent empty \":method\" header");
+            goto failed;
+        }
+
+        r->method_name = *value;
+
+        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;
+            }
+        }
+
+        for (i = 0; i < value->len; i++) {
+            ch = value->data[i];
+
+            if ((ch < 'A' || ch > 'Z') && ch != '_' && ch != '-') {
+                ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
+                              "client sent invalid method: \"%V\"", value);
+                goto failed;
+            }
+        }
+
+        ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
+                       "http3 method \"%V\" %ui", value, r->method);
+        return NGX_OK;
+    }
+
+    if (name->len == 5 && ngx_strncmp(name->data, ":path", 5) == 0) {
+
+        if (r->uri_start) {
+            ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
+                          "client sent duplicate \":path\" header");
+            goto failed;
+        }
+
+        if (value->len == 0) {
+            ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
+                          "client sent empty \":path\" header");
+            goto failed;
+        }
+
+        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, r->connection->log, 0,
+                          "client sent invalid \":path\" header: \"%V\"",
+                          value);
+            goto failed;
+        }
+
+        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
+                       "http3 path \"%V\"", value);
+        return NGX_OK;
+    }
+
+    if (name->len == 7 && ngx_strncmp(name->data, ":scheme", 7) == 0) {
+
+        if (r->schema.len) {
+            ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
+                          "client sent duplicate \":scheme\" header");
+            goto failed;
+        }
+
+        if (value->len == 0) {
+            ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
+                          "client sent empty \":scheme\" header");
+            goto failed;
+        }
+
+        for (i = 0; i < value->len; i++) {
+            ch = value->data[i];
+
+            c = (u_char) (ch | 0x20);
+            if (c >= 'a' && c <= 'z') {
+                continue;
+            }
+
+            if (((ch >= '0' && ch <= '9')
+                 || ch == '+' || ch == '-' || ch == '.')
+                && i > 0)
+            {
+                continue;
+            }
+
+            ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
+                          "client sent invalid \":scheme\" header: \"%V\"",
+                          value);
+            goto failed;
+        }
+
+        r->schema = *value;
+
+        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
+                       "http3 schema \"%V\"", value);
+        return NGX_OK;
+    }
+
+    if (name->len == 10 && ngx_strncmp(name->data, ":authority", 10) == 0) {
+
+        if (r->host_start) {
+            ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
+                          "client sent duplicate \":authority\" header");
+            goto failed;
+        }
+
+        r->host_start = value->data;
+        r->host_end = value->data + value->len;
+
+        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
+                       "http3 authority \"%V\"", value);
+        return NGX_OK;
+    }
+
+    ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
+                  "client sent unknown pseudo-header \"%V\"", name);
+
+failed:
+
+    ngx_http_finalize_request(r, NGX_HTTP_BAD_REQUEST);
+    return NGX_ERROR;
+}
+
+
+static ngx_int_t
+ngx_http_v3_init_pseudo_headers(ngx_http_request_t *r)
+{
+    size_t      len;
+    u_char     *p;
+    ngx_int_t   rc;
+    ngx_str_t   host;
+
+    if (r->request_line.len) {
+        return NGX_OK;
+    }
+
+    if (r->method_name.len == 0) {
+        ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
+                      "client sent no \":method\" header");
+        goto failed;
+    }
+
+    if (r->schema.len == 0) {
+        ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
+                      "client sent no \":scheme\" header");
+        goto failed;
+    }
+
+    if (r->uri_start == NULL) {
+        ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
+                      "client sent no \":path\" header");
+        goto failed;
+    }
+
+    len = r->method_name.len + 1
+          + (r->uri_end - r->uri_start) + 1
+          + sizeof("HTTP/3.0") - 1;
+
+    p = ngx_pnalloc(r->pool, len);
+    if (p == NULL) {
+        ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
+        return NGX_ERROR;
+    }
+
+    r->request_line.data = p;
+
+    p = ngx_cpymem(p, r->method_name.data, r->method_name.len);
+    *p++ = ' ';
+    p = ngx_cpymem(p, r->uri_start, r->uri_end - r->uri_start);
+    *p++ = ' ';
+    p = ngx_cpymem(p, "HTTP/3.0", sizeof("HTTP/3.0") - 1);
+
+    r->request_line.len = p - r->request_line.data;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
+                   "http3 request line: \"%V\"", &r->request_line);
+
+    ngx_str_set(&r->http_protocol, "HTTP/3.0");
+
+    if (ngx_http_process_request_uri(r) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    if (r->host_end) {
+
+        host.len = r->host_end - r->host_start;
+        host.data = r->host_start;
+
+        rc = ngx_http_validate_host(&host, r->pool, 0);
+
+        if (rc == NGX_DECLINED) {
+            ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
+                          "client sent invalid host in request line");
+            goto failed;
+        }
+
+        if (rc == NGX_ERROR) {
+            ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
+            return NGX_ERROR;
+        }
+
+        if (ngx_http_set_virtual_server(r, &host) == NGX_ERROR) {
+            return NGX_ERROR;
+        }
+
+        r->headers_in.server = host;
+    }
+
+    if (ngx_list_init(&r->headers_in.headers, r->pool, 20,
+                      sizeof(ngx_table_elt_t))
+        != NGX_OK)
+    {
+        ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+
+failed:
+
+    ngx_http_finalize_request(r, NGX_HTTP_BAD_REQUEST);
+    return NGX_ERROR;
+}
+
+
+static ngx_int_t
+ngx_http_v3_process_request_header(ngx_http_request_t *r)
+{
+    ssize_t                  n;
+    ngx_buf_t               *b;
+    ngx_connection_t        *c;
+    ngx_http_v3_session_t   *h3c;
+    ngx_http_v3_srv_conf_t  *h3scf;
+
+    c = r->connection;
+
+    if (ngx_http_v3_init_pseudo_headers(r) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    h3c = ngx_http_v3_get_session(c);
+    h3scf = ngx_http_get_module_srv_conf(r, ngx_http_v3_module);
+
+    if ((h3c->hq && !h3scf->enable_hq) || (!h3c->hq && !h3scf->enable)) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "client attempted to request the server name "
+                      "for which the negotiated protocol is disabled");
+        ngx_http_finalize_request(r, NGX_HTTP_MISDIRECTED_REQUEST);
+        return NGX_ERROR;
+    }
+
+    if (ngx_http_v3_construct_cookie_header(r) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    if (r->headers_in.server.len == 0) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "client sent neither \":authority\" nor \"Host\" header");
+        goto failed;
+    }
+
+    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, c->log, 0,
+                          "client sent \":authority\" and \"Host\" headers "
+                          "with different values");
+            goto failed;
+        }
+    }
+
+    if (r->headers_in.content_length) {
+        r->headers_in.content_length_n =
+                            ngx_atoof(r->headers_in.content_length->value.data,
+                                      r->headers_in.content_length->value.len);
+
+        if (r->headers_in.content_length_n == NGX_ERROR) {
+            ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                          "client sent invalid \"Content-Length\" header");
+            goto failed;
+        }
+
+    } else {
+        b = r->header_in;
+        n = b->last - b->pos;
+
+        if (n == 0) {
+            n = c->recv(c, b->start, b->end - b->start);
+
+            if (n == NGX_ERROR) {
+                ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
+                return NGX_ERROR;
+            }
+
+            if (n > 0) {
+                b->pos = b->start;
+                b->last = b->start + n;
+            }
+        }
+
+        if (n != 0) {
+            r->headers_in.chunked = 1;
+        }
+    }
+
+    if (r->method == NGX_HTTP_CONNECT) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0, "client sent CONNECT method");
+        ngx_http_finalize_request(r, NGX_HTTP_NOT_ALLOWED);
+        return NGX_ERROR;
+    }
+
+    if (r->method == NGX_HTTP_TRACE) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0, "client sent TRACE method");
+        ngx_http_finalize_request(r, NGX_HTTP_NOT_ALLOWED);
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+
+failed:
+
+    ngx_http_finalize_request(r, NGX_HTTP_BAD_REQUEST);
+    return NGX_ERROR;
+}
+
+
+static ngx_int_t
+ngx_http_v3_cookie(ngx_http_request_t *r, ngx_str_t *value)
+{
+    ngx_str_t    *val;
+    ngx_array_t  *cookies;
+
+    cookies = r->v3_parse->cookies;
+
+    if (cookies == NULL) {
+        cookies = ngx_array_create(r->pool, 2, sizeof(ngx_str_t));
+        if (cookies == NULL) {
+            return NGX_ERROR;
+        }
+
+        r->v3_parse->cookies = cookies;
+    }
+
+    val = ngx_array_push(cookies);
+    if (val == NULL) {
+        return NGX_ERROR;
+    }
+
+    *val = *value;
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_http_v3_construct_cookie_header(ngx_http_request_t *r)
+{
+    u_char                     *buf, *p, *end;
+    size_t                      len;
+    ngx_str_t                  *vals;
+    ngx_uint_t                  i;
+    ngx_array_t                *cookies;
+    ngx_table_elt_t            *h;
+    ngx_http_header_t          *hh;
+    ngx_http_core_main_conf_t  *cmcf;
+
+    static ngx_str_t cookie = ngx_string("cookie");
+
+    cookies = r->v3_parse->cookies;
+
+    if (cookies == NULL) {
+        return NGX_OK;
+    }
+
+    vals = cookies->elts;
+
+    i = 0;
+    len = 0;
+
+    do {
+        len += vals[i].len + 2;
+    } while (++i != cookies->nelts);
+
+    len -= 2;
+
+    buf = ngx_pnalloc(r->pool, len + 1);
+    if (buf == NULL) {
+        ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
+        return NGX_ERROR;
+    }
+
+    p = buf;
+    end = buf + len;
+
+    for (i = 0; /* void */ ; i++) {
+
+        p = ngx_cpymem(p, vals[i].data, vals[i].len);
+
+        if (p == end) {
+            *p = '\0';
+            break;
+        }
+
+        *p++ = ';'; *p++ = ' ';
+    }
+
+    h = ngx_list_push(&r->headers_in.headers);
+    if (h == NULL) {
+        ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
+        return NGX_ERROR;
+    }
+
+    h->hash = ngx_hash(ngx_hash(ngx_hash(ngx_hash(
+                                    ngx_hash('c', 'o'), 'o'), 'k'), 'i'), 'e');
+
+    h->key.len = cookie.len;
+    h->key.data = cookie.data;
+
+    h->value.len = len;
+    h->value.data = buf;
+
+    h->lowcase_key = cookie.data;
+
+    cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module);
+
+    hh = ngx_hash_find(&cmcf->headers_in_hash, h->hash,
+                       h->lowcase_key, h->key.len);
+
+    if (hh == NULL) {
+        ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
+        return NGX_ERROR;
+    }
+
+    if (hh->handler(r, h, hh->offset) != NGX_OK) {
+        /*
+         * request has been finalized already
+         * in ngx_http_process_multi_header_lines()
+         */
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_http_v3_read_request_body(ngx_http_request_t *r)
+{
+    size_t                     preread;
+    ngx_int_t                  rc;
+    ngx_chain_t               *cl, out;
+    ngx_http_request_body_t   *rb;
+    ngx_http_core_loc_conf_t  *clcf;
+
+    rb = r->request_body;
+
+    preread = r->header_in->last - r->header_in->pos;
+
+    if (preread) {
+
+        /* there is the pre-read part of the request body */
+
+        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
+                       "http3 client request body preread %uz", preread);
+
+        out.buf = r->header_in;
+        out.next = NULL;
+        cl = &out;
+
+    } else {
+        cl = NULL;
+    }
+
+    rc = ngx_http_v3_request_body_filter(r, cl);
+    if (rc != NGX_OK) {
+        return rc;
+    }
+
+    if (rb->rest == 0 && rb->last_saved) {
+        /* the whole request body was pre-read */
+        r->request_body_no_buffering = 0;
+        rb->post_handler(r);
+        return NGX_OK;
+    }
+
+    if (rb->rest < 0) {
+        ngx_log_error(NGX_LOG_ALERT, r->connection->log, 0,
+                      "negative request body rest");
+        return NGX_HTTP_INTERNAL_SERVER_ERROR;
+    }
+
+    clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
+
+    rb->buf = ngx_create_temp_buf(r->pool, clcf->client_body_buffer_size);
+    if (rb->buf == NULL) {
+        return NGX_HTTP_INTERNAL_SERVER_ERROR;
+    }
+
+    r->read_event_handler = ngx_http_v3_read_client_request_body_handler;
+    r->write_event_handler = ngx_http_request_empty_handler;
+
+    return ngx_http_v3_do_read_client_request_body(r);
+}
+
+
+static void
+ngx_http_v3_read_client_request_body_handler(ngx_http_request_t *r)
+{
+    ngx_int_t  rc;
+
+    if (r->connection->read->timedout) {
+        r->connection->timedout = 1;
+        ngx_http_finalize_request(r, NGX_HTTP_REQUEST_TIME_OUT);
+        return;
+    }
+
+    rc = ngx_http_v3_do_read_client_request_body(r);
+
+    if (rc >= NGX_HTTP_SPECIAL_RESPONSE) {
+        ngx_http_finalize_request(r, rc);
+    }
+}
+
+
+ngx_int_t
+ngx_http_v3_read_unbuffered_request_body(ngx_http_request_t *r)
+{
+    ngx_int_t  rc;
+
+    if (r->connection->read->timedout) {
+        r->connection->timedout = 1;
+        return NGX_HTTP_REQUEST_TIME_OUT;
+    }
+
+    rc = ngx_http_v3_do_read_client_request_body(r);
+
+    if (rc == NGX_OK) {
+        r->reading_body = 0;
+    }
+
+    return rc;
+}
+
+
+static ngx_int_t
+ngx_http_v3_do_read_client_request_body(ngx_http_request_t *r)
+{
+    off_t                      rest;
+    size_t                     size;
+    ssize_t                    n;
+    ngx_int_t                  rc;
+    ngx_uint_t                 flush;
+    ngx_chain_t                out;
+    ngx_connection_t          *c;
+    ngx_http_request_body_t   *rb;
+    ngx_http_core_loc_conf_t  *clcf;
+
+    c = r->connection;
+    rb = r->request_body;
+    flush = 1;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 read client request body");
+
+    for ( ;; ) {
+        for ( ;; ) {
+            if (rb->rest == 0) {
+                break;
+            }
+
+            if (rb->buf->last == rb->buf->end) {
+
+                /* update chains */
+
+                rc = ngx_http_v3_request_body_filter(r, NULL);
+
+                if (rc != NGX_OK) {
+                    return rc;
+                }
+
+                if (rb->busy != NULL) {
+                    if (r->request_body_no_buffering) {
+                        if (c->read->timer_set) {
+                            ngx_del_timer(c->read);
+                        }
+
+                        if (ngx_handle_read_event(c->read, 0) != NGX_OK) {
+                            return NGX_HTTP_INTERNAL_SERVER_ERROR;
+                        }
+
+                        return NGX_AGAIN;
+                    }
+
+                    if (rb->filter_need_buffering) {
+                        clcf = ngx_http_get_module_loc_conf(r,
+                                                         ngx_http_core_module);
+                        ngx_add_timer(c->read, clcf->client_body_timeout);
+
+                        if (ngx_handle_read_event(c->read, 0) != NGX_OK) {
+                            return NGX_HTTP_INTERNAL_SERVER_ERROR;
+                        }
+
+                        return NGX_AGAIN;
+                    }
+
+                    ngx_log_error(NGX_LOG_ALERT, c->log, 0,
+                                  "busy buffers after request body flush");
+
+                    return NGX_HTTP_INTERNAL_SERVER_ERROR;
+                }
+
+                flush = 0;
+                rb->buf->pos = rb->buf->start;
+                rb->buf->last = rb->buf->start;
+            }
+
+            size = rb->buf->end - rb->buf->last;
+            rest = rb->rest - (rb->buf->last - rb->buf->pos);
+
+            if ((off_t) size > rest) {
+                size = (size_t) rest;
+            }
+
+            if (size == 0) {
+                break;
+            }
+
+            n = c->recv(c, rb->buf->last, size);
+
+            ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                           "http3 client request body recv %z", n);
+
+            if (n == NGX_AGAIN) {
+                break;
+            }
+
+            if (n == 0) {
+                rb->buf->last_buf = 1;
+            }
+
+            if (n == NGX_ERROR) {
+                c->error = 1;
+                return NGX_HTTP_BAD_REQUEST;
+            }
+
+            rb->buf->last += n;
+
+            /* pass buffer to request body filter chain */
+
+            flush = 0;
+            out.buf = rb->buf;
+            out.next = NULL;
+
+            rc = ngx_http_v3_request_body_filter(r, &out);
+
+            if (rc != NGX_OK) {
+                return rc;
+            }
+
+            if (rb->rest == 0) {
+                break;
+            }
+
+            if (rb->buf->last < rb->buf->end) {
+                break;
+            }
+        }
+
+        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 client request body rest %O", rb->rest);
+
+        if (flush) {
+            rc = ngx_http_v3_request_body_filter(r, NULL);
+
+            if (rc != NGX_OK) {
+                return rc;
+            }
+        }
+
+        if (rb->rest == 0 && rb->last_saved) {
+            break;
+        }
+
+        if (!c->read->ready || rb->rest == 0) {
+
+            clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
+            ngx_add_timer(c->read, clcf->client_body_timeout);
+
+            if (ngx_handle_read_event(c->read, 0) != NGX_OK) {
+                return NGX_HTTP_INTERNAL_SERVER_ERROR;
+            }
+
+            return NGX_AGAIN;
+        }
+    }
+
+    if (c->read->timer_set) {
+        ngx_del_timer(c->read);
+    }
+
+    if (!r->request_body_no_buffering) {
+        r->read_event_handler = ngx_http_block_reading;
+        rb->post_handler(r);
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_http_v3_request_body_filter(ngx_http_request_t *r, ngx_chain_t *in)
+{
+    off_t                      max;
+    size_t                     size;
+    u_char                    *p;
+    ngx_int_t                  rc;
+    ngx_buf_t                 *b;
+    ngx_uint_t                 last;
+    ngx_chain_t               *cl, *out, *tl, **ll;
+    ngx_http_v3_session_t     *h3c;
+    ngx_http_request_body_t   *rb;
+    ngx_http_core_loc_conf_t  *clcf;
+    ngx_http_core_srv_conf_t  *cscf;
+    ngx_http_v3_parse_data_t  *st;
+
+    rb = r->request_body;
+    st = &r->v3_parse->body;
+
+    h3c = ngx_http_v3_get_session(r->connection);
+
+    if (rb->rest == -1) {
+
+        ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
+                       "http3 request body filter");
+
+        cscf = ngx_http_get_module_srv_conf(r, ngx_http_core_module);
+
+        rb->rest = cscf->large_client_header_buffers.size;
+    }
+
+    clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
+
+    max = r->headers_in.content_length_n;
+
+    if (max == -1 && clcf->client_max_body_size) {
+        max = clcf->client_max_body_size;
+    }
+
+    out = NULL;
+    ll = &out;
+    last = 0;
+
+    for (cl = in; cl; cl = cl->next) {
+
+        ngx_log_debug7(NGX_LOG_DEBUG_EVENT, r->connection->log, 0,
+                       "http3 body buf "
+                       "t:%d f:%d %p, pos %p, size: %z file: %O, size: %O",
+                       cl->buf->temporary, cl->buf->in_file,
+                       cl->buf->start, cl->buf->pos,
+                       cl->buf->last - cl->buf->pos,
+                       cl->buf->file_pos,
+                       cl->buf->file_last - cl->buf->file_pos);
+
+        if (cl->buf->last_buf) {
+            last = 1;
+        }
+
+        b = NULL;
+
+        while (cl->buf->pos < cl->buf->last) {
+
+            if (st->length == 0) {
+                p = cl->buf->pos;
+
+                rc = ngx_http_v3_parse_data(r->connection, st, cl->buf);
+
+                r->request_length += cl->buf->pos - p;
+                h3c->total_bytes += cl->buf->pos - p;
+
+                if (ngx_http_v3_check_flood(r->connection) != NGX_OK) {
+                    return NGX_HTTP_CLOSE;
+                }
+
+                if (rc == NGX_AGAIN) {
+                    continue;
+                }
+
+                if (rc == NGX_DONE) {
+                    last = 1;
+                    goto done;
+                }
+
+                if (rc > 0) {
+                    ngx_quic_reset_stream(r->connection, rc);
+                    ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
+                                  "client sent invalid body");
+                    return NGX_HTTP_BAD_REQUEST;
+                }
+
+                if (rc == NGX_ERROR) {
+                    return NGX_HTTP_INTERNAL_SERVER_ERROR;
+                }
+
+                /* rc == NGX_OK */
+
+                if (max != -1 && (uint64_t) (max - rb->received) < st->length) {
+                    ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
+                                  "client intended to send too large "
+                                  "body: %O+%ui bytes",
+                                  rb->received, st->length);
+
+                    return NGX_HTTP_REQUEST_ENTITY_TOO_LARGE;
+                }
+
+                continue;
+            }
+
+            if (b
+                && st->length <= 128
+                && (uint64_t) (cl->buf->last - cl->buf->pos) >= st->length)
+            {
+                rb->received += st->length;
+                r->request_length += st->length;
+                h3c->total_bytes += st->length;
+                h3c->payload_bytes += st->length;
+
+                if (st->length < 8) {
+
+                    while (st->length) {
+                        *b->last++ = *cl->buf->pos++;
+                        st->length--;
+                    }
+
+                } else {
+                    ngx_memmove(b->last, cl->buf->pos, st->length);
+                    b->last += st->length;
+                    cl->buf->pos += st->length;
+                    st->length = 0;
+                }
+
+                continue;
+            }
+
+            tl = ngx_chain_get_free_buf(r->pool, &rb->free);
+            if (tl == NULL) {
+                return NGX_HTTP_INTERNAL_SERVER_ERROR;
+            }
+
+            b = tl->buf;
+
+            ngx_memzero(b, sizeof(ngx_buf_t));
+
+            b->temporary = 1;
+            b->tag = (ngx_buf_tag_t) &ngx_http_read_client_request_body;
+            b->start = cl->buf->pos;
+            b->pos = cl->buf->pos;
+            b->last = cl->buf->last;
+            b->end = cl->buf->end;
+            b->flush = r->request_body_no_buffering;
+
+            *ll = tl;
+            ll = &tl->next;
+
+            size = cl->buf->last - cl->buf->pos;
+
+            if (size > st->length) {
+                cl->buf->pos += (size_t) st->length;
+                rb->received += st->length;
+                r->request_length += st->length;
+                h3c->total_bytes += st->length;
+                h3c->payload_bytes += st->length;
+                st->length = 0;
+
+            } else {
+                st->length -= size;
+                rb->received += size;
+                r->request_length += size;
+                h3c->total_bytes += size;
+                h3c->payload_bytes += size;
+                cl->buf->pos = cl->buf->last;
+            }
+
+            b->last = cl->buf->pos;
+        }
+    }
+
+done:
+
+    if (last) {
+
+        if (st->length > 0) {
+            ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
+                          "client prematurely closed stream");
+            r->connection->error = 1;
+            return NGX_HTTP_BAD_REQUEST;
+        }
+
+        if (r->headers_in.content_length_n == -1) {
+            r->headers_in.content_length_n = rb->received;
+
+        } else if (r->headers_in.content_length_n != rb->received) {
+            ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
+                          "client sent less body data than expected: "
+                          "%O out of %O bytes of request body received",
+                          rb->received, r->headers_in.content_length_n);
+            return NGX_HTTP_BAD_REQUEST;
+        }
+
+        rb->rest = 0;
+
+        tl = ngx_chain_get_free_buf(r->pool, &rb->free);
+        if (tl == NULL) {
+            return NGX_HTTP_INTERNAL_SERVER_ERROR;
+        }
+
+        b = tl->buf;
+
+        ngx_memzero(b, sizeof(ngx_buf_t));
+
+        b->last_buf = 1;
+
+        *ll = tl;
+
+    } else {
+
+        /* set rb->rest, amount of data we want to see next time */
+
+        cscf = ngx_http_get_module_srv_conf(r, ngx_http_core_module);
+
+        rb->rest = (off_t) cscf->large_client_header_buffers.size;
+    }
+
+    rc = ngx_http_top_request_body_filter(r, out);
+
+    ngx_chain_update_chains(r->pool, &rb->free, &rb->busy, &out,
+                            (ngx_buf_tag_t) &ngx_http_read_client_request_body);
+
+    return rc;
+}
diff --git a/src/http/v3/ngx_http_v3_table.c b/src/http/v3/ngx_http_v3_table.c
new file mode 100644
index 0000000..428e732
--- /dev/null
+++ b/src/http/v3/ngx_http_v3_table.c
@@ -0,0 +1,715 @@
+
+/*
+ * 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 target);
+static void ngx_http_v3_unblock(void *data);
+static ngx_int_t ngx_http_v3_new_entry(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_field_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;
+    ngx_http_v3_session_t        *h3c;
+    ngx_http_v3_dynamic_table_t  *dt;
+
+    if (dynamic) {
+        ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 ref insert dynamic[%ui] \"%V\"", index, value);
+
+        h3c = ngx_http_v3_get_session(c);
+        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, 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_field_t          *field;
+    ngx_http_v3_session_t        *h3c;
+    ngx_http_v3_dynamic_table_t  *dt;
+
+    size = ngx_http_v3_table_entry_size(name, value);
+
+    h3c = ngx_http_v3_get_session(c);
+    dt = &h3c->table;
+
+    if (size > dt->capacity) {
+        ngx_log_error(NGX_LOG_ERR, c->log, 0,
+                      "not enough dynamic table capacity");
+        return NGX_HTTP_V3_ERR_ENCODER_STREAM_ERROR;
+    }
+
+    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_field_t) + name->len + value->len,
+                  c->log);
+    if (p == NULL) {
+        return NGX_ERROR;
+    }
+
+    field = (ngx_http_v3_field_t *) p;
+
+    field->name.data = p + sizeof(ngx_http_v3_field_t);
+    field->name.len = name->len;
+    field->value.data = ngx_cpymem(field->name.data, name->data, name->len);
+    field->value.len = value->len;
+    ngx_memcpy(field->value.data, value->data, value->len);
+
+    dt->elts[dt->nelts++] = field;
+    dt->size += size;
+
+    dt->insert_count++;
+
+    if (ngx_http_v3_evict(c, dt->capacity) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    ngx_post_event(&dt->send_insert_count, &ngx_posted_events);
+
+    if (ngx_http_v3_new_entry(c) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}
+
+
+void
+ngx_http_v3_inc_insert_count_handler(ngx_event_t *ev)
+{
+    ngx_connection_t             *c;
+    ngx_http_v3_session_t        *h3c;
+    ngx_http_v3_dynamic_table_t  *dt;
+
+    c = ev->data;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 inc insert count handler");
+
+    h3c = ngx_http_v3_get_session(c);
+    dt = &h3c->table;
+
+    if (dt->insert_count > dt->ack_insert_count) {
+        if (ngx_http_v3_send_inc_insert_count(c,
+                                       dt->insert_count - dt->ack_insert_count)
+            != NGX_OK)
+        {
+            return;
+        }
+
+        dt->ack_insert_count = dt->insert_count;
+    }
+}
+
+
+ngx_int_t
+ngx_http_v3_set_capacity(ngx_connection_t *c, ngx_uint_t capacity)
+{
+    ngx_uint_t                     max, prev_max;
+    ngx_http_v3_field_t          **elts;
+    ngx_http_v3_session_t         *h3c;
+    ngx_http_v3_srv_conf_t        *h3scf;
+    ngx_http_v3_dynamic_table_t   *dt;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 set capacity %ui", capacity);
+
+    h3c = ngx_http_v3_get_session(c);
+    h3scf = ngx_http_v3_get_module_srv_conf(c, 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;
+    }
+
+    if (ngx_http_v3_evict(c, capacity) != NGX_OK) {
+        return NGX_HTTP_V3_ERR_ENCODER_STREAM_ERROR;
+    }
+
+    dt = &h3c->table;
+    max = capacity / 32;
+    prev_max = dt->capacity / 32;
+
+    if (max > prev_max) {
+        elts = ngx_alloc((max + 1) * sizeof(void *), c->log);
+        if (elts == NULL) {
+            return NGX_ERROR;
+        }
+
+        if (dt->elts) {
+            ngx_memcpy(elts, dt->elts, dt->nelts * sizeof(void *));
+            ngx_free(dt->elts);
+        }
+
+        dt->elts = elts;
+    }
+
+    dt->capacity = capacity;
+
+    return NGX_OK;
+}
+
+
+void
+ngx_http_v3_cleanup_table(ngx_http_v3_session_t *h3c)
+{
+    ngx_uint_t                    n;
+    ngx_http_v3_dynamic_table_t  *dt;
+
+    dt = &h3c->table;
+
+    if (dt->elts == NULL) {
+        return;
+    }
+
+    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 target)
+{
+    size_t                        size;
+    ngx_uint_t                    n;
+    ngx_http_v3_field_t          *field;
+    ngx_http_v3_session_t        *h3c;
+    ngx_http_v3_dynamic_table_t  *dt;
+
+    h3c = ngx_http_v3_get_session(c);
+    dt = &h3c->table;
+    n = 0;
+
+    while (dt->size > target) {
+        field = dt->elts[n++];
+        size = ngx_http_v3_table_entry_size(&field->name, &field->value);
+
+        ngx_log_debug4(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 evict [%ui] \"%V\":\"%V\" size:%uz",
+                       dt->base, &field->name, &field->value, size);
+
+        ngx_free(field);
+        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_session_t        *h3c;
+    ngx_http_v3_dynamic_table_t  *dt;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 duplicate %ui", index);
+
+    h3c = ngx_http_v3_get_session(c);
+    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_section(ngx_connection_t *c, ngx_uint_t stream_id)
+{
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 ack section %ui", stream_id);
+
+    /* we do not use dynamic tables */
+
+    return NGX_HTTP_V3_ERR_DECODER_STREAM_ERROR;
+}
+
+
+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);
+
+    /* we do not use dynamic tables */
+
+    return NGX_HTTP_V3_ERR_DECODER_STREAM_ERROR;
+}
+
+
+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_field_t  *field;
+
+    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;
+    }
+
+    field = &ngx_http_v3_static_table[index];
+
+    ngx_log_debug3(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 static[%ui] lookup \"%V\":\"%V\"",
+                   index, &field->name, &field->value);
+
+    if (name) {
+        *name = field->name;
+    }
+
+    if (value) {
+        *value = field->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_field_t          *field;
+    ngx_http_v3_session_t        *h3c;
+    ngx_http_v3_dynamic_table_t  *dt;
+
+    h3c = ngx_http_v3_get_session(c);
+    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;
+    }
+
+    field = dt->elts[index - dt->base];
+
+    ngx_log_debug3(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 dynamic[%ui] lookup \"%V\":\"%V\"",
+                   index, &field->name, &field->value);
+
+    if (name) {
+        *name = field->name;
+    }
+
+    if (value) {
+        *value = field->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_session_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 = ngx_http_v3_get_session(c);
+    dt = &h3c->table;
+
+    h3scf = ngx_http_v3_get_module_srv_conf(c, 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_pool_cleanup_t           *cln;
+    ngx_http_v3_block_t          *block;
+    ngx_http_v3_session_t        *h3c;
+    ngx_http_v3_srv_conf_t       *h3scf;
+    ngx_http_v3_dynamic_table_t  *dt;
+
+    h3c = ngx_http_v3_get_session(c);
+    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_v3_get_module_srv_conf(c, 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");
+
+            ngx_http_v3_finalize_connection(c,
+                                          NGX_HTTP_V3_ERR_DECOMPRESSION_FAILED,
+                                          "too many blocked streams");
+            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;
+}
+
+
+void
+ngx_http_v3_ack_insert_count(ngx_connection_t *c, uint64_t insert_count)
+{
+    ngx_http_v3_session_t        *h3c;
+    ngx_http_v3_dynamic_table_t  *dt;
+
+    h3c = ngx_http_v3_get_session(c);
+    dt = &h3c->table;
+
+    if (dt->ack_insert_count < insert_count) {
+        dt->ack_insert_count = insert_count;
+    }
+}
+
+
+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_entry(ngx_connection_t *c)
+{
+    ngx_queue_t            *q;
+    ngx_connection_t       *bc;
+    ngx_http_v3_block_t    *block;
+    ngx_http_v3_session_t  *h3c;
+
+    h3c = ngx_http_v3_get_session(c);
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 new dynamic entry, 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_FIELD_SECTION_SIZE:
+        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 param SETTINGS_MAX_FIELD_SECTION_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;
+}
diff --git a/src/http/v3/ngx_http_v3_table.h b/src/http/v3/ngx_http_v3_table.h
new file mode 100644
index 0000000..1c2fb17
--- /dev/null
+++ b/src/http/v3/ngx_http_v3_table.h
@@ -0,0 +1,58 @@
+
+/*
+ * Copyright (C) Roman Arutyunyan
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_HTTP_V3_TABLE_H_INCLUDED_
+#define _NGX_HTTP_V3_TABLE_H_INCLUDED_
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_http.h>
+
+
+typedef struct {
+    ngx_str_t                     name;
+    ngx_str_t                     value;
+} ngx_http_v3_field_t;
+
+
+typedef struct {
+    ngx_http_v3_field_t         **elts;
+    ngx_uint_t                    nelts;
+    ngx_uint_t                    base;
+    size_t                        size;
+    size_t                        capacity;
+    uint64_t                      insert_count;
+    uint64_t                      ack_insert_count;
+    ngx_event_t                   send_insert_count;
+} ngx_http_v3_dynamic_table_t;
+
+
+void ngx_http_v3_inc_insert_count_handler(ngx_event_t *ev);
+void ngx_http_v3_cleanup_table(ngx_http_v3_session_t *h3c);
+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_section(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);
+void ngx_http_v3_ack_insert_count(ngx_connection_t *c, uint64_t insert_count);
+ngx_int_t ngx_http_v3_set_param(ngx_connection_t *c, uint64_t id,
+    uint64_t value);
+
+
+#endif /* _NGX_HTTP_V3_TABLE_H_INCLUDED_ */
diff --git a/src/http/v3/ngx_http_v3_uni.c b/src/http/v3/ngx_http_v3_uni.c
new file mode 100644
index 0000000..302064b
--- /dev/null
+++ b/src/http/v3/ngx_http_v3_uni.c
@@ -0,0 +1,622 @@
+
+/*
+ * Copyright (C) Roman Arutyunyan
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_http.h>
+
+
+typedef struct {
+    ngx_http_v3_parse_uni_t         parse;
+    ngx_int_t                       index;
+} ngx_http_v3_uni_stream_t;
+
+
+static void ngx_http_v3_close_uni_stream(ngx_connection_t *c);
+static void ngx_http_v3_uni_read_handler(ngx_event_t *rev);
+static void ngx_http_v3_uni_dummy_read_handler(ngx_event_t *wev);
+static void ngx_http_v3_uni_dummy_write_handler(ngx_event_t *wev);
+
+
+void
+ngx_http_v3_init_uni_stream(ngx_connection_t *c)
+{
+    uint64_t                   n;
+    ngx_http_v3_session_t     *h3c;
+    ngx_http_v3_uni_stream_t  *us;
+
+    h3c = ngx_http_v3_get_session(c);
+    if (h3c->hq) {
+        ngx_http_v3_finalize_connection(c,
+                                        NGX_HTTP_V3_ERR_STREAM_CREATION_ERROR,
+                                        "uni stream in hq mode");
+        c->data = NULL;
+        ngx_http_v3_close_uni_stream(c);
+        return;
+    }
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 init uni stream");
+
+    n = c->quic->id >> 2;
+
+    if (n >= NGX_HTTP_V3_MAX_UNI_STREAMS) {
+        ngx_http_v3_finalize_connection(c,
+                                      NGX_HTTP_V3_ERR_STREAM_CREATION_ERROR,
+                                      "reached maximum number of uni streams");
+        c->data = NULL;
+        ngx_http_v3_close_uni_stream(c);
+        return;
+    }
+
+    ngx_quic_cancelable_stream(c);
+
+    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,
+                                        "memory allocation error");
+        c->data = NULL;
+        ngx_http_v3_close_uni_stream(c);
+        return;
+    }
+
+    us->index = -1;
+
+    c->data = us;
+
+    c->read->handler = ngx_http_v3_uni_read_handler;
+    c->write->handler = ngx_http_v3_uni_dummy_write_handler;
+
+    ngx_http_v3_uni_read_handler(c->read);
+}
+
+
+static void
+ngx_http_v3_close_uni_stream(ngx_connection_t *c)
+{
+    ngx_pool_t                *pool;
+    ngx_http_v3_session_t     *h3c;
+    ngx_http_v3_uni_stream_t  *us;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 close stream");
+
+    us = c->data;
+
+    if (us && us->index >= 0) {
+        h3c = ngx_http_v3_get_session(c);
+        h3c->known_streams[us->index] = NULL;
+    }
+
+    c->destroyed = 1;
+
+    pool = c->pool;
+
+    ngx_close_connection(c);
+
+    ngx_destroy_pool(pool);
+}
+
+
+ngx_int_t
+ngx_http_v3_register_uni_stream(ngx_connection_t *c, uint64_t type)
+{
+    ngx_int_t                  index;
+    ngx_http_v3_session_t     *h3c;
+    ngx_http_v3_uni_stream_t  *us;
+
+    h3c = ngx_http_v3_get_session(c);
+
+    switch (type) {
+
+    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;
+        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;
+        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;
+
+        break;
+
+    default:
+
+        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                       "http3 stream 0x%02xL", type);
+
+        if (h3c->known_streams[NGX_HTTP_V3_STREAM_CLIENT_ENCODER] == NULL
+            || h3c->known_streams[NGX_HTTP_V3_STREAM_CLIENT_DECODER] == NULL
+            || h3c->known_streams[NGX_HTTP_V3_STREAM_CLIENT_CONTROL] == NULL)
+        {
+            ngx_log_error(NGX_LOG_INFO, c->log, 0, "missing mandatory stream");
+            return NGX_HTTP_V3_ERR_STREAM_CREATION_ERROR;
+        }
+
+        index = -1;
+    }
+
+    if (index >= 0) {
+        if (h3c->known_streams[index]) {
+            ngx_log_error(NGX_LOG_INFO, c->log, 0, "stream exists");
+            return NGX_HTTP_V3_ERR_STREAM_CREATION_ERROR;
+        }
+
+        h3c->known_streams[index] = c;
+
+        us = c->data;
+        us->index = index;
+    }
+
+    return NGX_OK;
+}
+
+
+static void
+ngx_http_v3_uni_read_handler(ngx_event_t *rev)
+{
+    u_char                     buf[128];
+    ssize_t                    n;
+    ngx_buf_t                  b;
+    ngx_int_t                  rc;
+    ngx_connection_t          *c;
+    ngx_http_v3_session_t     *h3c;
+    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");
+
+    if (c->close) {
+        ngx_http_v3_close_uni_stream(c);
+        return;
+    }
+
+    ngx_memzero(&b, sizeof(ngx_buf_t));
+
+    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;
+        }
+
+        b.pos = buf;
+        b.last = buf + n;
+
+        h3c = ngx_http_v3_get_session(c);
+        h3c->total_bytes += n;
+
+        if (ngx_http_v3_check_flood(c) != NGX_OK) {
+            ngx_http_v3_close_uni_stream(c);
+            return;
+        }
+
+        rc = ngx_http_v3_parse_uni(c, &us->parse, &b);
+
+        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_uni_dummy_read_handler(ngx_event_t *rev)
+{
+    u_char             ch;
+    ngx_connection_t  *c;
+
+    c = rev->data;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 dummy read handler");
+
+    if (c->close) {
+        ngx_http_v3_close_uni_stream(c);
+        return;
+    }
+
+    if (rev->ready) {
+        if (c->recv(c, &ch, 1) != 0) {
+            ngx_http_v3_finalize_connection(c, NGX_HTTP_V3_ERR_NO_ERROR, NULL);
+            ngx_http_v3_close_uni_stream(c);
+            return;
+        }
+    }
+
+    if (ngx_handle_read_event(rev, 0) != NGX_OK) {
+        ngx_http_v3_finalize_connection(c, NGX_HTTP_V3_ERR_INTERNAL_ERROR,
+                                        NULL);
+        ngx_http_v3_close_uni_stream(c);
+    }
+}
+
+
+static void
+ngx_http_v3_uni_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);
+    }
+}
+
+
+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_session_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 = ngx_http_v3_get_session(c);
+
+    if (index >= 0) {
+        if (h3c->known_streams[index]) {
+            return h3c->known_streams[index];
+        }
+    }
+
+    sc = ngx_quic_open_stream(c, 0);
+    if (sc == NULL) {
+        goto failed;
+    }
+
+    ngx_quic_cancelable_stream(sc);
+
+    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_dummy_read_handler;
+    sc->write->handler = ngx_http_v3_uni_dummy_write_handler;
+
+    if (index >= 0) {
+        h3c->known_streams[index] = sc;
+    }
+
+    n = (u_char *) ngx_http_v3_encode_varlen_int(buf, type) - buf;
+
+    h3c = ngx_http_v3_get_session(c);
+    h3c->total_bytes += n;
+
+    if (sc->send(sc, buf, n) != (ssize_t) n) {
+        goto failed;
+    }
+
+    ngx_post_event(sc->read, &ngx_posted_events);
+
+    return sc;
+
+failed:
+
+    ngx_log_error(NGX_LOG_ERR, c->log, 0, "failed to create server stream");
+
+    ngx_http_v3_finalize_connection(c, NGX_HTTP_V3_ERR_STREAM_CREATION_ERROR,
+                                    "failed to create server stream");
+    if (sc) {
+        ngx_http_v3_close_uni_stream(sc);
+    }
+
+    return NULL;
+}
+
+
+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_session_t   *h3c;
+    ngx_http_v3_srv_conf_t  *h3scf;
+
+    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_ERROR;
+    }
+
+    h3scf = ngx_http_v3_get_module_srv_conf(c, 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;
+
+    h3c = ngx_http_v3_get_session(c);
+    h3c->total_bytes += n;
+
+    if (cc->send(cc, buf, n) != (ssize_t) n) {
+        goto failed;
+    }
+
+    return NGX_OK;
+
+failed:
+
+    ngx_log_error(NGX_LOG_ERR, c->log, 0, "failed to send settings");
+
+    ngx_http_v3_finalize_connection(c, NGX_HTTP_V3_ERR_EXCESSIVE_LOAD,
+                                    "failed to send settings");
+    ngx_http_v3_close_uni_stream(cc);
+
+    return NGX_ERROR;
+}
+
+
+ngx_int_t
+ngx_http_v3_send_goaway(ngx_connection_t *c, uint64_t id)
+{
+    u_char                 *p, buf[NGX_HTTP_V3_VARLEN_INT_LEN * 3];
+    size_t                  n;
+    ngx_connection_t       *cc;
+    ngx_http_v3_session_t  *h3c;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 send goaway %uL", id);
+
+    cc = ngx_http_v3_get_uni_stream(c, NGX_HTTP_V3_STREAM_CONTROL);
+    if (cc == NULL) {
+        return NGX_ERROR;
+    }
+
+    n = ngx_http_v3_encode_varlen_int(NULL, id);
+    p = (u_char *) ngx_http_v3_encode_varlen_int(buf, NGX_HTTP_V3_FRAME_GOAWAY);
+    p = (u_char *) ngx_http_v3_encode_varlen_int(p, n);
+    p = (u_char *) ngx_http_v3_encode_varlen_int(p, id);
+    n = p - buf;
+
+    h3c = ngx_http_v3_get_session(c);
+    h3c->total_bytes += n;
+
+    if (cc->send(cc, buf, n) != (ssize_t) n) {
+        goto failed;
+    }
+
+    return NGX_OK;
+
+failed:
+
+    ngx_log_error(NGX_LOG_ERR, c->log, 0, "failed to send goaway");
+
+    ngx_http_v3_finalize_connection(c, NGX_HTTP_V3_ERR_EXCESSIVE_LOAD,
+                                    "failed to send goaway");
+    ngx_http_v3_close_uni_stream(cc);
+
+    return NGX_ERROR;
+}
+
+
+ngx_int_t
+ngx_http_v3_send_ack_section(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_http_v3_session_t  *h3c;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 send section acknowledgement %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;
+
+    h3c = ngx_http_v3_get_session(c);
+    h3c->total_bytes += n;
+
+    if (dc->send(dc, buf, n) != (ssize_t) n) {
+        goto failed;
+    }
+
+    return NGX_OK;
+
+failed:
+
+    ngx_log_error(NGX_LOG_ERR, c->log, 0,
+                  "failed to send section acknowledgement");
+
+    ngx_http_v3_finalize_connection(c, NGX_HTTP_V3_ERR_EXCESSIVE_LOAD,
+                                    "failed to send section acknowledgement");
+    ngx_http_v3_close_uni_stream(dc);
+
+    return NGX_ERROR;
+}
+
+
+ngx_int_t
+ngx_http_v3_send_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_http_v3_session_t  *h3c;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 send stream cancellation %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;
+
+    h3c = ngx_http_v3_get_session(c);
+    h3c->total_bytes += n;
+
+    if (dc->send(dc, buf, n) != (ssize_t) n) {
+        goto failed;
+    }
+
+    return NGX_OK;
+
+failed:
+
+    ngx_log_error(NGX_LOG_ERR, c->log, 0, "failed to send stream cancellation");
+
+    ngx_http_v3_finalize_connection(c, NGX_HTTP_V3_ERR_EXCESSIVE_LOAD,
+                                    "failed to send stream cancellation");
+    ngx_http_v3_close_uni_stream(dc);
+
+    return NGX_ERROR;
+}
+
+
+ngx_int_t
+ngx_http_v3_send_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_http_v3_session_t  *h3c;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+                   "http3 send insert count increment %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;
+
+    h3c = ngx_http_v3_get_session(c);
+    h3c->total_bytes += n;
+
+    if (dc->send(dc, buf, n) != (ssize_t) n) {
+        goto failed;
+    }
+
+    return NGX_OK;
+
+failed:
+
+    ngx_log_error(NGX_LOG_ERR, c->log, 0,
+                  "failed to send insert count increment");
+
+    ngx_http_v3_finalize_connection(c, NGX_HTTP_V3_ERR_EXCESSIVE_LOAD,
+                                    "failed to send insert count increment");
+    ngx_http_v3_close_uni_stream(dc);
+
+    return NGX_ERROR;
+}
+
+
+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);
+
+    /* we do not use dynamic tables */
+
+    return NGX_OK;
+}
diff --git a/src/http/v3/ngx_http_v3_uni.h b/src/http/v3/ngx_http_v3_uni.h
new file mode 100644
index 0000000..2805894
--- /dev/null
+++ b/src/http/v3/ngx_http_v3_uni.h
@@ -0,0 +1,34 @@
+
+/*
+ * Copyright (C) Roman Arutyunyan
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_HTTP_V3_UNI_H_INCLUDED_
+#define _NGX_HTTP_V3_UNI_H_INCLUDED_
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_http.h>
+
+
+void ngx_http_v3_init_uni_stream(ngx_connection_t *c);
+ngx_int_t ngx_http_v3_register_uni_stream(ngx_connection_t *c, uint64_t type);
+
+ngx_int_t ngx_http_v3_cancel_stream(ngx_connection_t *c, ngx_uint_t stream_id);
+
+ngx_connection_t *ngx_http_v3_get_uni_stream(ngx_connection_t *c,
+    ngx_uint_t type);
+ngx_int_t ngx_http_v3_send_settings(ngx_connection_t *c);
+ngx_int_t ngx_http_v3_send_goaway(ngx_connection_t *c, uint64_t id);
+ngx_int_t ngx_http_v3_send_ack_section(ngx_connection_t *c,
+    ngx_uint_t stream_id);
+ngx_int_t ngx_http_v3_send_cancel_stream(ngx_connection_t *c,
+    ngx_uint_t stream_id);
+ngx_int_t ngx_http_v3_send_inc_insert_count(ngx_connection_t *c,
+    ngx_uint_t inc);
+
+
+#endif /* _NGX_HTTP_V3_UNI_H_INCLUDED_ */
diff --git a/src/mail/ngx_mail_core_module.c b/src/mail/ngx_mail_core_module.c
index 115671c..228a8d0 100644
--- a/src/mail/ngx_mail_core_module.c
+++ b/src/mail/ngx_mail_core_module.c
@@ -308,7 +308,7 @@ ngx_mail_core_listen(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
     ngx_str_t                  *value, size;
     ngx_url_t                   u;
     ngx_uint_t                  i, n, m;
-    ngx_mail_listen_t          *ls, *als;
+    ngx_mail_listen_t          *ls, *als, *nls;
     ngx_mail_module_t          *module;
     ngx_mail_core_main_conf_t  *cmcf;
 
@@ -333,7 +333,7 @@ ngx_mail_core_listen(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
 
     cmcf = ngx_mail_conf_get_module_main_conf(cf, ngx_mail_core_module);
 
-    ls = ngx_array_push_n(&cmcf->listen, u.naddrs);
+    ls = ngx_array_push(&cmcf->listen);
     if (ls == NULL) {
         return NGX_CONF_ERROR;
     }
@@ -441,7 +441,7 @@ ngx_mail_core_listen(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
             continue;
 #else
             ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
-                               "bind ipv6only is not supported "
+                               "ipv6only is not supported "
                                "on this platform");
             return NGX_CONF_ERROR;
 #endif
@@ -564,24 +564,44 @@ ngx_mail_core_listen(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
         }
 
         ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
-                           "the invalid \"%V\" parameter", &value[i]);
+                           "invalid parameter \"%V\"", &value[i]);
         return NGX_CONF_ERROR;
     }
 
-    als = cmcf->listen.elts;
-
     for (n = 0; n < u.naddrs; n++) {
-        ls[n] = ls[0];
 
-        ls[n].sockaddr = u.addrs[n].sockaddr;
-        ls[n].socklen = u.addrs[n].socklen;
-        ls[n].addr_text = u.addrs[n].name;
-        ls[n].wildcard = ngx_inet_wildcard(ls[n].sockaddr);
+        for (i = 0; i < n; i++) {
+            if (ngx_cmp_sockaddr(u.addrs[n].sockaddr, u.addrs[n].socklen,
+                                 u.addrs[i].sockaddr, u.addrs[i].socklen, 1)
+                == NGX_OK)
+            {
+                goto next;
+            }
+        }
+
+        if (n != 0) {
+            nls = ngx_array_push(&cmcf->listen);
+            if (nls == NULL) {
+                return NGX_CONF_ERROR;
+            }
+
+            *nls = *ls;
+
+        } else {
+            nls = ls;
+        }
 
-        for (i = 0; i < cmcf->listen.nelts - u.naddrs + n; i++) {
+        nls->sockaddr = u.addrs[n].sockaddr;
+        nls->socklen = u.addrs[n].socklen;
+        nls->addr_text = u.addrs[n].name;
+        nls->wildcard = ngx_inet_wildcard(nls->sockaddr);
+
+        als = cmcf->listen.elts;
+
+        for (i = 0; i < cmcf->listen.nelts - 1; i++) {
 
             if (ngx_cmp_sockaddr(als[i].sockaddr, als[i].socklen,
-                                 ls[n].sockaddr, ls[n].socklen, 1)
+                                 nls->sockaddr, nls->socklen, 1)
                 != NGX_OK)
             {
                 continue;
@@ -589,9 +609,12 @@ ngx_mail_core_listen(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
 
             ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                                "duplicate \"%V\" address and port pair",
-                               &ls[n].addr_text);
+                               &nls->addr_text);
             return NGX_CONF_ERROR;
         }
+
+    next:
+        continue;
     }
 
     return NGX_CONF_OK;
diff --git a/src/mail/ngx_mail_handler.c b/src/mail/ngx_mail_handler.c
index 246ba97..1167df3 100644
--- a/src/mail/ngx_mail_handler.c
+++ b/src/mail/ngx_mail_handler.c
@@ -283,11 +283,11 @@ ngx_mail_init_session_handler(ngx_event_t *rev)
 
     s = c->data;
 
-    sslcf = ngx_mail_get_module_srv_conf(s, ngx_mail_ssl_module);
-
-    if (sslcf->enable || s->ssl) {
+    if (s->ssl) {
         c->log->action = "SSL handshaking";
 
+        sslcf = ngx_mail_get_module_srv_conf(s, ngx_mail_ssl_module);
+
         ngx_mail_ssl_init_connection(&sslcf->ssl, c);
         return;
     }
diff --git a/src/mail/ngx_mail_proxy_module.c b/src/mail/ngx_mail_proxy_module.c
index a7ab077..efed9ab 100644
--- a/src/mail/ngx_mail_proxy_module.c
+++ b/src/mail/ngx_mail_proxy_module.c
@@ -327,7 +327,9 @@ ngx_mail_proxy_pop3_handler(ngx_event_t *rev)
         c->log->action = NULL;
         ngx_log_error(NGX_LOG_INFO, c->log, 0, "client logged in");
 
-        if (s->buffer->pos < s->buffer->last) {
+        if (s->buffer->pos < s->buffer->last
+            || s->connection->read->ready)
+        {
             ngx_post_event(c->write, &ngx_posted_events);
         }
 
@@ -486,7 +488,9 @@ ngx_mail_proxy_imap_handler(ngx_event_t *rev)
         c->log->action = NULL;
         ngx_log_error(NGX_LOG_INFO, c->log, 0, "client logged in");
 
-        if (s->buffer->pos < s->buffer->last) {
+        if (s->buffer->pos < s->buffer->last
+            || s->connection->read->ready)
+        {
             ngx_post_event(c->write, &ngx_posted_events);
         }
 
@@ -821,7 +825,9 @@ ngx_mail_proxy_smtp_handler(ngx_event_t *rev)
         c->log->action = NULL;
         ngx_log_error(NGX_LOG_INFO, c->log, 0, "client logged in");
 
-        if (s->buffer->pos < s->buffer->last) {
+        if (s->buffer->pos < s->buffer->last
+            || s->connection->read->ready)
+        {
             ngx_post_event(c->write, &ngx_posted_events);
         }
 
@@ -890,7 +896,7 @@ ngx_mail_proxy_send_proxy_protocol(ngx_mail_session_t *s)
     u_char            *p;
     ssize_t            n, size;
     ngx_connection_t  *c;
-    u_char             buf[NGX_PROXY_PROTOCOL_MAX_HEADER];
+    u_char             buf[NGX_PROXY_PROTOCOL_V1_MAX_HEADER];
 
     s->connection->log->action = "sending PROXY protocol header to upstream";
 
@@ -898,7 +904,7 @@ ngx_mail_proxy_send_proxy_protocol(ngx_mail_session_t *s)
                    "mail proxy send PROXY protocol header");
 
     p = ngx_proxy_protocol_write(s->connection, buf,
-                                 buf + NGX_PROXY_PROTOCOL_MAX_HEADER);
+                                 buf + NGX_PROXY_PROTOCOL_V1_MAX_HEADER);
     if (p == NULL) {
         ngx_mail_proxy_internal_server_error(s);
         return NGX_ERROR;
diff --git a/src/mail/ngx_mail_ssl_module.c b/src/mail/ngx_mail_ssl_module.c
index 2a1043e..aebb4cc 100644
--- a/src/mail/ngx_mail_ssl_module.c
+++ b/src/mail/ngx_mail_ssl_module.c
@@ -23,8 +23,6 @@ static int ngx_mail_ssl_alpn_select(ngx_ssl_conn_t *ssl_conn,
 static void *ngx_mail_ssl_create_conf(ngx_conf_t *cf);
 static char *ngx_mail_ssl_merge_conf(ngx_conf_t *cf, void *parent, void *child);
 
-static char *ngx_mail_ssl_enable(ngx_conf_t *cf, ngx_command_t *cmd,
-    void *conf);
 static char *ngx_mail_ssl_starttls(ngx_conf_t *cf, ngx_command_t *cmd,
     void *conf);
 static char *ngx_mail_ssl_password_file(ngx_conf_t *cf, ngx_command_t *cmd,
@@ -65,24 +63,12 @@ static ngx_conf_enum_t  ngx_mail_ssl_verify[] = {
 };
 
 
-static ngx_conf_deprecated_t  ngx_mail_ssl_deprecated = {
-    ngx_conf_deprecated, "ssl", "listen ... ssl"
-};
-
-
 static ngx_conf_post_t  ngx_mail_ssl_conf_command_post =
     { ngx_mail_ssl_conf_command_check };
 
 
 static ngx_command_t  ngx_mail_ssl_commands[] = {
 
-    { ngx_string("ssl"),
-      NGX_MAIL_MAIN_CONF|NGX_MAIL_SRV_CONF|NGX_CONF_FLAG,
-      ngx_mail_ssl_enable,
-      NGX_MAIL_SRV_CONF_OFFSET,
-      offsetof(ngx_mail_ssl_conf_t, enable),
-      &ngx_mail_ssl_deprecated },
-
     { ngx_string("starttls"),
       NGX_MAIL_MAIN_CONF|NGX_MAIL_SRV_CONF|NGX_CONF_TAKE1,
       ngx_mail_ssl_starttls,
@@ -322,7 +308,6 @@ ngx_mail_ssl_create_conf(ngx_conf_t *cf)
      *     scf->shm_zone = NULL;
      */
 
-    scf->enable = NGX_CONF_UNSET;
     scf->starttls = NGX_CONF_UNSET_UINT;
     scf->certificates = NGX_CONF_UNSET_PTR;
     scf->certificate_keys = NGX_CONF_UNSET_PTR;
@@ -349,7 +334,6 @@ ngx_mail_ssl_merge_conf(ngx_conf_t *cf, void *parent, void *child)
     char                *mode;
     ngx_pool_cleanup_t  *cln;
 
-    ngx_conf_merge_value(conf->enable, prev->enable, 0);
     ngx_conf_merge_uint_value(conf->starttls, prev->starttls,
                          NGX_MAIL_STARTTLS_OFF);
 
@@ -360,8 +344,9 @@ ngx_mail_ssl_merge_conf(ngx_conf_t *cf, void *parent, void *child)
                          prev->prefer_server_ciphers, 0);
 
     ngx_conf_merge_bitmask_value(conf->protocols, prev->protocols,
-                         (NGX_CONF_BITMASK_SET|NGX_SSL_TLSv1
-                          |NGX_SSL_TLSv1_1|NGX_SSL_TLSv1_2));
+                         (NGX_CONF_BITMASK_SET
+                          |NGX_SSL_TLSv1|NGX_SSL_TLSv1_1
+                          |NGX_SSL_TLSv1_2|NGX_SSL_TLSv1_3));
 
     ngx_conf_merge_uint_value(conf->verify, prev->verify, 0);
     ngx_conf_merge_uint_value(conf->verify_depth, prev->verify_depth, 1);
@@ -393,9 +378,6 @@ ngx_mail_ssl_merge_conf(ngx_conf_t *cf, void *parent, void *child)
     if (conf->listen) {
         mode = "listen ... ssl";
 
-    } else if (conf->enable) {
-        mode = "ssl";
-
     } else if (conf->starttls != NGX_MAIL_STARTTLS_OFF) {
         mode = "starttls";
 
@@ -544,34 +526,6 @@ ngx_mail_ssl_merge_conf(ngx_conf_t *cf, void *parent, void *child)
 }
 
 
-static char *
-ngx_mail_ssl_enable(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
-{
-    ngx_mail_ssl_conf_t  *scf = conf;
-
-    char  *rv;
-
-    rv = ngx_conf_set_flag_slot(cf, cmd, conf);
-
-    if (rv != NGX_CONF_OK) {
-        return rv;
-    }
-
-    if (scf->enable && (ngx_int_t) scf->starttls > NGX_MAIL_STARTTLS_OFF) {
-        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
-                           "\"starttls\" directive conflicts with \"ssl on\"");
-        return NGX_CONF_ERROR;
-    }
-
-    if (!scf->listen) {
-        scf->file = cf->conf_file->file.name.data;
-        scf->line = cf->conf_file->line;
-    }
-
-    return NGX_CONF_OK;
-}
-
-
 static char *
 ngx_mail_ssl_starttls(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
 {
@@ -585,12 +539,6 @@ ngx_mail_ssl_starttls(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
         return rv;
     }
 
-    if (scf->enable == 1 && (ngx_int_t) scf->starttls > NGX_MAIL_STARTTLS_OFF) {
-        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
-                           "\"ssl\" directive conflicts with \"starttls\"");
-        return NGX_CONF_ERROR;
-    }
-
     if (!scf->listen) {
         scf->file = cf->conf_file->file.name.data;
         scf->line = cf->conf_file->line;
@@ -682,7 +630,7 @@ ngx_mail_ssl_session_cache(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
                 len++;
             }
 
-            if (len == 0) {
+            if (len == 0 || j == value[i].len) {
                 goto invalid;
             }
 
diff --git a/src/mail/ngx_mail_ssl_module.h b/src/mail/ngx_mail_ssl_module.h
index a0a6113..c0eb6a3 100644
--- a/src/mail/ngx_mail_ssl_module.h
+++ b/src/mail/ngx_mail_ssl_module.h
@@ -20,7 +20,6 @@
 
 
 typedef struct {
-    ngx_flag_t       enable;
     ngx_flag_t       prefer_server_ciphers;
 
     ngx_ssl_t        ssl;
diff --git a/src/os/unix/ngx_darwin_init.c b/src/os/unix/ngx_darwin_init.c
index aabe02f..70748ee 100644
--- a/src/os/unix/ngx_darwin_init.c
+++ b/src/os/unix/ngx_darwin_init.c
@@ -9,11 +9,12 @@
 #include <ngx_core.h>
 
 
-char    ngx_darwin_kern_ostype[16];
-char    ngx_darwin_kern_osrelease[128];
-int     ngx_darwin_hw_ncpu;
-int     ngx_darwin_kern_ipc_somaxconn;
-u_long  ngx_darwin_net_inet_tcp_sendspace;
+char     ngx_darwin_kern_ostype[16];
+char     ngx_darwin_kern_osrelease[128];
+int      ngx_darwin_hw_ncpu;
+int      ngx_darwin_kern_ipc_somaxconn;
+u_long   ngx_darwin_net_inet_tcp_sendspace;
+int64_t  ngx_darwin_hw_cachelinesize;
 
 ngx_uint_t  ngx_debug_malloc;
 
@@ -56,6 +57,10 @@ sysctl_t sysctls[] = {
       &ngx_darwin_kern_ipc_somaxconn,
       sizeof(ngx_darwin_kern_ipc_somaxconn), 0 },
 
+    { "hw.cachelinesize",
+      &ngx_darwin_hw_cachelinesize,
+      sizeof(ngx_darwin_hw_cachelinesize), 0 },
+
     { NULL, NULL, 0, 0 }
 };
 
@@ -155,6 +160,7 @@ ngx_os_specific_init(ngx_log_t *log)
         return NGX_ERROR;
     }
 
+    ngx_cacheline_size = ngx_darwin_hw_cachelinesize;
     ngx_ncpu = ngx_darwin_hw_ncpu;
 
     if (ngx_darwin_kern_ipc_somaxconn > 32767) {
diff --git a/src/os/unix/ngx_errno.h b/src/os/unix/ngx_errno.h
index 7d6ca76..07fa8ce 100644
--- a/src/os/unix/ngx_errno.h
+++ b/src/os/unix/ngx_errno.h
@@ -54,6 +54,7 @@ typedef int               ngx_err_t;
 #define NGX_ENOMOREFILES  0
 #define NGX_ELOOP         ELOOP
 #define NGX_EBADF         EBADF
+#define NGX_EMSGSIZE      EMSGSIZE
 
 #if (NGX_HAVE_OPENAT)
 #define NGX_EMLINK        EMLINK
diff --git a/src/os/unix/ngx_files.c b/src/os/unix/ngx_files.c
index 1c82a8e..2fec1ec 100644
--- a/src/os/unix/ngx_files.c
+++ b/src/os/unix/ngx_files.c
@@ -110,6 +110,8 @@ ngx_thread_read(ngx_file_t *file, u_char *buf, size_t size, off_t offset,
             return NGX_ERROR;
         }
 
+        task->event.log = file->log;
+
         file->thread_task = task;
     }
 
@@ -493,6 +495,8 @@ ngx_thread_write_chain_to_file(ngx_file_t *file, ngx_chain_t *cl, off_t offset,
             return NGX_ERROR;
         }
 
+        task->event.log = file->log;
+
         file->thread_task = task;
     }
 
diff --git a/src/os/unix/ngx_linux_config.h b/src/os/unix/ngx_linux_config.h
index 3036cae..88fef47 100644
--- a/src/os/unix/ngx_linux_config.h
+++ b/src/os/unix/ngx_linux_config.h
@@ -103,6 +103,10 @@ typedef struct iocb  ngx_aiocb_t;
 #include <linux/capability.h>
 #endif
 
+#if (NGX_HAVE_UDP_SEGMENT)
+#include <netinet/udp.h>
+#endif
+
 
 #define NGX_LISTEN_BACKLOG        511
 
diff --git a/src/os/unix/ngx_linux_sendfile_chain.c b/src/os/unix/ngx_linux_sendfile_chain.c
index 101d91a..603f917 100644
--- a/src/os/unix/ngx_linux_sendfile_chain.c
+++ b/src/os/unix/ngx_linux_sendfile_chain.c
@@ -332,6 +332,7 @@ ngx_linux_sendfile_thread(ngx_connection_t *c, ngx_buf_t *file, size_t size)
             return NGX_ERROR;
         }
 
+        task->event.log = c->log;
         task->handler = ngx_linux_sendfile_thread_handler;
 
         c->sendfile_task = task;
diff --git a/src/os/unix/ngx_posix_init.c b/src/os/unix/ngx_posix_init.c
index 7824735..6ac931c 100644
--- a/src/os/unix/ngx_posix_init.c
+++ b/src/os/unix/ngx_posix_init.c
@@ -51,7 +51,10 @@ ngx_os_init(ngx_log_t *log)
     }
 
     ngx_pagesize = getpagesize();
-    ngx_cacheline_size = NGX_CPU_CACHE_LINE;
+
+    if (ngx_cacheline_size == 0) {
+        ngx_cacheline_size = NGX_CPU_CACHE_LINE;
+    }
 
     for (n = ngx_pagesize; n >>= 1; ngx_pagesize_shift++) { /* void */ }
 
diff --git a/src/os/unix/ngx_process_cycle.c b/src/os/unix/ngx_process_cycle.c
index 07cd05e..5bc5ce9 100644
--- a/src/os/unix/ngx_process_cycle.c
+++ b/src/os/unix/ngx_process_cycle.c
@@ -736,6 +736,7 @@ ngx_worker_process_cycle(ngx_cycle_t *cycle, void *data)
                 ngx_set_shutdown_timer(cycle);
                 ngx_close_listening_sockets(cycle);
                 ngx_close_idle_connections(cycle);
+                ngx_event_process_posted(cycle, &ngx_posted_events);
             }
         }
 
@@ -758,7 +759,6 @@ ngx_worker_process_init(ngx_cycle_t *cycle, ngx_int_t worker)
     ngx_cpuset_t     *cpu_affinity;
     struct rlimit     rlmt;
     ngx_core_conf_t  *ccf;
-    ngx_listening_t  *ls;
 
     if (ngx_set_environment(cycle, NULL) == NULL) {
         /* fatal */
@@ -888,15 +888,6 @@ ngx_worker_process_init(ngx_cycle_t *cycle, ngx_int_t worker)
     tp = ngx_timeofday();
     srandom(((unsigned) ngx_pid << 16) ^ tp->sec ^ tp->msec);
 
-    /*
-     * disable deleting previous events for the listening sockets because
-     * in the worker processes there are no events at all at this point
-     */
-    ls = cycle->listening.elts;
-    for (i = 0; i < cycle->listening.nelts; i++) {
-        ls[i].previous = NULL;
-    }
-
     for (i = 0; cycle->modules[i]; i++) {
         if (cycle->modules[i]->init_process) {
             if (cycle->modules[i]->init_process(cycle) == NGX_ERROR) {
@@ -957,7 +948,7 @@ ngx_worker_process_exit(ngx_cycle_t *cycle)
         }
     }
 
-    if (ngx_exiting) {
+    if (ngx_exiting && !ngx_terminate) {
         c = cycle->connections;
         for (i = 0; i < cycle->connection_n; i++) {
             if (c[i].fd != -1
@@ -972,11 +963,11 @@ ngx_worker_process_exit(ngx_cycle_t *cycle)
                 ngx_debug_quit = 1;
             }
         }
+    }
 
-        if (ngx_debug_quit) {
-            ngx_log_error(NGX_LOG_ALERT, cycle->log, 0, "aborting");
-            ngx_debug_point();
-        }
+    if (ngx_debug_quit) {
+        ngx_log_error(NGX_LOG_ALERT, cycle->log, 0, "aborting");
+        ngx_debug_point();
     }
 
     /*
diff --git a/src/os/unix/ngx_readv_chain.c b/src/os/unix/ngx_readv_chain.c
index b1ae4b5..48fa0a7 100644
--- a/src/os/unix/ngx_readv_chain.c
+++ b/src/os/unix/ngx_readv_chain.c
@@ -46,6 +46,7 @@ ngx_readv_chain(ngx_connection_t *c, ngx_chain_t *chain, off_t limit)
                 return 0;
 
             } else {
+                rev->ready = 0;
                 return NGX_AGAIN;
             }
         }
@@ -55,12 +56,15 @@ ngx_readv_chain(ngx_connection_t *c, ngx_chain_t *chain, off_t limit)
 
 #if (NGX_HAVE_EPOLLRDHUP)
 
-    if (ngx_event_flags & NGX_USE_EPOLL_EVENT) {
+    if ((ngx_event_flags & NGX_USE_EPOLL_EVENT)
+        && ngx_use_epoll_rdhup)
+    {
         ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
                        "readv: eof:%d, avail:%d",
                        rev->pending_eof, rev->available);
 
         if (rev->available == 0 && !rev->pending_eof) {
+            rev->ready = 0;
             return NGX_AGAIN;
         }
     }
diff --git a/src/os/unix/ngx_recv.c b/src/os/unix/ngx_recv.c
index ddfae4d..cc04a5f 100644
--- a/src/os/unix/ngx_recv.c
+++ b/src/os/unix/ngx_recv.c
@@ -52,7 +52,9 @@ ngx_unix_recv(ngx_connection_t *c, u_char *buf, size_t size)
 
 #if (NGX_HAVE_EPOLLRDHUP)
 
-    if (ngx_event_flags & NGX_USE_EPOLL_EVENT) {
+    if ((ngx_event_flags & NGX_USE_EPOLL_EVENT)
+        && ngx_use_epoll_rdhup)
+    {
         ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
                        "recv: eof:%d, avail:%d",
                        rev->pending_eof, rev->available);
diff --git a/src/os/unix/ngx_socket.h b/src/os/unix/ngx_socket.h
index ec66a6f..4480adc 100644
--- a/src/os/unix/ngx_socket.h
+++ b/src/os/unix/ngx_socket.h
@@ -13,6 +13,8 @@
 
 
 #define NGX_WRITE_SHUTDOWN SHUT_WR
+#define NGX_READ_SHUTDOWN  SHUT_RD
+#define NGX_RDWR_SHUTDOWN  SHUT_RDWR
 
 typedef int  ngx_socket_t;
 
diff --git a/src/os/unix/ngx_udp_sendmsg_chain.c b/src/os/unix/ngx_udp_sendmsg_chain.c
index 3d1d6dd..fa917b5 100644
--- a/src/os/unix/ngx_udp_sendmsg_chain.c
+++ b/src/os/unix/ngx_udp_sendmsg_chain.c
@@ -12,7 +12,7 @@
 
 static ngx_chain_t *ngx_udp_output_chain_to_iovec(ngx_iovec_t *vec,
     ngx_chain_t *in, ngx_log_t *log);
-static ssize_t ngx_sendmsg(ngx_connection_t *c, ngx_iovec_t *vec);
+static ssize_t ngx_sendmsg_vec(ngx_connection_t *c, ngx_iovec_t *vec);
 
 
 ngx_chain_t *
@@ -88,7 +88,7 @@ ngx_udp_unix_sendmsg_chain(ngx_connection_t *c, ngx_chain_t *in, off_t limit)
 
         send += vec.size;
 
-        n = ngx_sendmsg(c, &vec);
+        n = ngx_sendmsg_vec(c, &vec);
 
         if (n == NGX_ERROR) {
             return NGX_CHAIN_ERROR;
@@ -204,24 +204,13 @@ ngx_udp_output_chain_to_iovec(ngx_iovec_t *vec, ngx_chain_t *in, ngx_log_t *log)
 
 
 static ssize_t
-ngx_sendmsg(ngx_connection_t *c, ngx_iovec_t *vec)
+ngx_sendmsg_vec(ngx_connection_t *c, ngx_iovec_t *vec)
 {
-    ssize_t        n;
-    ngx_err_t      err;
-    struct msghdr  msg;
-
-#if (NGX_HAVE_MSGHDR_MSG_CONTROL)
-
-#if (NGX_HAVE_IP_SENDSRCADDR)
-    u_char         msg_control[CMSG_SPACE(sizeof(struct in_addr))];
-#elif (NGX_HAVE_IP_PKTINFO)
-    u_char         msg_control[CMSG_SPACE(sizeof(struct in_pktinfo))];
-#endif
-
-#if (NGX_HAVE_INET6 && NGX_HAVE_IPV6_RECVPKTINFO)
-    u_char         msg_control6[CMSG_SPACE(sizeof(struct in6_pktinfo))];
-#endif
+    struct msghdr    msg;
 
+#if (NGX_HAVE_ADDRINFO_CMSG)
+    struct cmsghdr  *cmsg;
+    u_char           msg_control[CMSG_SPACE(sizeof(ngx_addrinfo_t))];
 #endif
 
     ngx_memzero(&msg, sizeof(struct msghdr));
@@ -234,88 +223,180 @@ ngx_sendmsg(ngx_connection_t *c, ngx_iovec_t *vec)
     msg.msg_iov = vec->iovs;
     msg.msg_iovlen = vec->count;
 
-#if (NGX_HAVE_MSGHDR_MSG_CONTROL)
-
+#if (NGX_HAVE_ADDRINFO_CMSG)
     if (c->listening && c->listening->wildcard && c->local_sockaddr) {
 
+        msg.msg_control = msg_control;
+        msg.msg_controllen = sizeof(msg_control);
+        ngx_memzero(msg_control, sizeof(msg_control));
+
+        cmsg = CMSG_FIRSTHDR(&msg);
+
+        msg.msg_controllen = ngx_set_srcaddr_cmsg(cmsg, c->local_sockaddr);
+    }
+#endif
+
+    return ngx_sendmsg(c, &msg, 0);
+}
+
+
+#if (NGX_HAVE_ADDRINFO_CMSG)
+
+size_t
+ngx_set_srcaddr_cmsg(struct cmsghdr *cmsg, struct sockaddr *local_sockaddr)
+{
+    size_t                len;
 #if (NGX_HAVE_IP_SENDSRCADDR)
+    struct in_addr       *addr;
+    struct sockaddr_in   *sin;
+#elif (NGX_HAVE_IP_PKTINFO)
+    struct in_pktinfo    *pkt;
+    struct sockaddr_in   *sin;
+#endif
 
-        if (c->local_sockaddr->sa_family == AF_INET) {
-            struct cmsghdr      *cmsg;
-            struct in_addr      *addr;
-            struct sockaddr_in  *sin;
+#if (NGX_HAVE_INET6 && NGX_HAVE_IPV6_RECVPKTINFO)
+    struct in6_pktinfo   *pkt6;
+    struct sockaddr_in6  *sin6;
+#endif
 
-            msg.msg_control = &msg_control;
-            msg.msg_controllen = sizeof(msg_control);
 
-            cmsg = CMSG_FIRSTHDR(&msg);
-            cmsg->cmsg_level = IPPROTO_IP;
-            cmsg->cmsg_type = IP_SENDSRCADDR;
-            cmsg->cmsg_len = CMSG_LEN(sizeof(struct in_addr));
+#if (NGX_HAVE_IP_SENDSRCADDR) || (NGX_HAVE_IP_PKTINFO)
 
-            sin = (struct sockaddr_in *) c->local_sockaddr;
+    if (local_sockaddr->sa_family == AF_INET) {
 
-            addr = (struct in_addr *) CMSG_DATA(cmsg);
-            *addr = sin->sin_addr;
-        }
+        cmsg->cmsg_level = IPPROTO_IP;
+
+#if (NGX_HAVE_IP_SENDSRCADDR)
+
+        cmsg->cmsg_type = IP_SENDSRCADDR;
+        cmsg->cmsg_len = CMSG_LEN(sizeof(struct in_addr));
+        len = CMSG_SPACE(sizeof(struct in_addr));
+
+        sin = (struct sockaddr_in *) local_sockaddr;
+
+        addr = (struct in_addr *) CMSG_DATA(cmsg);
+        *addr = sin->sin_addr;
 
 #elif (NGX_HAVE_IP_PKTINFO)
 
-        if (c->local_sockaddr->sa_family == AF_INET) {
-            struct cmsghdr      *cmsg;
-            struct in_pktinfo   *pkt;
-            struct sockaddr_in  *sin;
+        cmsg->cmsg_type = IP_PKTINFO;
+        cmsg->cmsg_len = CMSG_LEN(sizeof(struct in_pktinfo));
+        len = CMSG_SPACE(sizeof(struct in_pktinfo));
 
-            msg.msg_control = &msg_control;
-            msg.msg_controllen = sizeof(msg_control);
+        sin = (struct sockaddr_in *) local_sockaddr;
 
-            cmsg = CMSG_FIRSTHDR(&msg);
-            cmsg->cmsg_level = IPPROTO_IP;
-            cmsg->cmsg_type = IP_PKTINFO;
-            cmsg->cmsg_len = CMSG_LEN(sizeof(struct in_pktinfo));
+        pkt = (struct in_pktinfo *) CMSG_DATA(cmsg);
+        ngx_memzero(pkt, sizeof(struct in_pktinfo));
+        pkt->ipi_spec_dst = sin->sin_addr;
 
-            sin = (struct sockaddr_in *) c->local_sockaddr;
+#endif
+        return len;
+    }
 
-            pkt = (struct in_pktinfo *) CMSG_DATA(cmsg);
-            ngx_memzero(pkt, sizeof(struct in_pktinfo));
-            pkt->ipi_spec_dst = sin->sin_addr;
-        }
+#endif
+
+#if (NGX_HAVE_INET6 && NGX_HAVE_IPV6_RECVPKTINFO)
+    if (local_sockaddr->sa_family == AF_INET6) {
+
+        cmsg->cmsg_level = IPPROTO_IPV6;
+        cmsg->cmsg_type = IPV6_PKTINFO;
+        cmsg->cmsg_len = CMSG_LEN(sizeof(struct in6_pktinfo));
+        len = CMSG_SPACE(sizeof(struct in6_pktinfo));
 
+        sin6 = (struct sockaddr_in6 *) local_sockaddr;
+
+        pkt6 = (struct in6_pktinfo *) CMSG_DATA(cmsg);
+        ngx_memzero(pkt6, sizeof(struct in6_pktinfo));
+        pkt6->ipi6_addr = sin6->sin6_addr;
+
+        return len;
+    }
+#endif
+
+    return 0;
+}
+
+
+ngx_int_t
+ngx_get_srcaddr_cmsg(struct cmsghdr *cmsg, struct sockaddr *local_sockaddr)
+{
+
+#if (NGX_HAVE_IP_RECVDSTADDR)
+    struct in_addr       *addr;
+    struct sockaddr_in   *sin;
+#elif (NGX_HAVE_IP_PKTINFO)
+    struct in_pktinfo    *pkt;
+    struct sockaddr_in   *sin;
 #endif
 
 #if (NGX_HAVE_INET6 && NGX_HAVE_IPV6_RECVPKTINFO)
+    struct in6_pktinfo   *pkt6;
+    struct sockaddr_in6  *sin6;
+#endif
 
-        if (c->local_sockaddr->sa_family == AF_INET6) {
-            struct cmsghdr       *cmsg;
-            struct in6_pktinfo   *pkt6;
-            struct sockaddr_in6  *sin6;
 
-            msg.msg_control = &msg_control6;
-            msg.msg_controllen = sizeof(msg_control6);
+#if (NGX_HAVE_IP_RECVDSTADDR)
 
-            cmsg = CMSG_FIRSTHDR(&msg);
-            cmsg->cmsg_level = IPPROTO_IPV6;
-            cmsg->cmsg_type = IPV6_PKTINFO;
-            cmsg->cmsg_len = CMSG_LEN(sizeof(struct in6_pktinfo));
+    if (cmsg->cmsg_level == IPPROTO_IP
+        && cmsg->cmsg_type == IP_RECVDSTADDR
+        && local_sockaddr->sa_family == AF_INET)
+    {
+        addr = (struct in_addr *) CMSG_DATA(cmsg);
+        sin = (struct sockaddr_in *) local_sockaddr;
+        sin->sin_addr = *addr;
 
-            sin6 = (struct sockaddr_in6 *) c->local_sockaddr;
+        return NGX_OK;
+    }
 
-            pkt6 = (struct in6_pktinfo *) CMSG_DATA(cmsg);
-            ngx_memzero(pkt6, sizeof(struct in6_pktinfo));
-            pkt6->ipi6_addr = sin6->sin6_addr;
-        }
+#elif (NGX_HAVE_IP_PKTINFO)
+
+    if (cmsg->cmsg_level == IPPROTO_IP
+        && cmsg->cmsg_type == IP_PKTINFO
+        && local_sockaddr->sa_family == AF_INET)
+    {
+        pkt = (struct in_pktinfo *) CMSG_DATA(cmsg);
+        sin = (struct sockaddr_in *) local_sockaddr;
+        sin->sin_addr = pkt->ipi_addr;
+
+        return NGX_OK;
+    }
 
 #endif
+
+#if (NGX_HAVE_INET6 && NGX_HAVE_IPV6_RECVPKTINFO)
+
+    if (cmsg->cmsg_level == IPPROTO_IPV6
+        && cmsg->cmsg_type == IPV6_PKTINFO
+        && local_sockaddr->sa_family == AF_INET6)
+    {
+        pkt6 = (struct in6_pktinfo *) CMSG_DATA(cmsg);
+        sin6 = (struct sockaddr_in6 *) local_sockaddr;
+        sin6->sin6_addr = pkt6->ipi6_addr;
+
+        return NGX_OK;
     }
 
 #endif
 
-eintr:
+    return NGX_DECLINED;
+}
 
-    n = sendmsg(c->fd, &msg, 0);
+#endif
 
-    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                   "sendmsg: %z of %uz", n, vec->size);
+
+ssize_t
+ngx_sendmsg(ngx_connection_t *c, struct msghdr *msg, int flags)
+{
+    ssize_t    n;
+    ngx_err_t  err;
+#if (NGX_DEBUG)
+    size_t      size;
+    ngx_uint_t  i;
+#endif
+
+eintr:
+
+    n = sendmsg(c->fd, msg, flags);
 
     if (n == -1) {
         err = ngx_errno;
@@ -338,5 +419,14 @@ eintr:
         }
     }
 
+#if (NGX_DEBUG)
+    for (i = 0, size = 0; i < (size_t) msg->msg_iovlen; i++) {
+        size += msg->msg_iov[i].iov_len;
+    }
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "sendmsg: %z of %uz", n, size);
+#endif
+
     return n;
 }
diff --git a/src/os/win32/nginx.ico b/src/os/win32/nginx.ico
new file mode 100644
index 0000000000000000000000000000000000000000..70f79db1c631c154f1d5002e0f5fc7139a0358bc
GIT binary patch
literal 1350
zcma)+ziJ~f5XL`80i|0%KtL|9bQN+C*emZbQl$@RuG{Ud2`K}4i#&qiAbt%&USP{D
zmQ21+@~j<moKIx*+mS}|FOq=;0@mv@a)EUKt^ll7JADD%ac#M*`YZRI&w+2`nsQ6g
zxiWrMXg=G20xyTkiSNd2kcl&yjnbX1*(BZBnSDs_`bD7u{40~F#jf8o@Nl~U!W7NO
zI5JQcQ(-fZOu(5k_nAP-v?rUA+@7%{r}MIrU-w^m9AC*D<~Qf<b+kwIF`W{^>3wnH
z|Av*p*r%x4m7`O=k?!2EM(NI;y_Mc~ABBuiYpmoVw*co9*!C8f0;?j&$UsTVg+oI&
zfHNoVM<6HKlLPCf=b0n_Ez-qIKYRZ*j>mN}xi9(6d3zn3qxzW6m-@XgPK9Uuf`3r&
zWkvZxncs^RcH`uG>3ETAAokmONyu>{Df1u8&2JkdDcYEVjZM#1QmR$RZtv&aALRRJ
z)OrqPMcieeCncHE*h<QLgEai&^PuDrF;9p?$EQT8R#S3&KkxoZH3y;eseUafE6Uuj
z3o5(uw|?nuN_15&uYv1J;5*;r0|VYuxK!4(CmXkyOBFYid&j%Rd-lEsw(r0jfBPQ!
R^Y%p9(nf3Ala1T!>?gSO#Y_MI

literal 0
HcmV?d00001

diff --git a/src/os/win32/nginx.rc b/src/os/win32/nginx.rc
new file mode 100644
index 0000000..dc8b7ab
--- /dev/null
+++ b/src/os/win32/nginx.rc
@@ -0,0 +1,6 @@
+
+// Copyright (C) Igor Sysoev
+// Copyright (C) Nginx, Inc.
+
+
+nginx   icon  discardable  "src\\os\\win32\\nginx.ico"
diff --git a/src/os/win32/nginx_icon16.xpm b/src/os/win32/nginx_icon16.xpm
new file mode 100644
index 0000000..45e4bad
--- /dev/null
+++ b/src/os/win32/nginx_icon16.xpm
@@ -0,0 +1,24 @@
+/* XPM */
+static char * nginx_xpm[] = {
+"16 16 2 2",
+/* colors */
+"   c none",
+"GG c #009900",
+/* pixels */
+"                                ",
+"        GGGGGGGGGGGGGGGG        ",
+"        GGGGGGGGGGGGGGGG        ",
+"      GGGGGGGGGGGGGGGGGGGG      ",
+"      GGGGGG        GGGGGG      ",
+"    GGGGGG            GGGGGG    ",
+"    GGGGGG                      ",
+"  GGGGGG      GGGGGGGGGGGGGGGG  ",
+"  GGGGGG    GGGGGGGGGGGGGGGGGG  ",
+"    GGGGGG    GGGGGGGGGGGGGG    ",
+"    GGGGGG            GGGGGG    ",
+"      GGGGGG        GGGGGG      ",
+"      GGGGGGGGGGGGGGGGGGGG      ",
+"        GGGGGGGGGGGGGGGG        ",
+"        GGGGGGGGGGGGGGGG        ",
+"                                "
+};
diff --git a/src/os/win32/nginx_icon32.xpm b/src/os/win32/nginx_icon32.xpm
new file mode 100644
index 0000000..eb26638
--- /dev/null
+++ b/src/os/win32/nginx_icon32.xpm
@@ -0,0 +1,39 @@
+/* XPM */
+static char * nginx_xpm[] = {
+"32 32 2 2",
+/* colors */
+"   c none",
+"GG c #009900",
+/* pixels */
+"                                                                ",
+"                                                                ",
+"                                                                ",
+"                                                                ",
+"                  GGGGGGGGGGGGGGGGGGGGGGGGGGGG                  ",
+"                GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG                ",
+"                GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG                ",
+"              GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG              ",
+"              GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG              ",
+"            GGGGGGGGGG                    GGGGGGGGGG            ",
+"            GGGGGGGGGG                    GGGGGGGGGG            ",
+"          GGGGGGGGGG                        GGGGGGGGGG          ",
+"          GGGGGGGGGG                        GGGGGGGGGG          ",
+"        GGGGGGGGGG                                              ",
+"        GGGGGGGGGG                                              ",
+"      GGGGGGGGGG            GGGGGGGGGGGGGGGGGGGGGGGGGGGGGG      ",
+"      GGGGGGGGGG          GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG      ",
+"        GGGGGGGGGG        GGGGGGGGGGGGGGGGGGGGGGGGGGGGGG        ",
+"        GGGGGGGGGG        GGGGGGGGGGGGGGGGGGGGGGGGGGGGGG        ",
+"          GGGGGGGGGG        GGGGGGGGGGGGGGGGGGGGGGGGGG          ",
+"          GGGGGGGGGG                        GGGGGGGGGG          ",
+"            GGGGGGGGGG                    GGGGGGGGGG            ",
+"            GGGGGGGGGG                    GGGGGGGGGG            ",
+"              GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG              ",
+"              GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG              ",
+"                GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG                ",
+"                GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG                ",
+"                  GGGGGGGGGGGGGGGGGGGGGGGGGGGG                  ",
+"                                                                ",
+"                                                                ",
+"                                                                ",
+"                                                                "
diff --git a/src/os/win32/nginx_icon48.xpm b/src/os/win32/nginx_icon48.xpm
new file mode 100644
index 0000000..c25ba0f
--- /dev/null
+++ b/src/os/win32/nginx_icon48.xpm
@@ -0,0 +1,55 @@
+/* XPM */
+static char * nginx_xpm[] = {
+"48 48 2 2",
+/* colors */
+"   c none",
+"GG c #009900",
+/* pixels */
+"                                                                                                ",
+"                                                                                                ",
+"                                                                                                ",
+"                                                                                                ",
+"                                                                                                ",
+"                        GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG                        ",
+"                        GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG                        ",
+"                      GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG                      ",
+"                      GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG                      ",
+"                    GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG                    ",
+"                    GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG                    ",
+"                  GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG                  ",
+"                  GGGGGGGGGGGGGGGG                            GGGGGGGGGGGGGGGG                  ",
+"                GGGGGGGGGGGGGGGG                                GGGGGGGGGGGGGGGG                ",
+"                GGGGGGGGGGGGGGGG                                GGGGGGGGGGGGGGGG                ",
+"              GGGGGGGGGGGGGGGG                                    GGGGGGGGGGGGGGGG              ",
+"              GGGGGGGGGGGGGGGG                                    GGGGGGGGGGGGGGGG              ",
+"            GGGGGGGGGGGGGGGG                                        GGGGGGGGGGGGGGGG            ",
+"            GGGGGGGGGGGGGGGG                                        GGGGGGGGGGGGGGGG            ",
+"          GGGGGGGGGGGGGGGG                                            GGGGGGGGGGGGGGGG          ",
+"          GGGGGGGGGGGGGGGG                                                                      ",
+"        GGGGGGGGGGGGGGGG                                                                        ",
+"        GGGGGGGGGGGGGGGG                                                                        ",
+"      GGGGGGGGGGGGGGGG                    GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG      ",
+"      GGGGGGGGGGGGGGGG                  GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG      ",
+"        GGGGGGGGGGGGGGGG              GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG        ",
+"        GGGGGGGGGGGGGGGG              GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG        ",
+"          GGGGGGGGGGGGGGGG            GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG          ",
+"          GGGGGGGGGGGGGGGG              GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG          ",
+"            GGGGGGGGGGGGGGGG              GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG            ",
+"            GGGGGGGGGGGGGGGG                                        GGGGGGGGGGGGGGGG            ",
+"              GGGGGGGGGGGGGGGG                                    GGGGGGGGGGGGGGGG              ",
+"              GGGGGGGGGGGGGGGG                                    GGGGGGGGGGGGGGGG              ",
+"                GGGGGGGGGGGGGGGG                                GGGGGGGGGGGGGGGG                ",
+"                GGGGGGGGGGGGGGGG                                GGGGGGGGGGGGGGGG                ",
+"                  GGGGGGGGGGGGGGGG                            GGGGGGGGGGGGGGGG                  ",
+"                  GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG                  ",
+"                    GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG                    ",
+"                    GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG                    ",
+"                      GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG                      ",
+"                      GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG                      ",
+"                        GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG                        ",
+"                        GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG                        ",
+"                                                                                                ",
+"                                                                                                ",
+"                                                                                                ",
+"                                                                                                ",
+"                                                                                                ",
diff --git a/src/os/win32/ngx_alloc.c b/src/os/win32/ngx_alloc.c
new file mode 100644
index 0000000..0c0ef30
--- /dev/null
+++ b/src/os/win32/ngx_alloc.c
@@ -0,0 +1,44 @@
+
+/*
+ * Copyright (C) Igor Sysoev
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+
+ngx_uint_t  ngx_pagesize;
+ngx_uint_t  ngx_pagesize_shift;
+ngx_uint_t  ngx_cacheline_size;
+
+
+void *ngx_alloc(size_t size, ngx_log_t *log)
+{
+    void  *p;
+
+    p = malloc(size);
+    if (p == NULL) {
+        ngx_log_error(NGX_LOG_EMERG, log, ngx_errno,
+                      "malloc(%uz) failed", size);
+    }
+
+    ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, log, 0, "malloc: %p:%uz", p, size);
+
+    return p;
+}
+
+
+void *ngx_calloc(size_t size, ngx_log_t *log)
+{
+    void  *p;
+
+    p = ngx_alloc(size, log);
+
+    if (p) {
+        ngx_memzero(p, size);
+    }
+
+    return p;
+}
diff --git a/src/os/win32/ngx_alloc.h b/src/os/win32/ngx_alloc.h
new file mode 100644
index 0000000..5a0fa3f
--- /dev/null
+++ b/src/os/win32/ngx_alloc.h
@@ -0,0 +1,27 @@
+
+/*
+ * Copyright (C) Igor Sysoev
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_ALLOC_H_INCLUDED_
+#define _NGX_ALLOC_H_INCLUDED_
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+
+void *ngx_alloc(size_t size, ngx_log_t *log);
+void *ngx_calloc(size_t size, ngx_log_t *log);
+
+#define ngx_free          free
+#define ngx_memalign(alignment, size, log)  ngx_alloc(size, log)
+
+extern ngx_uint_t  ngx_pagesize;
+extern ngx_uint_t  ngx_pagesize_shift;
+extern ngx_uint_t  ngx_cacheline_size;
+
+
+#endif /* _NGX_ALLOC_H_INCLUDED_ */
diff --git a/src/os/win32/ngx_atomic.h b/src/os/win32/ngx_atomic.h
new file mode 100644
index 0000000..113f561
--- /dev/null
+++ b/src/os/win32/ngx_atomic.h
@@ -0,0 +1,69 @@
+
+/*
+ * Copyright (C) Igor Sysoev
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_ATOMIC_H_INCLUDED_
+#define _NGX_ATOMIC_H_INCLUDED_
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+
+#define NGX_HAVE_ATOMIC_OPS   1
+
+typedef int32_t                     ngx_atomic_int_t;
+typedef uint32_t                    ngx_atomic_uint_t;
+typedef volatile ngx_atomic_uint_t  ngx_atomic_t;
+#define NGX_ATOMIC_T_LEN            (sizeof("-2147483648") - 1)
+
+
+#if defined( __WATCOMC__ ) || defined( __BORLANDC__ ) || defined(__GNUC__)    \
+    || ( _MSC_VER >= 1300 )
+
+/* the new SDK headers */
+
+#define ngx_atomic_cmp_set(lock, old, set)                                    \
+    ((ngx_atomic_uint_t) InterlockedCompareExchange((long *) lock, set, old)  \
+                         == old)
+
+#else
+
+/* the old MS VC6.0SP2 SDK headers */
+
+#define ngx_atomic_cmp_set(lock, old, set)                                    \
+    (InterlockedCompareExchange((void **) lock, (void *) set, (void *) old)   \
+     == (void *) old)
+
+#endif
+
+
+#define ngx_atomic_fetch_add(p, add) InterlockedExchangeAdd((long *) p, add)
+
+
+#define ngx_memory_barrier()
+
+
+#if defined( __BORLANDC__ ) || ( __WATCOMC__ < 1230 )
+
+/*
+ * Borland C++ 5.5 (tasm32) and Open Watcom C prior to 1.3
+ * do not understand the "pause" instruction
+ */
+
+#define ngx_cpu_pause()
+#else
+#define ngx_cpu_pause()       __asm { pause }
+#endif
+
+
+void ngx_spinlock(ngx_atomic_t *lock, ngx_atomic_int_t value, ngx_uint_t spin);
+
+#define ngx_trylock(lock)  (*(lock) == 0 && ngx_atomic_cmp_set(lock, 0, 1))
+#define ngx_unlock(lock)    *(lock) = 0
+
+
+#endif /* _NGX_ATOMIC_H_INCLUDED_ */
diff --git a/src/os/win32/ngx_dlopen.c b/src/os/win32/ngx_dlopen.c
new file mode 100644
index 0000000..804f49d
--- /dev/null
+++ b/src/os/win32/ngx_dlopen.c
@@ -0,0 +1,22 @@
+
+/*
+ * Copyright (C) Maxim Dounin
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+
+char *
+ngx_dlerror(void)
+{
+    u_char         *p;
+    static u_char   errstr[NGX_MAX_ERROR_STR];
+
+    p = ngx_strerror(ngx_errno, errstr, NGX_MAX_ERROR_STR);
+    *p = '\0';
+
+    return (char *) errstr;
+}
diff --git a/src/os/win32/ngx_dlopen.h b/src/os/win32/ngx_dlopen.h
new file mode 100644
index 0000000..0d6b405
--- /dev/null
+++ b/src/os/win32/ngx_dlopen.h
@@ -0,0 +1,32 @@
+
+/*
+ * Copyright (C) Maxim Dounin
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_DLOPEN_H_INCLUDED_
+#define _NGX_DLOPEN_H_INCLUDED_
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+
+#define NGX_HAVE_DLOPEN  1
+
+
+#define ngx_dlopen(path)           LoadLibrary((char *) path)
+#define ngx_dlopen_n               "LoadLibrary()"
+
+#define ngx_dlsym(handle, symbol)  (void *) GetProcAddress(handle, symbol)
+#define ngx_dlsym_n                "GetProcAddress()"
+
+#define ngx_dlclose(handle)        (FreeLibrary(handle) ? 0 : -1)
+#define ngx_dlclose_n              "FreeLibrary()"
+
+
+char *ngx_dlerror(void);
+
+
+#endif /* _NGX_DLOPEN_H_INCLUDED_ */
diff --git a/src/os/win32/ngx_errno.c b/src/os/win32/ngx_errno.c
new file mode 100644
index 0000000..966d3de
--- /dev/null
+++ b/src/os/win32/ngx_errno.c
@@ -0,0 +1,60 @@
+
+/*
+ * Copyright (C) Igor Sysoev
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+
+u_char *
+ngx_strerror(ngx_err_t err, u_char *errstr, size_t size)
+{
+    u_int          len;
+    static u_long  lang = MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US);
+
+    if (size == 0) {
+        return errstr;
+    }
+
+    len = FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM,
+                        NULL, err, lang, (char *) errstr, size, NULL);
+
+    if (len == 0 && lang) {
+
+        /*
+         * Try to use English messages first and fallback to a language,
+         * based on locale: non-English Windows have no English messages
+         * at all.  This way allows to use English messages at least on
+         * Windows with MUI.
+         */
+
+        lang = 0;
+
+        len = FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM,
+                            NULL, err, lang, (char *) errstr, size, NULL);
+    }
+
+    if (len == 0) {
+        return ngx_snprintf(errstr, size,
+                            "FormatMessage() error:(%d)", GetLastError());
+    }
+
+    /* remove ".\r\n\0" */
+    while (errstr[len] == '\0' || errstr[len] == CR
+           || errstr[len] == LF || errstr[len] == '.')
+    {
+        --len;
+    }
+
+    return &errstr[++len];
+}
+
+
+ngx_int_t
+ngx_strerror_init(void)
+{
+    return NGX_OK;
+}
diff --git a/src/os/win32/ngx_errno.h b/src/os/win32/ngx_errno.h
new file mode 100644
index 0000000..1e73a83
--- /dev/null
+++ b/src/os/win32/ngx_errno.h
@@ -0,0 +1,72 @@
+
+/*
+ * Copyright (C) Igor Sysoev
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_ERRNO_H_INCLUDED_
+#define _NGX_ERRNO_H_INCLUDED_
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+
+typedef DWORD                      ngx_err_t;
+
+#define ngx_errno                  GetLastError()
+#define ngx_set_errno(err)         SetLastError(err)
+#define ngx_socket_errno           WSAGetLastError()
+#define ngx_set_socket_errno(err)  WSASetLastError(err)
+
+#define NGX_EPERM                  ERROR_ACCESS_DENIED
+#define NGX_ENOENT                 ERROR_FILE_NOT_FOUND
+#define NGX_ENOPATH                ERROR_PATH_NOT_FOUND
+#define NGX_ENOMEM                 ERROR_NOT_ENOUGH_MEMORY
+#define NGX_EACCES                 ERROR_ACCESS_DENIED
+/*
+ * there are two EEXIST error codes:
+ * ERROR_FILE_EXISTS used by CreateFile(CREATE_NEW),
+ * and ERROR_ALREADY_EXISTS used by CreateDirectory();
+ * MoveFile() uses both
+ */
+#define NGX_EEXIST                 ERROR_ALREADY_EXISTS
+#define NGX_EEXIST_FILE            ERROR_FILE_EXISTS
+#define NGX_EXDEV                  ERROR_NOT_SAME_DEVICE
+#define NGX_ENOTDIR                ERROR_PATH_NOT_FOUND
+#define NGX_EISDIR                 ERROR_CANNOT_MAKE
+#define NGX_ENOSPC                 ERROR_DISK_FULL
+#define NGX_EPIPE                  EPIPE
+#define NGX_EAGAIN                 WSAEWOULDBLOCK
+#define NGX_EINPROGRESS            WSAEINPROGRESS
+#define NGX_ENOPROTOOPT            WSAENOPROTOOPT
+#define NGX_EOPNOTSUPP             WSAEOPNOTSUPP
+#define NGX_EADDRINUSE             WSAEADDRINUSE
+#define NGX_ECONNABORTED           WSAECONNABORTED
+#define NGX_ECONNRESET             WSAECONNRESET
+#define NGX_ENOTCONN               WSAENOTCONN
+#define NGX_ETIMEDOUT              WSAETIMEDOUT
+#define NGX_ECONNREFUSED           WSAECONNREFUSED
+#define NGX_ENAMETOOLONG           ERROR_BAD_PATHNAME
+#define NGX_ENETDOWN               WSAENETDOWN
+#define NGX_ENETUNREACH            WSAENETUNREACH
+#define NGX_EHOSTDOWN              WSAEHOSTDOWN
+#define NGX_EHOSTUNREACH           WSAEHOSTUNREACH
+#define NGX_ENOMOREFILES           ERROR_NO_MORE_FILES
+#define NGX_EILSEQ                 ERROR_NO_UNICODE_TRANSLATION
+#define NGX_ELOOP                  0
+#define NGX_EBADF                  WSAEBADF
+#define NGX_EMSGSIZE               WSAEMSGSIZE
+
+#define NGX_EALREADY               WSAEALREADY
+#define NGX_EINVAL                 WSAEINVAL
+#define NGX_EMFILE                 WSAEMFILE
+#define NGX_ENFILE                 WSAEMFILE
+
+
+u_char *ngx_strerror(ngx_err_t err, u_char *errstr, size_t size);
+ngx_int_t ngx_strerror_init(void);
+
+
+#endif /* _NGX_ERRNO_H_INCLUDED_ */
diff --git a/src/os/win32/ngx_event_log.c b/src/os/win32/ngx_event_log.c
new file mode 100644
index 0000000..e11ed1e
--- /dev/null
+++ b/src/os/win32/ngx_event_log.c
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) Igor Sysoev
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+
+#define NGX_MAX_ERROR_STR   2048
+
+
+void ngx_cdecl
+ngx_event_log(ngx_err_t err, const char *fmt, ...)
+{
+    u_char         *p, *last;
+    long            types;
+    HKEY            key;
+    HANDLE          ev;
+    va_list         args;
+    u_char          text[NGX_MAX_ERROR_STR];
+    const char     *msgarg[9];
+    static u_char   netmsg[] = "%SystemRoot%\\System32\\netmsg.dll";
+
+    last = text + NGX_MAX_ERROR_STR;
+    p = text + GetModuleFileName(NULL, (char *) text, NGX_MAX_ERROR_STR - 50);
+
+    *p++ = ':';
+    ngx_linefeed(p);
+
+    va_start(args, fmt);
+    p = ngx_vslprintf(p, last, fmt, args);
+    va_end(args);
+
+    if (err) {
+        p = ngx_log_errno(p, last, err);
+    }
+
+    if (p > last - NGX_LINEFEED_SIZE - 1) {
+        p = last - NGX_LINEFEED_SIZE - 1;
+    }
+
+    ngx_linefeed(p);
+
+    *p = '\0';
+
+    /*
+     * we do not log errors here since we use
+     * Event Log only to log our own logs open errors
+     */
+
+    if (RegCreateKeyEx(HKEY_LOCAL_MACHINE,
+           "SYSTEM\\CurrentControlSet\\Services\\EventLog\\Application\\nginx",
+           0, NULL, REG_OPTION_NON_VOLATILE, KEY_SET_VALUE, NULL, &key, NULL)
+        != 0)
+    {
+        return;
+    }
+
+    if (RegSetValueEx(key, "EventMessageFile", 0, REG_EXPAND_SZ,
+                      netmsg, sizeof(netmsg) - 1)
+        != 0)
+    {
+        return;
+    }
+
+    types = EVENTLOG_ERROR_TYPE;
+
+    if (RegSetValueEx(key, "TypesSupported", 0, REG_DWORD,
+                      (u_char *) &types, sizeof(long))
+        != 0)
+    {
+        return;
+    }
+
+    RegCloseKey(key);
+
+    ev = RegisterEventSource(NULL, "nginx");
+
+    msgarg[0] = (char *) text;
+    msgarg[1] = NULL;
+    msgarg[2] = NULL;
+    msgarg[3] = NULL;
+    msgarg[4] = NULL;
+    msgarg[5] = NULL;
+    msgarg[6] = NULL;
+    msgarg[7] = NULL;
+    msgarg[8] = NULL;
+
+    /*
+     * the 3299 event id in netmsg.dll has the generic message format:
+     *     "%1 %2 %3 %4 %5 %6 %7 %8 %9"
+     */
+
+    ReportEvent(ev, EVENTLOG_ERROR_TYPE, 0, 3299, NULL, 9, 0, msgarg, NULL);
+
+    DeregisterEventSource(ev);
+}
diff --git a/src/os/win32/ngx_files.c b/src/os/win32/ngx_files.c
new file mode 100644
index 0000000..90644ad
--- /dev/null
+++ b/src/os/win32/ngx_files.c
@@ -0,0 +1,1459 @@
+
+/*
+ * Copyright (C) Igor Sysoev
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+
+#define NGX_UTF16_BUFLEN  256
+#define NGX_UTF8_BUFLEN   512
+
+static ngx_int_t ngx_win32_check_filename(u_short *u, size_t len,
+    ngx_uint_t dirname);
+static u_short *ngx_utf8_to_utf16(u_short *utf16, u_char *utf8, size_t *len,
+    size_t reserved);
+static u_char *ngx_utf16_to_utf8(u_char *utf8, u_short *utf16, size_t *len,
+    size_t *allocated);
+uint32_t ngx_utf16_decode(u_short **u, size_t n);
+
+
+/* FILE_FLAG_BACKUP_SEMANTICS allows to obtain a handle to a directory */
+
+ngx_fd_t
+ngx_open_file(u_char *name, u_long mode, u_long create, u_long access)
+{
+    size_t      len;
+    u_short    *u;
+    ngx_fd_t    fd;
+    ngx_err_t   err;
+    u_short     utf16[NGX_UTF16_BUFLEN];
+
+    len = NGX_UTF16_BUFLEN;
+    u = ngx_utf8_to_utf16(utf16, name, &len, 0);
+
+    if (u == NULL) {
+        return INVALID_HANDLE_VALUE;
+    }
+
+    fd = INVALID_HANDLE_VALUE;
+
+    if (create == NGX_FILE_OPEN
+        && ngx_win32_check_filename(u, len, 0) != NGX_OK)
+    {
+        goto failed;
+    }
+
+    fd = CreateFileW(u, mode,
+                     FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,
+                     NULL, create, FILE_FLAG_BACKUP_SEMANTICS, NULL);
+
+failed:
+
+    if (u != utf16) {
+        err = ngx_errno;
+        ngx_free(u);
+        ngx_set_errno(err);
+    }
+
+    return fd;
+}
+
+
+ngx_fd_t
+ngx_open_tempfile(u_char *name, ngx_uint_t persistent, ngx_uint_t access)
+{
+    size_t      len;
+    u_short    *u;
+    ngx_fd_t    fd;
+    ngx_err_t   err;
+    u_short     utf16[NGX_UTF16_BUFLEN];
+
+    len = NGX_UTF16_BUFLEN;
+    u = ngx_utf8_to_utf16(utf16, name, &len, 0);
+
+    if (u == NULL) {
+        return INVALID_HANDLE_VALUE;
+    }
+
+    fd = CreateFileW(u,
+                     GENERIC_READ|GENERIC_WRITE,
+                     FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,
+                     NULL,
+                     CREATE_NEW,
+                     persistent ? 0:
+                         FILE_ATTRIBUTE_TEMPORARY|FILE_FLAG_DELETE_ON_CLOSE,
+                     NULL);
+
+    if (u != utf16) {
+        err = ngx_errno;
+        ngx_free(u);
+        ngx_set_errno(err);
+    }
+
+    return fd;
+}
+
+
+ssize_t
+ngx_read_file(ngx_file_t *file, u_char *buf, size_t size, off_t offset)
+{
+    u_long      n;
+    ngx_err_t   err;
+    OVERLAPPED  ovlp, *povlp;
+
+    ovlp.Internal = 0;
+    ovlp.InternalHigh = 0;
+    ovlp.Offset = (u_long) offset;
+    ovlp.OffsetHigh = (u_long) (offset >> 32);
+    ovlp.hEvent = NULL;
+
+    povlp = &ovlp;
+
+    if (ReadFile(file->fd, buf, size, &n, povlp) == 0) {
+        err = ngx_errno;
+
+        if (err == ERROR_HANDLE_EOF) {
+            return 0;
+        }
+
+        ngx_log_error(NGX_LOG_ERR, file->log, err,
+                      "ReadFile() \"%s\" failed", file->name.data);
+        return NGX_ERROR;
+    }
+
+    file->offset += n;
+
+    return n;
+}
+
+
+ssize_t
+ngx_write_file(ngx_file_t *file, u_char *buf, size_t size, off_t offset)
+{
+    u_long      n;
+    OVERLAPPED  ovlp, *povlp;
+
+    ovlp.Internal = 0;
+    ovlp.InternalHigh = 0;
+    ovlp.Offset = (u_long) offset;
+    ovlp.OffsetHigh = (u_long) (offset >> 32);
+    ovlp.hEvent = NULL;
+
+    povlp = &ovlp;
+
+    if (WriteFile(file->fd, buf, size, &n, povlp) == 0) {
+        ngx_log_error(NGX_LOG_ERR, file->log, ngx_errno,
+                      "WriteFile() \"%s\" failed", file->name.data);
+        return NGX_ERROR;
+    }
+
+    if (n != size) {
+        ngx_log_error(NGX_LOG_CRIT, file->log, 0,
+                      "WriteFile() \"%s\" has written only %ul of %uz",
+                      file->name.data, n, size);
+        return NGX_ERROR;
+    }
+
+    file->offset += n;
+
+    return n;
+}
+
+
+ssize_t
+ngx_write_chain_to_file(ngx_file_t *file, ngx_chain_t *cl, off_t offset,
+    ngx_pool_t *pool)
+{
+    u_char   *buf, *prev;
+    size_t    size;
+    ssize_t   total, n;
+
+    total = 0;
+
+    while (cl) {
+        buf = cl->buf->pos;
+        prev = buf;
+        size = 0;
+
+        /* coalesce the neighbouring bufs */
+
+        while (cl && prev == cl->buf->pos) {
+            size += cl->buf->last - cl->buf->pos;
+            prev = cl->buf->last;
+            cl = cl->next;
+        }
+
+        n = ngx_write_file(file, buf, size, offset);
+
+        if (n == NGX_ERROR) {
+            return NGX_ERROR;
+        }
+
+        total += n;
+        offset += n;
+    }
+
+    return total;
+}
+
+
+ssize_t
+ngx_read_fd(ngx_fd_t fd, void *buf, size_t size)
+{
+    u_long  n;
+
+    if (ReadFile(fd, buf, size, &n, NULL) != 0) {
+        return (size_t) n;
+    }
+
+    return -1;
+}
+
+
+ssize_t
+ngx_write_fd(ngx_fd_t fd, void *buf, size_t size)
+{
+    u_long  n;
+
+    if (WriteFile(fd, buf, size, &n, NULL) != 0) {
+        return (size_t) n;
+    }
+
+    return -1;
+}
+
+
+ssize_t
+ngx_write_console(ngx_fd_t fd, void *buf, size_t size)
+{
+    u_long  n;
+
+    (void) CharToOemBuff(buf, buf, size);
+
+    if (WriteFile(fd, buf, size, &n, NULL) != 0) {
+        return (size_t) n;
+    }
+
+    return -1;
+}
+
+
+ngx_int_t
+ngx_delete_file(u_char *name)
+{
+    long        rc;
+    size_t      len;
+    u_short    *u;
+    ngx_err_t   err;
+    u_short     utf16[NGX_UTF16_BUFLEN];
+
+    len = NGX_UTF16_BUFLEN;
+    u = ngx_utf8_to_utf16(utf16, name, &len, 0);
+
+    if (u == NULL) {
+        return NGX_FILE_ERROR;
+    }
+
+    rc = NGX_FILE_ERROR;
+
+    if (ngx_win32_check_filename(u, len, 0) != NGX_OK) {
+        goto failed;
+    }
+
+    rc = DeleteFileW(u);
+
+failed:
+
+    if (u != utf16) {
+        err = ngx_errno;
+        ngx_free(u);
+        ngx_set_errno(err);
+    }
+
+    return rc;
+}
+
+
+ngx_int_t
+ngx_rename_file(u_char *from, u_char *to)
+{
+    long        rc;
+    size_t      len;
+    u_short    *fu, *tu;
+    ngx_err_t   err;
+    u_short     utf16f[NGX_UTF16_BUFLEN];
+    u_short     utf16t[NGX_UTF16_BUFLEN];
+
+    len = NGX_UTF16_BUFLEN;
+    fu = ngx_utf8_to_utf16(utf16f, from, &len, 0);
+
+    if (fu == NULL) {
+        return NGX_FILE_ERROR;
+    }
+
+    rc = NGX_FILE_ERROR;
+    tu = NULL;
+
+    if (ngx_win32_check_filename(fu, len, 0) != NGX_OK) {
+        goto failed;
+    }
+
+    len = NGX_UTF16_BUFLEN;
+    tu = ngx_utf8_to_utf16(utf16t, to, &len, 0);
+
+    if (tu == NULL) {
+        goto failed;
+    }
+
+    if (ngx_win32_check_filename(tu, len, 1) != NGX_OK) {
+        goto failed;
+    }
+
+    rc = MoveFileW(fu, tu);
+
+failed:
+
+    if (fu != utf16f) {
+        err = ngx_errno;
+        ngx_free(fu);
+        ngx_set_errno(err);
+    }
+
+    if (tu && tu != utf16t) {
+        err = ngx_errno;
+        ngx_free(tu);
+        ngx_set_errno(err);
+    }
+
+    return rc;
+}
+
+
+ngx_err_t
+ngx_win32_rename_file(ngx_str_t *from, ngx_str_t *to, ngx_log_t *log)
+{
+    u_char             *name;
+    ngx_err_t           err;
+    ngx_uint_t          collision;
+    ngx_atomic_uint_t   num;
+
+    name = ngx_alloc(to->len + 1 + NGX_ATOMIC_T_LEN + 1 + sizeof("DELETE"),
+                     log);
+    if (name == NULL) {
+        return NGX_ENOMEM;
+    }
+
+    ngx_memcpy(name, to->data, to->len);
+
+    collision = 0;
+
+    /* mutex_lock() (per cache or single ?) */
+
+    for ( ;; ) {
+        num = ngx_next_temp_number(collision);
+
+        ngx_sprintf(name + to->len, ".%0muA.DELETE%Z", num);
+
+        if (ngx_rename_file(to->data, name) != NGX_FILE_ERROR) {
+            break;
+        }
+
+        err = ngx_errno;
+
+        if (err == NGX_EEXIST || err == NGX_EEXIST_FILE) {
+            collision = 1;
+            continue;
+        }
+
+        ngx_log_error(NGX_LOG_CRIT, log, err,
+                      "MoveFile() \"%s\" to \"%s\" failed", to->data, name);
+        goto failed;
+    }
+
+    if (ngx_rename_file(from->data, to->data) == NGX_FILE_ERROR) {
+        err = ngx_errno;
+
+    } else {
+        err = 0;
+    }
+
+    if (ngx_delete_file(name) == NGX_FILE_ERROR) {
+        ngx_log_error(NGX_LOG_CRIT, log, ngx_errno,
+                      "DeleteFile() \"%s\" failed", name);
+    }
+
+failed:
+
+    /* mutex_unlock() */
+
+    ngx_free(name);
+
+    return err;
+}
+
+
+ngx_int_t
+ngx_file_info(u_char *file, ngx_file_info_t *sb)
+{
+    size_t                      len;
+    long                        rc;
+    u_short                    *u;
+    ngx_err_t                   err;
+    WIN32_FILE_ATTRIBUTE_DATA   fa;
+    u_short                     utf16[NGX_UTF16_BUFLEN];
+
+    len = NGX_UTF16_BUFLEN;
+
+    u = ngx_utf8_to_utf16(utf16, file, &len, 0);
+
+    if (u == NULL) {
+        return NGX_FILE_ERROR;
+    }
+
+    rc = NGX_FILE_ERROR;
+
+    if (ngx_win32_check_filename(u, len, 0) != NGX_OK) {
+        goto failed;
+    }
+
+    rc = GetFileAttributesExW(u, GetFileExInfoStandard, &fa);
+
+    sb->dwFileAttributes = fa.dwFileAttributes;
+    sb->ftCreationTime = fa.ftCreationTime;
+    sb->ftLastAccessTime = fa.ftLastAccessTime;
+    sb->ftLastWriteTime = fa.ftLastWriteTime;
+    sb->nFileSizeHigh = fa.nFileSizeHigh;
+    sb->nFileSizeLow = fa.nFileSizeLow;
+
+failed:
+
+    if (u != utf16) {
+        err = ngx_errno;
+        ngx_free(u);
+        ngx_set_errno(err);
+    }
+
+    return rc;
+}
+
+
+ngx_int_t
+ngx_set_file_time(u_char *name, ngx_fd_t fd, time_t s)
+{
+    uint64_t  intervals;
+    FILETIME  ft;
+
+    /* 116444736000000000 is commented in src/os/win32/ngx_time.c */
+
+    intervals = s * 10000000 + 116444736000000000;
+
+    ft.dwLowDateTime = (DWORD) intervals;
+    ft.dwHighDateTime = (DWORD) (intervals >> 32);
+
+    if (SetFileTime(fd, NULL, NULL, &ft) != 0) {
+        return NGX_OK;
+    }
+
+    return NGX_ERROR;
+}
+
+
+ngx_int_t
+ngx_create_file_mapping(ngx_file_mapping_t *fm)
+{
+    LARGE_INTEGER  size;
+
+    fm->fd = ngx_open_file(fm->name, NGX_FILE_RDWR, NGX_FILE_TRUNCATE,
+                           NGX_FILE_DEFAULT_ACCESS);
+
+    if (fm->fd == NGX_INVALID_FILE) {
+        ngx_log_error(NGX_LOG_CRIT, fm->log, ngx_errno,
+                      ngx_open_file_n " \"%s\" failed", fm->name);
+        return NGX_ERROR;
+    }
+
+    fm->handle = NULL;
+
+    size.QuadPart = fm->size;
+
+    if (SetFilePointerEx(fm->fd, size, NULL, FILE_BEGIN) == 0) {
+        ngx_log_error(NGX_LOG_CRIT, fm->log, ngx_errno,
+                      "SetFilePointerEx(\"%s\", %uz) failed",
+                      fm->name, fm->size);
+        goto failed;
+    }
+
+    if (SetEndOfFile(fm->fd) == 0) {
+        ngx_log_error(NGX_LOG_CRIT, fm->log, ngx_errno,
+                      "SetEndOfFile() \"%s\" failed", fm->name);
+        goto failed;
+    }
+
+    fm->handle = CreateFileMapping(fm->fd, NULL, PAGE_READWRITE,
+                                   (u_long) ((off_t) fm->size >> 32),
+                                   (u_long) ((off_t) fm->size & 0xffffffff),
+                                   NULL);
+    if (fm->handle == NULL) {
+        ngx_log_error(NGX_LOG_CRIT, fm->log, ngx_errno,
+                      "CreateFileMapping(%s, %uz) failed",
+                      fm->name, fm->size);
+        goto failed;
+    }
+
+    fm->addr = MapViewOfFile(fm->handle, FILE_MAP_WRITE, 0, 0, 0);
+
+    if (fm->addr != NULL) {
+        return NGX_OK;
+    }
+
+    ngx_log_error(NGX_LOG_CRIT, fm->log, ngx_errno,
+                  "MapViewOfFile(%uz) of file mapping \"%s\" failed",
+                  fm->size, fm->name);
+
+failed:
+
+    if (fm->handle) {
+        if (CloseHandle(fm->handle) == 0) {
+            ngx_log_error(NGX_LOG_ALERT, fm->log, ngx_errno,
+                          "CloseHandle() of file mapping \"%s\" failed",
+                          fm->name);
+        }
+    }
+
+    if (ngx_close_file(fm->fd) == NGX_FILE_ERROR) {
+        ngx_log_error(NGX_LOG_ALERT, fm->log, ngx_errno,
+                      ngx_close_file_n " \"%s\" failed", fm->name);
+    }
+
+    return NGX_ERROR;
+}
+
+
+void
+ngx_close_file_mapping(ngx_file_mapping_t *fm)
+{
+    if (UnmapViewOfFile(fm->addr) == 0) {
+        ngx_log_error(NGX_LOG_ALERT, fm->log, ngx_errno,
+                      "UnmapViewOfFile(%p) of file mapping \"%s\" failed",
+                      fm->addr, &fm->name);
+    }
+
+    if (CloseHandle(fm->handle) == 0) {
+        ngx_log_error(NGX_LOG_ALERT, fm->log, ngx_errno,
+                      "CloseHandle() of file mapping \"%s\" failed",
+                      &fm->name);
+    }
+
+    if (ngx_close_file(fm->fd) == NGX_FILE_ERROR) {
+        ngx_log_error(NGX_LOG_ALERT, fm->log, ngx_errno,
+                      ngx_close_file_n " \"%s\" failed", fm->name);
+    }
+}
+
+
+u_char *
+ngx_realpath(u_char *path, u_char *resolved)
+{
+    /* STUB */
+    return path;
+}
+
+
+size_t
+ngx_getcwd(u_char *buf, size_t size)
+{
+    u_char   *p;
+    size_t    n;
+    u_short   utf16[NGX_MAX_PATH];
+
+    n = GetCurrentDirectoryW(NGX_MAX_PATH, utf16);
+
+    if (n == 0) {
+        return 0;
+    }
+
+    if (n > NGX_MAX_PATH) {
+        ngx_set_errno(ERROR_INSUFFICIENT_BUFFER);
+        return 0;
+    }
+
+    p = ngx_utf16_to_utf8(buf, utf16, &size, NULL);
+
+    if (p == NULL) {
+        return 0;
+    }
+
+    if (p != buf) {
+        ngx_free(p);
+        ngx_set_errno(ERROR_INSUFFICIENT_BUFFER);
+        return 0;
+    }
+
+    return size - 1;
+}
+
+
+ngx_int_t
+ngx_open_dir(ngx_str_t *name, ngx_dir_t *dir)
+{
+    size_t      len;
+    u_short    *u, *p;
+    ngx_err_t   err;
+    u_short     utf16[NGX_UTF16_BUFLEN];
+
+    len = NGX_UTF16_BUFLEN - 2;
+    u = ngx_utf8_to_utf16(utf16, name->data, &len, 2);
+
+    if (u == NULL) {
+        return NGX_ERROR;
+    }
+
+    if (ngx_win32_check_filename(u, len, 0) != NGX_OK) {
+        goto failed;
+    }
+
+    p = &u[len - 1];
+
+    *p++ = '/';
+    *p++ = '*';
+    *p = '\0';
+
+    dir->dir = FindFirstFileW(u, &dir->finddata);
+
+    if (dir->dir == INVALID_HANDLE_VALUE) {
+        goto failed;
+    }
+
+    if (u != utf16) {
+        ngx_free(u);
+    }
+
+    dir->valid_info = 1;
+    dir->ready = 1;
+    dir->name = NULL;
+    dir->allocated = 0;
+
+    return NGX_OK;
+
+failed:
+
+    if (u != utf16) {
+        err = ngx_errno;
+        ngx_free(u);
+        ngx_set_errno(err);
+    }
+
+    return NGX_ERROR;
+}
+
+
+ngx_int_t
+ngx_read_dir(ngx_dir_t *dir)
+{
+    u_char  *name;
+    size_t   len, allocated;
+
+    if (dir->ready) {
+        dir->ready = 0;
+        goto convert;
+    }
+
+    if (FindNextFileW(dir->dir, &dir->finddata) != 0) {
+        dir->type = 1;
+        goto convert;
+    }
+
+    return NGX_ERROR;
+
+convert:
+
+    name = dir->name;
+    len = dir->allocated;
+
+    name = ngx_utf16_to_utf8(name, dir->finddata.cFileName, &len, &allocated);
+
+    if (name == NULL) {
+        return NGX_ERROR;
+    }
+
+    if (name != dir->name) {
+
+        if (dir->name) {
+            ngx_free(dir->name);
+        }
+
+        dir->name = name;
+        dir->allocated = allocated;
+    }
+
+    dir->namelen = len - 1;
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_close_dir(ngx_dir_t *dir)
+{
+    if (dir->name) {
+        ngx_free(dir->name);
+    }
+
+    if (FindClose(dir->dir) == 0) {
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_create_dir(u_char *name, ngx_uint_t access)
+{
+    long        rc;
+    size_t      len;
+    u_short    *u;
+    ngx_err_t   err;
+    u_short     utf16[NGX_UTF16_BUFLEN];
+
+    len = NGX_UTF16_BUFLEN;
+    u = ngx_utf8_to_utf16(utf16, name, &len, 0);
+
+    if (u == NULL) {
+        return NGX_FILE_ERROR;
+    }
+
+    rc = NGX_FILE_ERROR;
+
+    if (ngx_win32_check_filename(u, len, 1) != NGX_OK) {
+        goto failed;
+    }
+
+    rc = CreateDirectoryW(u, NULL);
+
+failed:
+
+    if (u != utf16) {
+        err = ngx_errno;
+        ngx_free(u);
+        ngx_set_errno(err);
+    }
+
+    return rc;
+}
+
+
+ngx_int_t
+ngx_delete_dir(u_char *name)
+{
+    long        rc;
+    size_t      len;
+    u_short    *u;
+    ngx_err_t   err;
+    u_short     utf16[NGX_UTF16_BUFLEN];
+
+    len = NGX_UTF16_BUFLEN;
+    u = ngx_utf8_to_utf16(utf16, name, &len, 0);
+
+    if (u == NULL) {
+        return NGX_FILE_ERROR;
+    }
+
+    rc = NGX_FILE_ERROR;
+
+    if (ngx_win32_check_filename(u, len, 0) != NGX_OK) {
+        goto failed;
+    }
+
+    rc = RemoveDirectoryW(u);
+
+failed:
+
+    if (u != utf16) {
+        err = ngx_errno;
+        ngx_free(u);
+        ngx_set_errno(err);
+    }
+
+    return rc;
+}
+
+
+ngx_int_t
+ngx_open_glob(ngx_glob_t *gl)
+{
+    u_char     *p;
+    size_t      len;
+    u_short    *u;
+    ngx_err_t   err;
+    u_short     utf16[NGX_UTF16_BUFLEN];
+
+    len = NGX_UTF16_BUFLEN;
+    u = ngx_utf8_to_utf16(utf16, gl->pattern, &len, 0);
+
+    if (u == NULL) {
+        return NGX_ERROR;
+    }
+
+    gl->dir = FindFirstFileW(u, &gl->finddata);
+
+    if (gl->dir == INVALID_HANDLE_VALUE) {
+
+        err = ngx_errno;
+
+        if (u != utf16) {
+            ngx_free(u);
+        }
+
+        if ((err == ERROR_FILE_NOT_FOUND || err == ERROR_PATH_NOT_FOUND)
+             && gl->test)
+        {
+            gl->no_match = 1;
+            return NGX_OK;
+        }
+
+        ngx_set_errno(err);
+
+        return NGX_ERROR;
+    }
+
+    for (p = gl->pattern; *p; p++) {
+        if (*p == '/') {
+            gl->last = p + 1 - gl->pattern;
+        }
+    }
+
+    if (u != utf16) {
+        ngx_free(u);
+    }
+
+    gl->ready = 1;
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_read_glob(ngx_glob_t *gl, ngx_str_t *name)
+{
+    u_char     *p;
+    size_t      len;
+    ngx_err_t   err;
+    u_char      utf8[NGX_UTF8_BUFLEN];
+
+    if (gl->no_match) {
+        return NGX_DONE;
+    }
+
+    if (gl->ready) {
+        gl->ready = 0;
+        goto convert;
+    }
+
+    ngx_free(gl->name.data);
+    gl->name.data = NULL;
+
+    if (FindNextFileW(gl->dir, &gl->finddata) != 0) {
+        goto convert;
+    }
+
+    err = ngx_errno;
+
+    if (err == NGX_ENOMOREFILES) {
+        return NGX_DONE;
+    }
+
+    ngx_log_error(NGX_LOG_ALERT, gl->log, err,
+                  "FindNextFile(%s) failed", gl->pattern);
+
+    return NGX_ERROR;
+
+convert:
+
+    len = NGX_UTF8_BUFLEN;
+    p = ngx_utf16_to_utf8(utf8, gl->finddata.cFileName, &len, NULL);
+
+    if (p == NULL) {
+        return NGX_ERROR;
+    }
+
+    gl->name.len = gl->last + len - 1;
+
+    gl->name.data = ngx_alloc(gl->name.len + 1, gl->log);
+    if (gl->name.data == NULL) {
+        goto failed;
+    }
+
+    ngx_memcpy(gl->name.data, gl->pattern, gl->last);
+    ngx_cpystrn(gl->name.data + gl->last, p, len);
+
+    if (p != utf8) {
+        ngx_free(p);
+    }
+
+    *name = gl->name;
+
+    return NGX_OK;
+
+failed:
+
+    if (p != utf8) {
+        err = ngx_errno;
+        ngx_free(p);
+        ngx_set_errno(err);
+    }
+
+    return NGX_ERROR;
+}
+
+
+void
+ngx_close_glob(ngx_glob_t *gl)
+{
+    if (gl->name.data) {
+        ngx_free(gl->name.data);
+    }
+
+    if (gl->dir == INVALID_HANDLE_VALUE) {
+        return;
+    }
+
+    if (FindClose(gl->dir) == 0) {
+        ngx_log_error(NGX_LOG_ALERT, gl->log, ngx_errno,
+                      "FindClose(%s) failed", gl->pattern);
+    }
+}
+
+
+ngx_int_t
+ngx_de_info(u_char *name, ngx_dir_t *dir)
+{
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_de_link_info(u_char *name, ngx_dir_t *dir)
+{
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_read_ahead(ngx_fd_t fd, size_t n)
+{
+    return ~NGX_FILE_ERROR;
+}
+
+
+ngx_int_t
+ngx_directio_on(ngx_fd_t fd)
+{
+    return ~NGX_FILE_ERROR;
+}
+
+
+ngx_int_t
+ngx_directio_off(ngx_fd_t fd)
+{
+    return ~NGX_FILE_ERROR;
+}
+
+
+size_t
+ngx_fs_bsize(u_char *name)
+{
+    u_long    sc, bs, nfree, ncl;
+    size_t    len;
+    u_short  *u;
+    u_short   utf16[NGX_UTF16_BUFLEN];
+
+    len = NGX_UTF16_BUFLEN;
+    u = ngx_utf8_to_utf16(utf16, name, &len, 0);
+
+    if (u == NULL) {
+        return 512;
+    }
+
+    if (GetDiskFreeSpaceW(u, &sc, &bs, &nfree, &ncl) == 0) {
+
+        if (u != utf16) {
+            ngx_free(u);
+        }
+
+        return 512;
+    }
+
+    if (u != utf16) {
+        ngx_free(u);
+    }
+
+    return sc * bs;
+}
+
+
+off_t
+ngx_fs_available(u_char *name)
+{
+    size_t           len;
+    u_short         *u;
+    ULARGE_INTEGER   navail;
+    u_short          utf16[NGX_UTF16_BUFLEN];
+
+    len = NGX_UTF16_BUFLEN;
+    u = ngx_utf8_to_utf16(utf16, name, &len, 0);
+
+    if (u == NULL) {
+        return NGX_MAX_OFF_T_VALUE;
+    }
+
+    if (GetDiskFreeSpaceExW(u, &navail, NULL, NULL) == 0) {
+
+        if (u != utf16) {
+            ngx_free(u);
+        }
+
+        return NGX_MAX_OFF_T_VALUE;
+    }
+
+    if (u != utf16) {
+        ngx_free(u);
+    }
+
+    return (off_t) navail.QuadPart;
+}
+
+
+static ngx_int_t
+ngx_win32_check_filename(u_short *u, size_t len, ngx_uint_t dirname)
+{
+    u_long      n;
+    u_short    *lu, *p, *slash, ch;
+    ngx_err_t   err;
+    enum {
+        sw_start = 0,
+        sw_normal,
+        sw_after_slash,
+        sw_after_colon,
+        sw_after_dot
+    } state;
+
+    /* check for NTFS streams (":"), trailing dots and spaces */
+
+    lu = NULL;
+    slash = NULL;
+    state = sw_start;
+
+#if (NGX_SUPPRESS_WARN)
+    ch = 0;
+#endif
+
+    for (p = u; *p; p++) {
+        ch = *p;
+
+        switch (state) {
+
+        case sw_start:
+
+            /*
+             * skip till first "/" to allow paths starting with drive and
+             * relative path, like "c:html/"
+             */
+
+            if (ch == '/' || ch == '\\') {
+                state = sw_after_slash;
+                slash = p;
+            }
+
+            break;
+
+        case sw_normal:
+
+            if (ch == ':') {
+                state = sw_after_colon;
+                break;
+            }
+
+            if (ch == '.' || ch == ' ') {
+                state = sw_after_dot;
+                break;
+            }
+
+            if (ch == '/' || ch == '\\') {
+                state = sw_after_slash;
+                slash = p;
+                break;
+            }
+
+            break;
+
+        case sw_after_slash:
+
+            if (ch == '/' || ch == '\\') {
+                break;
+            }
+
+            if (ch == '.') {
+                break;
+            }
+
+            if (ch == ':') {
+                state = sw_after_colon;
+                break;
+            }
+
+            state = sw_normal;
+            break;
+
+        case sw_after_colon:
+
+            if (ch == '/' || ch == '\\') {
+                state = sw_after_slash;
+                slash = p;
+                break;
+            }
+
+            goto invalid;
+
+        case sw_after_dot:
+
+            if (ch == '/' || ch == '\\') {
+                goto invalid;
+            }
+
+            if (ch == ':') {
+                goto invalid;
+            }
+
+            if (ch == '.' || ch == ' ') {
+                break;
+            }
+
+            state = sw_normal;
+            break;
+        }
+    }
+
+    if (state == sw_after_dot) {
+        goto invalid;
+    }
+
+    if (dirname && slash) {
+        ch = *slash;
+        *slash = '\0';
+        len = slash - u + 1;
+    }
+
+    /* check if long name match */
+
+    lu = malloc(len * 2);
+    if (lu == NULL) {
+        return NGX_ERROR;
+    }
+
+    n = GetLongPathNameW(u, lu, len);
+
+    if (n == 0) {
+
+        if (dirname && slash && ngx_errno == NGX_ENOENT) {
+            ngx_set_errno(NGX_ENOPATH);
+        }
+
+        goto failed;
+    }
+
+    if (n != len - 1 || _wcsicmp(u, lu) != 0) {
+        goto invalid;
+    }
+
+    if (dirname && slash) {
+        *slash = ch;
+    }
+
+    ngx_free(lu);
+
+    return NGX_OK;
+
+invalid:
+
+    ngx_set_errno(NGX_ENOENT);
+
+failed:
+
+    if (dirname && slash) {
+        *slash = ch;
+    }
+
+    if (lu) {
+        err = ngx_errno;
+        ngx_free(lu);
+        ngx_set_errno(err);
+    }
+
+    return NGX_ERROR;
+}
+
+
+static u_short *
+ngx_utf8_to_utf16(u_short *utf16, u_char *utf8, size_t *len, size_t reserved)
+{
+    u_char    *p;
+    u_short   *u, *last;
+    uint32_t   n;
+
+    p = utf8;
+    u = utf16;
+    last = utf16 + *len;
+
+    while (u < last) {
+
+        if (*p < 0x80) {
+            *u++ = (u_short) *p;
+
+            if (*p == 0) {
+                *len = u - utf16;
+                return utf16;
+            }
+
+            p++;
+
+            continue;
+        }
+
+        if (u + 1 == last) {
+            *len = u - utf16;
+            break;
+        }
+
+        n = ngx_utf8_decode(&p, 4);
+
+        if (n > 0x10ffff) {
+            ngx_set_errno(NGX_EILSEQ);
+            return NULL;
+        }
+
+        if (n > 0xffff) {
+            n -= 0x10000;
+            *u++ = (u_short) (0xd800 + (n >> 10));
+            *u++ = (u_short) (0xdc00 + (n & 0x03ff));
+            continue;
+        }
+
+        *u++ = (u_short) n;
+    }
+
+    /* the given buffer is not enough, allocate a new one */
+
+    u = malloc(((p - utf8) + ngx_strlen(p) + 1 + reserved) * sizeof(u_short));
+    if (u == NULL) {
+        return NULL;
+    }
+
+    ngx_memcpy(u, utf16, *len * 2);
+
+    utf16 = u;
+    u += *len;
+
+    for ( ;; ) {
+
+        if (*p < 0x80) {
+            *u++ = (u_short) *p;
+
+            if (*p == 0) {
+                *len = u - utf16;
+                return utf16;
+            }
+
+            p++;
+
+            continue;
+        }
+
+        n = ngx_utf8_decode(&p, 4);
+
+        if (n > 0x10ffff) {
+            ngx_free(utf16);
+            ngx_set_errno(NGX_EILSEQ);
+            return NULL;
+        }
+
+        if (n > 0xffff) {
+            n -= 0x10000;
+            *u++ = (u_short) (0xd800 + (n >> 10));
+            *u++ = (u_short) (0xdc00 + (n & 0x03ff));
+            continue;
+        }
+
+        *u++ = (u_short) n;
+    }
+
+    /* unreachable */
+}
+
+
+static u_char *
+ngx_utf16_to_utf8(u_char *utf8, u_short *utf16, size_t *len, size_t *allocated)
+{
+    u_char    *p, *last;
+    u_short   *u, *j;
+    uint32_t   n;
+
+    u = utf16;
+    p = utf8;
+    last = utf8 + *len;
+
+    while (p < last) {
+
+        if (*u < 0x80) {
+            *p++ = (u_char) *u;
+
+            if (*u == 0) {
+                *len = p - utf8;
+                return utf8;
+            }
+
+            u++;
+
+            continue;
+        }
+
+        if (p >= last - 4) {
+            *len = p - utf8;
+            break;
+        }
+
+        n = ngx_utf16_decode(&u, 2);
+
+        if (n > 0x10ffff) {
+            ngx_set_errno(NGX_EILSEQ);
+            return NULL;
+        }
+
+        if (n >= 0x10000) {
+            *p++ = (u_char) (0xf0 + (n >> 18));
+            *p++ = (u_char) (0x80 + ((n >> 12) & 0x3f));
+            *p++ = (u_char) (0x80 + ((n >> 6) & 0x3f));
+            *p++ = (u_char) (0x80 + (n & 0x3f));
+            continue;
+        }
+
+        if (n >= 0x0800) {
+            *p++ = (u_char) (0xe0 + (n >> 12));
+            *p++ = (u_char) (0x80 + ((n >> 6) & 0x3f));
+            *p++ = (u_char) (0x80 + (n & 0x3f));
+            continue;
+        }
+
+        *p++ = (u_char) (0xc0 + (n >> 6));
+        *p++ = (u_char) (0x80 + (n & 0x3f));
+    }
+
+    /* the given buffer is not enough, allocate a new one */
+
+    for (j = u; *j; j++) { /* void */ }
+
+    p = malloc((j - utf16) * 4 + 1);
+    if (p == NULL) {
+        return NULL;
+    }
+
+    if (allocated) {
+        *allocated = (j - utf16) * 4 + 1;
+    }
+
+    ngx_memcpy(p, utf8, *len);
+
+    utf8 = p;
+    p += *len;
+
+    for ( ;; ) {
+
+        if (*u < 0x80) {
+            *p++ = (u_char) *u;
+
+            if (*u == 0) {
+                *len = p - utf8;
+                return utf8;
+            }
+
+            u++;
+
+            continue;
+        }
+
+        n = ngx_utf16_decode(&u, 2);
+
+        if (n > 0x10ffff) {
+            ngx_free(utf8);
+            ngx_set_errno(NGX_EILSEQ);
+            return NULL;
+        }
+
+        if (n >= 0x10000) {
+            *p++ = (u_char) (0xf0 + (n >> 18));
+            *p++ = (u_char) (0x80 + ((n >> 12) & 0x3f));
+            *p++ = (u_char) (0x80 + ((n >> 6) & 0x3f));
+            *p++ = (u_char) (0x80 + (n & 0x3f));
+            continue;
+        }
+
+        if (n >= 0x0800) {
+            *p++ = (u_char) (0xe0 + (n >> 12));
+            *p++ = (u_char) (0x80 + ((n >> 6) & 0x3f));
+            *p++ = (u_char) (0x80 + (n & 0x3f));
+            continue;
+        }
+
+        *p++ = (u_char) (0xc0 + (n >> 6));
+        *p++ = (u_char) (0x80 + (n & 0x3f));
+    }
+
+    /* unreachable */
+}
+
+
+/*
+ * ngx_utf16_decode() decodes one or two UTF-16 code units
+ * the return values:
+ *    0x80 - 0x10ffff         valid character
+ *    0x110000 - 0xfffffffd   invalid sequence
+ *    0xfffffffe              incomplete sequence
+ *    0xffffffff              error
+ */
+
+uint32_t
+ngx_utf16_decode(u_short **u, size_t n)
+{
+    uint32_t  k, m;
+
+    k = **u;
+
+    if (k < 0xd800 || k > 0xdfff) {
+        (*u)++;
+        return k;
+    }
+
+    if (k > 0xdbff) {
+        (*u)++;
+        return 0xffffffff;
+    }
+
+    if (n < 2) {
+        return 0xfffffffe;
+    }
+
+    (*u)++;
+
+    m = *(*u)++;
+
+    if (m < 0xdc00 || m > 0xdfff) {
+        return 0xffffffff;
+
+    }
+
+    return 0x10000 + ((k - 0xd800) << 10) + (m - 0xdc00);
+}
diff --git a/src/os/win32/ngx_files.h b/src/os/win32/ngx_files.h
new file mode 100644
index 0000000..78ba13a
--- /dev/null
+++ b/src/os/win32/ngx_files.h
@@ -0,0 +1,272 @@
+
+/*
+ * Copyright (C) Igor Sysoev
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_FILES_H_INCLUDED_
+#define _NGX_FILES_H_INCLUDED_
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+
+typedef HANDLE                      ngx_fd_t;
+typedef BY_HANDLE_FILE_INFORMATION  ngx_file_info_t;
+typedef uint64_t                    ngx_file_uniq_t;
+
+
+typedef struct {
+    u_char                         *name;
+    size_t                          size;
+    void                           *addr;
+    ngx_fd_t                        fd;
+    HANDLE                          handle;
+    ngx_log_t                      *log;
+} ngx_file_mapping_t;
+
+
+typedef struct {
+    HANDLE                          dir;
+    WIN32_FIND_DATAW                finddata;
+
+    u_char                         *name;
+    size_t                          namelen;
+    size_t                          allocated;
+
+    unsigned                        valid_info:1;
+    unsigned                        type:1;
+    unsigned                        ready:1;
+} ngx_dir_t;
+
+
+typedef struct {
+    HANDLE                          dir;
+    WIN32_FIND_DATAW                finddata;
+
+    unsigned                        ready:1;
+    unsigned                        test:1;
+    unsigned                        no_match:1;
+
+    u_char                         *pattern;
+    ngx_str_t                       name;
+    size_t                          last;
+    ngx_log_t                      *log;
+} ngx_glob_t;
+
+
+
+/* INVALID_FILE_ATTRIBUTES is specified but not defined at least in MSVC6SP2 */
+#ifndef INVALID_FILE_ATTRIBUTES
+#define INVALID_FILE_ATTRIBUTES     0xffffffff
+#endif
+
+/* INVALID_SET_FILE_POINTER is not defined at least in MSVC6SP2 */
+#ifndef INVALID_SET_FILE_POINTER
+#define INVALID_SET_FILE_POINTER    0xffffffff
+#endif
+
+
+#define NGX_INVALID_FILE            INVALID_HANDLE_VALUE
+#define NGX_FILE_ERROR              0
+
+
+ngx_fd_t ngx_open_file(u_char *name, u_long mode, u_long create, u_long access);
+#define ngx_open_file_n             "CreateFile()"
+
+#define NGX_FILE_RDONLY             GENERIC_READ
+#define NGX_FILE_WRONLY             GENERIC_WRITE
+#define NGX_FILE_RDWR               GENERIC_READ|GENERIC_WRITE
+#define NGX_FILE_APPEND             FILE_APPEND_DATA|SYNCHRONIZE
+#define NGX_FILE_NONBLOCK           0
+
+#define NGX_FILE_CREATE_OR_OPEN     OPEN_ALWAYS
+#define NGX_FILE_OPEN               OPEN_EXISTING
+#define NGX_FILE_TRUNCATE           CREATE_ALWAYS
+
+#define NGX_FILE_DEFAULT_ACCESS     0
+#define NGX_FILE_OWNER_ACCESS       0
+
+
+ngx_fd_t ngx_open_tempfile(u_char *name, ngx_uint_t persistent,
+    ngx_uint_t access);
+#define ngx_open_tempfile_n         "CreateFile()"
+
+
+#define ngx_close_file              CloseHandle
+#define ngx_close_file_n            "CloseHandle()"
+
+
+ssize_t ngx_read_fd(ngx_fd_t fd, void *buf, size_t size);
+#define ngx_read_fd_n               "ReadFile()"
+
+
+ssize_t ngx_write_fd(ngx_fd_t fd, void *buf, size_t size);
+#define ngx_write_fd_n              "WriteFile()"
+
+
+ssize_t ngx_write_console(ngx_fd_t fd, void *buf, size_t size);
+
+
+#define ngx_linefeed(p)             *p++ = CR; *p++ = LF;
+#define NGX_LINEFEED_SIZE           2
+#define NGX_LINEFEED                CRLF
+
+
+ngx_int_t ngx_delete_file(u_char *name);
+#define ngx_delete_file_n           "DeleteFile()"
+
+
+ngx_int_t ngx_rename_file(u_char *from, u_char *to);
+#define ngx_rename_file_n           "MoveFile()"
+ngx_err_t ngx_win32_rename_file(ngx_str_t *from, ngx_str_t *to, ngx_log_t *log);
+
+
+
+ngx_int_t ngx_set_file_time(u_char *name, ngx_fd_t fd, time_t s);
+#define ngx_set_file_time_n         "SetFileTime()"
+
+
+ngx_int_t ngx_file_info(u_char *filename, ngx_file_info_t *fi);
+#define ngx_file_info_n             "GetFileAttributesEx()"
+
+
+#define ngx_fd_info(fd, fi)         GetFileInformationByHandle(fd, fi)
+#define ngx_fd_info_n               "GetFileInformationByHandle()"
+
+
+#define ngx_link_info(name, fi)     ngx_file_info(name, fi)
+#define ngx_link_info_n             "GetFileAttributesEx()"
+
+
+#define ngx_is_dir(fi)                                                       \
+    (((fi)->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0)
+#define ngx_is_file(fi)                                                      \
+    (((fi)->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0)
+#define ngx_is_link(fi)     0
+#define ngx_is_exec(fi)     0
+
+#define ngx_file_access(fi) 0
+
+#define ngx_file_size(fi)                                                    \
+    (((off_t) (fi)->nFileSizeHigh << 32) | (fi)->nFileSizeLow)
+#define ngx_file_fs_size(fi)        ngx_file_size(fi)
+
+#define ngx_file_uniq(fi)                                                    \
+    (((ngx_file_uniq_t) (fi)->nFileIndexHigh << 32) | (fi)->nFileIndexLow)
+
+
+/* 116444736000000000 is commented in src/os/win32/ngx_time.c */
+
+#define ngx_file_mtime(fi)                                                   \
+ (time_t) (((((unsigned __int64) (fi)->ftLastWriteTime.dwHighDateTime << 32) \
+                               | (fi)->ftLastWriteTime.dwLowDateTime)        \
+                                          - 116444736000000000) / 10000000)
+
+ngx_int_t ngx_create_file_mapping(ngx_file_mapping_t *fm);
+void ngx_close_file_mapping(ngx_file_mapping_t *fm);
+
+
+u_char *ngx_realpath(u_char *path, u_char *resolved);
+#define ngx_realpath_n              ""
+
+
+size_t ngx_getcwd(u_char *buf, size_t size);
+#define ngx_getcwd_n                "GetCurrentDirectory()"
+
+
+#define ngx_path_separator(c)       ((c) == '/' || (c) == '\\')
+
+#define NGX_HAVE_MAX_PATH           1
+#define NGX_MAX_PATH                MAX_PATH
+
+
+ngx_int_t ngx_open_dir(ngx_str_t *name, ngx_dir_t *dir);
+#define ngx_open_dir_n              "FindFirstFile()"
+
+
+ngx_int_t ngx_read_dir(ngx_dir_t *dir);
+#define ngx_read_dir_n              "FindNextFile()"
+
+
+ngx_int_t ngx_close_dir(ngx_dir_t *dir);
+#define ngx_close_dir_n             "FindClose()"
+
+
+ngx_int_t ngx_create_dir(u_char *name, ngx_uint_t access);
+#define ngx_create_dir_n            "CreateDirectory()"
+
+
+ngx_int_t ngx_delete_dir(u_char *name);
+#define ngx_delete_dir_n            "RemoveDirectory()"
+
+
+#define ngx_dir_access(a)           (a)
+
+
+#define ngx_de_name(dir)            (dir)->name
+#define ngx_de_namelen(dir)         (dir)->namelen
+
+ngx_int_t ngx_de_info(u_char *name, ngx_dir_t *dir);
+#define ngx_de_info_n               "dummy()"
+
+ngx_int_t ngx_de_link_info(u_char *name, ngx_dir_t *dir);
+#define ngx_de_link_info_n          "dummy()"
+
+#define ngx_de_is_dir(dir)                                                   \
+    (((dir)->finddata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0)
+#define ngx_de_is_file(dir)                                                  \
+    (((dir)->finddata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0)
+#define ngx_de_is_link(dir)         0
+#define ngx_de_access(dir)          0
+#define ngx_de_size(dir)                                                     \
+  (((off_t) (dir)->finddata.nFileSizeHigh << 32) | (dir)->finddata.nFileSizeLow)
+#define ngx_de_fs_size(dir)         ngx_de_size(dir)
+
+/* 116444736000000000 is commented in src/os/win32/ngx_time.c */
+
+#define ngx_de_mtime(dir)                                                    \
+    (time_t) (((((unsigned __int64)                                          \
+                     (dir)->finddata.ftLastWriteTime.dwHighDateTime << 32)   \
+                      | (dir)->finddata.ftLastWriteTime.dwLowDateTime)       \
+                                          - 116444736000000000) / 10000000)
+
+
+ngx_int_t ngx_open_glob(ngx_glob_t *gl);
+#define ngx_open_glob_n             "FindFirstFile()"
+
+ngx_int_t ngx_read_glob(ngx_glob_t *gl, ngx_str_t *name);
+void ngx_close_glob(ngx_glob_t *gl);
+
+
+ssize_t ngx_read_file(ngx_file_t *file, u_char *buf, size_t size, off_t offset);
+#define ngx_read_file_n             "ReadFile()"
+
+ssize_t ngx_write_file(ngx_file_t *file, u_char *buf, size_t size,
+    off_t offset);
+
+ssize_t ngx_write_chain_to_file(ngx_file_t *file, ngx_chain_t *ce,
+    off_t offset, ngx_pool_t *pool);
+
+ngx_int_t ngx_read_ahead(ngx_fd_t fd, size_t n);
+#define ngx_read_ahead_n            "ngx_read_ahead_n"
+
+ngx_int_t ngx_directio_on(ngx_fd_t fd);
+#define ngx_directio_on_n           "ngx_directio_on_n"
+
+ngx_int_t ngx_directio_off(ngx_fd_t fd);
+#define ngx_directio_off_n          "ngx_directio_off_n"
+
+size_t ngx_fs_bsize(u_char *name);
+off_t ngx_fs_available(u_char *name);
+
+
+#define ngx_stdout               GetStdHandle(STD_OUTPUT_HANDLE)
+#define ngx_stderr               GetStdHandle(STD_ERROR_HANDLE)
+#define ngx_set_stderr(fd)       SetStdHandle(STD_ERROR_HANDLE, fd)
+#define ngx_set_stderr_n         "SetStdHandle(STD_ERROR_HANDLE)"
+
+
+#endif /* _NGX_FILES_H_INCLUDED_ */
diff --git a/src/os/win32/ngx_os.h b/src/os/win32/ngx_os.h
new file mode 100644
index 0000000..15f5aa0
--- /dev/null
+++ b/src/os/win32/ngx_os.h
@@ -0,0 +1,68 @@
+
+/*
+ * Copyright (C) Igor Sysoev
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_OS_H_INCLUDED_
+#define _NGX_OS_H_INCLUDED_
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+
+#define NGX_IO_SENDFILE    1
+
+
+typedef ssize_t (*ngx_recv_pt)(ngx_connection_t *c, u_char *buf, size_t size);
+typedef ssize_t (*ngx_recv_chain_pt)(ngx_connection_t *c, ngx_chain_t *in,
+    off_t limit);
+typedef ssize_t (*ngx_send_pt)(ngx_connection_t *c, u_char *buf, size_t size);
+typedef ngx_chain_t *(*ngx_send_chain_pt)(ngx_connection_t *c, ngx_chain_t *in,
+    off_t limit);
+
+typedef struct {
+    ngx_recv_pt        recv;
+    ngx_recv_chain_pt  recv_chain;
+    ngx_recv_pt        udp_recv;
+    ngx_send_pt        send;
+    ngx_send_pt        udp_send;
+    ngx_send_chain_pt  udp_send_chain;
+    ngx_send_chain_pt  send_chain;
+    ngx_uint_t         flags;
+} ngx_os_io_t;
+
+
+ngx_int_t ngx_os_init(ngx_log_t *log);
+void ngx_os_status(ngx_log_t *log);
+ngx_int_t ngx_os_signal_process(ngx_cycle_t *cycle, char *sig, ngx_pid_t pid);
+
+ssize_t ngx_wsarecv(ngx_connection_t *c, u_char *buf, size_t size);
+ssize_t ngx_overlapped_wsarecv(ngx_connection_t *c, u_char *buf, size_t size);
+ssize_t ngx_udp_wsarecv(ngx_connection_t *c, u_char *buf, size_t size);
+ssize_t ngx_udp_overlapped_wsarecv(ngx_connection_t *c, u_char *buf,
+    size_t size);
+ssize_t ngx_wsarecv_chain(ngx_connection_t *c, ngx_chain_t *chain, off_t limit);
+ssize_t ngx_wsasend(ngx_connection_t *c, u_char *buf, size_t size);
+ssize_t ngx_overlapped_wsasend(ngx_connection_t *c, u_char *buf, size_t size);
+ngx_chain_t *ngx_wsasend_chain(ngx_connection_t *c, ngx_chain_t *in,
+    off_t limit);
+ngx_chain_t *ngx_overlapped_wsasend_chain(ngx_connection_t *c, ngx_chain_t *in,
+    off_t limit);
+
+void ngx_cdecl ngx_event_log(ngx_err_t err, const char *fmt, ...);
+
+
+extern ngx_os_io_t  ngx_os_io;
+extern ngx_uint_t   ngx_ncpu;
+extern ngx_uint_t   ngx_max_wsabufs;
+extern ngx_int_t    ngx_max_sockets;
+extern ngx_uint_t   ngx_inherited_nonblocking;
+extern ngx_uint_t   ngx_tcp_nodelay_and_tcp_nopush;
+extern ngx_uint_t   ngx_win32_version;
+extern char         ngx_unique[];
+
+
+#endif /* _NGX_OS_H_INCLUDED_ */
diff --git a/src/os/win32/ngx_process.c b/src/os/win32/ngx_process.c
new file mode 100644
index 0000000..57b1ae9
--- /dev/null
+++ b/src/os/win32/ngx_process.c
@@ -0,0 +1,238 @@
+
+/*
+ * Copyright (C) Igor Sysoev
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+
+int              ngx_argc;
+char           **ngx_argv;
+char           **ngx_os_argv;
+
+ngx_int_t        ngx_last_process;
+ngx_process_t    ngx_processes[NGX_MAX_PROCESSES];
+
+
+ngx_pid_t
+ngx_spawn_process(ngx_cycle_t *cycle, char *name, ngx_int_t respawn)
+{
+    u_long          rc, n, code;
+    ngx_int_t       s;
+    ngx_pid_t       pid;
+    ngx_exec_ctx_t  ctx;
+    HANDLE          events[2];
+    char            file[MAX_PATH + 1];
+
+    if (respawn >= 0) {
+        s = respawn;
+
+    } else {
+        for (s = 0; s < ngx_last_process; s++) {
+            if (ngx_processes[s].handle == NULL) {
+                break;
+            }
+        }
+
+        if (s == NGX_MAX_PROCESSES) {
+            ngx_log_error(NGX_LOG_ALERT, cycle->log, 0,
+                          "no more than %d processes can be spawned",
+                          NGX_MAX_PROCESSES);
+            return NGX_INVALID_PID;
+        }
+    }
+
+    n = GetModuleFileName(NULL, file, MAX_PATH);
+
+    if (n == 0) {
+        ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
+                      "GetModuleFileName() failed");
+        return NGX_INVALID_PID;
+    }
+
+    file[n] = '\0';
+
+    ngx_log_debug1(NGX_LOG_DEBUG_CORE, cycle->log, 0,
+                   "GetModuleFileName: \"%s\"", file);
+
+    ctx.path = file;
+    ctx.name = name;
+    ctx.args = GetCommandLine();
+    ctx.argv = NULL;
+    ctx.envp = NULL;
+
+    pid = ngx_execute(cycle, &ctx);
+
+    if (pid == NGX_INVALID_PID) {
+        return pid;
+    }
+
+    ngx_memzero(&ngx_processes[s], sizeof(ngx_process_t));
+
+    ngx_processes[s].handle = ctx.child;
+    ngx_processes[s].pid = pid;
+    ngx_processes[s].name = name;
+
+    ngx_sprintf(ngx_processes[s].term_event, "ngx_%s_term_%P%Z", name, pid);
+    ngx_sprintf(ngx_processes[s].quit_event, "ngx_%s_quit_%P%Z", name, pid);
+    ngx_sprintf(ngx_processes[s].reopen_event, "ngx_%s_reopen_%P%Z",
+                name, pid);
+
+    events[0] = ngx_master_process_event;
+    events[1] = ctx.child;
+
+    rc = WaitForMultipleObjects(2, events, 0, 5000);
+
+    ngx_time_update();
+
+    ngx_log_debug1(NGX_LOG_DEBUG_CORE, cycle->log, 0,
+                   "WaitForMultipleObjects: %ul", rc);
+
+    switch (rc) {
+
+    case WAIT_OBJECT_0:
+
+        ngx_processes[s].term = OpenEvent(EVENT_MODIFY_STATE, 0,
+                                          (char *) ngx_processes[s].term_event);
+        if (ngx_processes[s].term == NULL) {
+            ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
+                          "OpenEvent(\"%s\") failed",
+                          ngx_processes[s].term_event);
+            goto failed;
+        }
+
+        ngx_processes[s].quit = OpenEvent(EVENT_MODIFY_STATE, 0,
+                                          (char *) ngx_processes[s].quit_event);
+        if (ngx_processes[s].quit == NULL) {
+            ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
+                          "OpenEvent(\"%s\") failed",
+                          ngx_processes[s].quit_event);
+            goto failed;
+        }
+
+        ngx_processes[s].reopen = OpenEvent(EVENT_MODIFY_STATE, 0,
+                                       (char *) ngx_processes[s].reopen_event);
+        if (ngx_processes[s].reopen == NULL) {
+            ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
+                          "OpenEvent(\"%s\") failed",
+                          ngx_processes[s].reopen_event);
+            goto failed;
+        }
+
+        if (ResetEvent(ngx_master_process_event) == 0) {
+            ngx_log_error(NGX_LOG_ALERT, cycle->log, 0,
+                          "ResetEvent(\"%s\") failed",
+                          ngx_master_process_event_name);
+            goto failed;
+        }
+
+        break;
+
+    case WAIT_OBJECT_0 + 1:
+        if (GetExitCodeProcess(ctx.child, &code) == 0) {
+            ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
+                          "GetExitCodeProcess(%P) failed", pid);
+        }
+
+        ngx_log_error(NGX_LOG_ALERT, cycle->log, 0,
+                      "%s process %P exited with code %Xl",
+                      name, pid, code);
+
+        goto failed;
+
+    case WAIT_TIMEOUT:
+        ngx_log_error(NGX_LOG_ALERT, cycle->log, 0,
+                      "the event \"%s\" was not signaled for 5s",
+                      ngx_master_process_event_name);
+        goto failed;
+
+    case WAIT_FAILED:
+        ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
+                      "WaitForSingleObject(\"%s\") failed",
+                      ngx_master_process_event_name);
+
+        goto failed;
+    }
+
+    if (respawn >= 0) {
+        return pid;
+    }
+
+    switch (respawn) {
+
+    case NGX_PROCESS_RESPAWN:
+        ngx_processes[s].just_spawn = 0;
+        break;
+
+    case NGX_PROCESS_JUST_RESPAWN:
+        ngx_processes[s].just_spawn = 1;
+        break;
+    }
+
+    if (s == ngx_last_process) {
+        ngx_last_process++;
+    }
+
+    return pid;
+
+failed:
+
+    if (ngx_processes[s].reopen) {
+        ngx_close_handle(ngx_processes[s].reopen);
+    }
+
+    if (ngx_processes[s].quit) {
+        ngx_close_handle(ngx_processes[s].quit);
+    }
+
+    if (ngx_processes[s].term) {
+        ngx_close_handle(ngx_processes[s].term);
+    }
+
+    TerminateProcess(ngx_processes[s].handle, 2);
+
+    if (ngx_processes[s].handle) {
+        ngx_close_handle(ngx_processes[s].handle);
+        ngx_processes[s].handle = NULL;
+    }
+
+    return NGX_INVALID_PID;
+}
+
+
+ngx_pid_t
+ngx_execute(ngx_cycle_t *cycle, ngx_exec_ctx_t *ctx)
+{
+    STARTUPINFO          si;
+    PROCESS_INFORMATION  pi;
+
+    ngx_memzero(&si, sizeof(STARTUPINFO));
+    si.cb = sizeof(STARTUPINFO);
+
+    ngx_memzero(&pi, sizeof(PROCESS_INFORMATION));
+
+    if (CreateProcess(ctx->path, ctx->args,
+                      NULL, NULL, 0, CREATE_NO_WINDOW, NULL, NULL, &si, &pi)
+        == 0)
+    {
+        ngx_log_error(NGX_LOG_CRIT, cycle->log, ngx_errno,
+                      "CreateProcess(\"%s\") failed", ngx_argv[0]);
+
+        return 0;
+    }
+
+    ctx->child = pi.hProcess;
+
+    if (CloseHandle(pi.hThread) == 0) {
+        ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
+                      "CloseHandle(pi.hThread) failed");
+    }
+
+    ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0,
+                  "start %s process %P", ctx->name, pi.dwProcessId);
+
+    return pi.dwProcessId;
+}
diff --git a/src/os/win32/ngx_process.h b/src/os/win32/ngx_process.h
new file mode 100644
index 0000000..7ec4cd9
--- /dev/null
+++ b/src/os/win32/ngx_process.h
@@ -0,0 +1,80 @@
+
+/*
+ * Copyright (C) Igor Sysoev
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_PROCESS_H_INCLUDED_
+#define _NGX_PROCESS_H_INCLUDED_
+
+
+typedef DWORD               ngx_pid_t;
+#define NGX_INVALID_PID     0
+
+
+#define ngx_getpid          GetCurrentProcessId
+#define ngx_getppid()       0
+#define ngx_log_pid         ngx_pid
+
+
+#define NGX_PROCESS_SYNC_NAME                                                 \
+    (sizeof("ngx_cache_manager_mutex_") + NGX_INT32_LEN)
+
+
+typedef uint64_t            ngx_cpuset_t;
+
+
+typedef struct {
+    HANDLE                  handle;
+    ngx_pid_t               pid;
+    char                   *name;
+
+    HANDLE                  term;
+    HANDLE                  quit;
+    HANDLE                  reopen;
+
+    u_char                  term_event[NGX_PROCESS_SYNC_NAME];
+    u_char                  quit_event[NGX_PROCESS_SYNC_NAME];
+    u_char                  reopen_event[NGX_PROCESS_SYNC_NAME];
+
+    unsigned                just_spawn:1;
+    unsigned                exiting:1;
+} ngx_process_t;
+
+
+typedef struct {
+    char                   *path;
+    char                   *name;
+    char                   *args;
+    char *const            *argv;
+    char *const            *envp;
+    HANDLE                  child;
+} ngx_exec_ctx_t;
+
+
+ngx_pid_t ngx_spawn_process(ngx_cycle_t *cycle, char *name, ngx_int_t respawn);
+ngx_pid_t ngx_execute(ngx_cycle_t *cycle, ngx_exec_ctx_t *ctx);
+
+#define ngx_debug_point()
+#define ngx_sched_yield()   SwitchToThread()
+
+
+#define NGX_MAX_PROCESSES         (MAXIMUM_WAIT_OBJECTS - 4)
+
+#define NGX_PROCESS_RESPAWN       -2
+#define NGX_PROCESS_JUST_RESPAWN  -3
+
+
+extern int                  ngx_argc;
+extern char               **ngx_argv;
+extern char               **ngx_os_argv;
+
+extern ngx_int_t            ngx_last_process;
+extern ngx_process_t        ngx_processes[NGX_MAX_PROCESSES];
+
+extern ngx_pid_t            ngx_pid;
+extern ngx_pid_t            ngx_parent;
+
+
+#endif /* _NGX_PROCESS_H_INCLUDED_ */
diff --git a/src/os/win32/ngx_process_cycle.c b/src/os/win32/ngx_process_cycle.c
new file mode 100644
index 0000000..a39335f
--- /dev/null
+++ b/src/os/win32/ngx_process_cycle.c
@@ -0,0 +1,1044 @@
+
+/*
+ * Copyright (C) Igor Sysoev
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_event.h>
+#include <nginx.h>
+
+
+static void ngx_console_init(ngx_cycle_t *cycle);
+static int __stdcall ngx_console_handler(u_long type);
+static ngx_int_t ngx_create_signal_events(ngx_cycle_t *cycle);
+static ngx_int_t ngx_start_worker_processes(ngx_cycle_t *cycle, ngx_int_t type);
+static void ngx_reopen_worker_processes(ngx_cycle_t *cycle);
+static void ngx_quit_worker_processes(ngx_cycle_t *cycle, ngx_uint_t old);
+static void ngx_terminate_worker_processes(ngx_cycle_t *cycle);
+static ngx_uint_t ngx_reap_worker(ngx_cycle_t *cycle, HANDLE h);
+static void ngx_master_process_exit(ngx_cycle_t *cycle);
+static void ngx_worker_process_cycle(ngx_cycle_t *cycle, char *mevn);
+static void ngx_worker_process_exit(ngx_cycle_t *cycle);
+static ngx_thread_value_t __stdcall ngx_worker_thread(void *data);
+static ngx_thread_value_t __stdcall ngx_cache_manager_thread(void *data);
+static void ngx_cache_manager_process_handler(void);
+static ngx_thread_value_t __stdcall ngx_cache_loader_thread(void *data);
+
+
+ngx_uint_t     ngx_process;
+ngx_uint_t     ngx_worker;
+ngx_pid_t      ngx_pid;
+ngx_pid_t      ngx_parent;
+
+ngx_uint_t     ngx_inherited;
+ngx_pid_t      ngx_new_binary;
+
+sig_atomic_t   ngx_terminate;
+sig_atomic_t   ngx_quit;
+sig_atomic_t   ngx_reopen;
+sig_atomic_t   ngx_reconfigure;
+ngx_uint_t     ngx_exiting;
+
+
+HANDLE         ngx_master_process_event;
+char           ngx_master_process_event_name[NGX_PROCESS_SYNC_NAME];
+
+static HANDLE  ngx_stop_event;
+static char    ngx_stop_event_name[NGX_PROCESS_SYNC_NAME];
+static HANDLE  ngx_quit_event;
+static char    ngx_quit_event_name[NGX_PROCESS_SYNC_NAME];
+static HANDLE  ngx_reopen_event;
+static char    ngx_reopen_event_name[NGX_PROCESS_SYNC_NAME];
+static HANDLE  ngx_reload_event;
+static char    ngx_reload_event_name[NGX_PROCESS_SYNC_NAME];
+
+HANDLE         ngx_cache_manager_mutex;
+char           ngx_cache_manager_mutex_name[NGX_PROCESS_SYNC_NAME];
+HANDLE         ngx_cache_manager_event;
+
+
+void
+ngx_master_process_cycle(ngx_cycle_t *cycle)
+{
+    u_long      nev, ev, timeout;
+    ngx_err_t   err;
+    ngx_int_t   n;
+    ngx_msec_t  timer;
+    ngx_uint_t  live;
+    HANDLE      events[MAXIMUM_WAIT_OBJECTS];
+
+    ngx_sprintf((u_char *) ngx_master_process_event_name,
+                "ngx_master_%s%Z", ngx_unique);
+
+    if (ngx_process == NGX_PROCESS_WORKER) {
+        ngx_worker_process_cycle(cycle, ngx_master_process_event_name);
+        return;
+    }
+
+    ngx_log_debug0(NGX_LOG_DEBUG_CORE, cycle->log, 0, "master started");
+
+    ngx_console_init(cycle);
+
+    SetEnvironmentVariable("ngx_unique", ngx_unique);
+
+    ngx_master_process_event = CreateEvent(NULL, 1, 0,
+                                           ngx_master_process_event_name);
+    if (ngx_master_process_event == NULL) {
+        ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
+                      "CreateEvent(\"%s\") failed",
+                      ngx_master_process_event_name);
+        exit(2);
+    }
+
+    if (ngx_create_signal_events(cycle) != NGX_OK) {
+        exit(2);
+    }
+
+    ngx_sprintf((u_char *) ngx_cache_manager_mutex_name,
+                "ngx_cache_manager_mutex_%s%Z", ngx_unique);
+
+    ngx_cache_manager_mutex = CreateMutex(NULL, 0,
+                                          ngx_cache_manager_mutex_name);
+    if (ngx_cache_manager_mutex == NULL) {
+        ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
+                   "CreateMutex(\"%s\") failed", ngx_cache_manager_mutex_name);
+        exit(2);
+    }
+
+
+    events[0] = ngx_stop_event;
+    events[1] = ngx_quit_event;
+    events[2] = ngx_reopen_event;
+    events[3] = ngx_reload_event;
+
+    ngx_close_listening_sockets(cycle);
+
+    if (ngx_start_worker_processes(cycle, NGX_PROCESS_RESPAWN) == 0) {
+        exit(2);
+    }
+
+    timer = 0;
+    timeout = INFINITE;
+
+    for ( ;; ) {
+
+        nev = 4;
+        for (n = 0; n < ngx_last_process; n++) {
+            if (ngx_processes[n].handle) {
+                events[nev++] = ngx_processes[n].handle;
+            }
+        }
+
+        if (timer) {
+            timeout = timer > ngx_current_msec ? timer - ngx_current_msec : 0;
+        }
+
+        ev = WaitForMultipleObjects(nev, events, 0, timeout);
+
+        err = ngx_errno;
+        ngx_time_update();
+
+        ngx_log_debug1(NGX_LOG_DEBUG_CORE, cycle->log, 0,
+                       "master WaitForMultipleObjects: %ul", ev);
+
+        if (ev == WAIT_OBJECT_0) {
+            ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "exiting");
+
+            if (ResetEvent(ngx_stop_event) == 0) {
+                ngx_log_error(NGX_LOG_ALERT, cycle->log, 0,
+                              "ResetEvent(\"%s\") failed", ngx_stop_event_name);
+            }
+
+            if (timer == 0) {
+                timer = ngx_current_msec + 5000;
+            }
+
+            ngx_terminate = 1;
+            ngx_quit_worker_processes(cycle, 0);
+
+            continue;
+        }
+
+        if (ev == WAIT_OBJECT_0 + 1) {
+            ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "shutting down");
+
+            if (ResetEvent(ngx_quit_event) == 0) {
+                ngx_log_error(NGX_LOG_ALERT, cycle->log, 0,
+                              "ResetEvent(\"%s\") failed", ngx_quit_event_name);
+            }
+
+            ngx_quit = 1;
+            ngx_quit_worker_processes(cycle, 0);
+
+            continue;
+        }
+
+        if (ev == WAIT_OBJECT_0 + 2) {
+            ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "reopening logs");
+
+            if (ResetEvent(ngx_reopen_event) == 0) {
+                ngx_log_error(NGX_LOG_ALERT, cycle->log, 0,
+                              "ResetEvent(\"%s\") failed",
+                              ngx_reopen_event_name);
+            }
+
+            ngx_reopen_files(cycle, -1);
+            ngx_reopen_worker_processes(cycle);
+
+            continue;
+        }
+
+        if (ev == WAIT_OBJECT_0 + 3) {
+            ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "reconfiguring");
+
+            if (ResetEvent(ngx_reload_event) == 0) {
+                ngx_log_error(NGX_LOG_ALERT, cycle->log, 0,
+                              "ResetEvent(\"%s\") failed",
+                              ngx_reload_event_name);
+            }
+
+            cycle = ngx_init_cycle(cycle);
+            if (cycle == NULL) {
+                cycle = (ngx_cycle_t *) ngx_cycle;
+                continue;
+            }
+
+            ngx_cycle = cycle;
+
+            ngx_close_listening_sockets(cycle);
+
+            if (ngx_start_worker_processes(cycle, NGX_PROCESS_JUST_RESPAWN)) {
+                ngx_quit_worker_processes(cycle, 1);
+            }
+
+            continue;
+        }
+
+        if (ev > WAIT_OBJECT_0 + 3 && ev < WAIT_OBJECT_0 + nev) {
+
+            ngx_log_debug0(NGX_LOG_DEBUG_CORE, cycle->log, 0, "reap worker");
+
+            live = ngx_reap_worker(cycle, events[ev]);
+
+            if (!live && (ngx_terminate || ngx_quit)) {
+                ngx_master_process_exit(cycle);
+            }
+
+            continue;
+        }
+
+        if (ev == WAIT_TIMEOUT) {
+            ngx_terminate_worker_processes(cycle);
+
+            ngx_master_process_exit(cycle);
+        }
+
+        if (ev == WAIT_FAILED) {
+            ngx_log_error(NGX_LOG_ALERT, cycle->log, err,
+                          "WaitForMultipleObjects() failed");
+
+            continue;
+        }
+
+        ngx_log_error(NGX_LOG_ALERT, cycle->log, 0,
+            "WaitForMultipleObjects() returned unexpected value %ul", ev);
+    }
+}
+
+
+static void
+ngx_console_init(ngx_cycle_t *cycle)
+{
+    ngx_core_conf_t  *ccf;
+
+    ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module);
+
+    if (ccf->daemon) {
+        if (FreeConsole() == 0) {
+            ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
+                          "FreeConsole() failed");
+        }
+
+        return;
+    }
+
+    if (SetConsoleCtrlHandler(ngx_console_handler, 1) == 0) {
+        ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
+                      "SetConsoleCtrlHandler() failed");
+    }
+}
+
+
+static int __stdcall
+ngx_console_handler(u_long type)
+{
+    char  *msg;
+
+    switch (type) {
+
+    case CTRL_C_EVENT:
+        msg = "Ctrl-C pressed, exiting";
+        break;
+
+    case CTRL_BREAK_EVENT:
+        msg = "Ctrl-Break pressed, exiting";
+        break;
+
+    case CTRL_CLOSE_EVENT:
+        msg = "console closing, exiting";
+        break;
+
+    case CTRL_LOGOFF_EVENT:
+        msg = "user logs off, exiting";
+        break;
+
+    default:
+        return 0;
+    }
+
+    ngx_log_error(NGX_LOG_NOTICE, ngx_cycle->log, 0, msg);
+
+    if (ngx_stop_event == NULL) {
+        return 1;
+    }
+
+    if (SetEvent(ngx_stop_event) == 0) {
+        ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, 0,
+                      "SetEvent(\"%s\") failed", ngx_stop_event_name);
+    }
+
+    return 1;
+}
+
+
+static ngx_int_t
+ngx_create_signal_events(ngx_cycle_t *cycle)
+{
+    ngx_sprintf((u_char *) ngx_stop_event_name,
+                "Global\\ngx_stop_%s%Z", ngx_unique);
+
+    ngx_stop_event = CreateEvent(NULL, 1, 0, ngx_stop_event_name);
+    if (ngx_stop_event == NULL) {
+        ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
+                      "CreateEvent(\"%s\") failed", ngx_stop_event_name);
+        return NGX_ERROR;
+    }
+
+
+    ngx_sprintf((u_char *) ngx_quit_event_name,
+                "Global\\ngx_quit_%s%Z", ngx_unique);
+
+    ngx_quit_event = CreateEvent(NULL, 1, 0, ngx_quit_event_name);
+    if (ngx_quit_event == NULL) {
+        ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
+                      "CreateEvent(\"%s\") failed", ngx_quit_event_name);
+        return NGX_ERROR;
+    }
+
+
+    ngx_sprintf((u_char *) ngx_reopen_event_name,
+                "Global\\ngx_reopen_%s%Z", ngx_unique);
+
+    ngx_reopen_event = CreateEvent(NULL, 1, 0, ngx_reopen_event_name);
+    if (ngx_reopen_event == NULL) {
+        ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
+                      "CreateEvent(\"%s\") failed", ngx_reopen_event_name);
+        return NGX_ERROR;
+    }
+
+
+    ngx_sprintf((u_char *) ngx_reload_event_name,
+                "Global\\ngx_reload_%s%Z", ngx_unique);
+
+    ngx_reload_event = CreateEvent(NULL, 1, 0, ngx_reload_event_name);
+    if (ngx_reload_event == NULL) {
+        ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
+                      "CreateEvent(\"%s\") failed", ngx_reload_event_name);
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_start_worker_processes(ngx_cycle_t *cycle, ngx_int_t type)
+{
+    ngx_int_t         n;
+    ngx_core_conf_t  *ccf;
+
+    ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "start worker processes");
+
+    ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module);
+
+    for (n = 0; n < ccf->worker_processes; n++) {
+        if (ngx_spawn_process(cycle, "worker", type) == NGX_INVALID_PID) {
+            break;
+        }
+    }
+
+    return n;
+}
+
+
+static void
+ngx_reopen_worker_processes(ngx_cycle_t *cycle)
+{
+    ngx_int_t  n;
+
+    for (n = 0; n < ngx_last_process; n++) {
+
+        if (ngx_processes[n].handle == NULL) {
+            continue;
+        }
+
+        if (SetEvent(ngx_processes[n].reopen) == 0) {
+            ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
+                          "SetEvent(\"%s\") failed",
+                          ngx_processes[n].reopen_event);
+        }
+    }
+}
+
+
+static void
+ngx_quit_worker_processes(ngx_cycle_t *cycle, ngx_uint_t old)
+{
+    ngx_int_t  n;
+
+    for (n = 0; n < ngx_last_process; n++) {
+
+        ngx_log_debug5(NGX_LOG_DEBUG_CORE, cycle->log, 0,
+                       "process: %d %P %p e:%d j:%d",
+                       n,
+                       ngx_processes[n].pid,
+                       ngx_processes[n].handle,
+                       ngx_processes[n].exiting,
+                       ngx_processes[n].just_spawn);
+
+        if (old && ngx_processes[n].just_spawn) {
+            ngx_processes[n].just_spawn = 0;
+            continue;
+        }
+
+        if (ngx_processes[n].handle == NULL) {
+            continue;
+        }
+
+        if (SetEvent(ngx_processes[n].quit) == 0) {
+            ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
+                          "SetEvent(\"%s\") failed",
+                          ngx_processes[n].quit_event);
+        }
+
+        ngx_processes[n].exiting = 1;
+    }
+}
+
+
+static void
+ngx_terminate_worker_processes(ngx_cycle_t *cycle)
+{
+    ngx_int_t  n;
+
+    for (n = 0; n < ngx_last_process; n++) {
+
+        if (ngx_processes[n].handle == NULL) {
+            continue;
+        }
+
+        if (TerminateProcess(ngx_processes[n].handle, 0) == 0) {
+            ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
+                          "TerminateProcess(\"%p\") failed",
+                          ngx_processes[n].handle);
+        }
+
+        ngx_processes[n].exiting = 1;
+
+        ngx_close_handle(ngx_processes[n].reopen);
+        ngx_close_handle(ngx_processes[n].quit);
+        ngx_close_handle(ngx_processes[n].term);
+        ngx_close_handle(ngx_processes[n].handle);
+    }
+}
+
+
+static ngx_uint_t
+ngx_reap_worker(ngx_cycle_t *cycle, HANDLE h)
+{
+    u_long     code;
+    ngx_int_t  n;
+
+    for (n = 0; n < ngx_last_process; n++) {
+
+        if (ngx_processes[n].handle != h) {
+            continue;
+        }
+
+        if (GetExitCodeProcess(h, &code) == 0) {
+            ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
+                          "GetExitCodeProcess(%P) failed",
+                          ngx_processes[n].pid);
+        }
+
+        ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0,
+                      "%s process %P exited with code %Xl",
+                      ngx_processes[n].name, ngx_processes[n].pid, code);
+
+        ngx_close_handle(ngx_processes[n].reopen);
+        ngx_close_handle(ngx_processes[n].quit);
+        ngx_close_handle(ngx_processes[n].term);
+        ngx_close_handle(h);
+
+        ngx_processes[n].handle = NULL;
+        ngx_processes[n].term = NULL;
+        ngx_processes[n].quit = NULL;
+        ngx_processes[n].reopen = NULL;
+
+        if (!ngx_processes[n].exiting && !ngx_terminate && !ngx_quit) {
+
+            if (ngx_spawn_process(cycle, ngx_processes[n].name, n)
+                == NGX_INVALID_PID)
+            {
+                ngx_log_error(NGX_LOG_ALERT, cycle->log, 0,
+                              "could not respawn %s", ngx_processes[n].name);
+
+                if (n == ngx_last_process - 1) {
+                    ngx_last_process--;
+                }
+            }
+        }
+
+        goto found;
+    }
+
+    ngx_log_error(NGX_LOG_ALERT, cycle->log, 0, "unknown process handle %p", h);
+
+found:
+
+    for (n = 0; n < ngx_last_process; n++) {
+
+        ngx_log_debug5(NGX_LOG_DEBUG_CORE, cycle->log, 0,
+                       "process: %d %P %p e:%d j:%d",
+                       n,
+                       ngx_processes[n].pid,
+                       ngx_processes[n].handle,
+                       ngx_processes[n].exiting,
+                       ngx_processes[n].just_spawn);
+
+        if (ngx_processes[n].handle) {
+            return 1;
+        }
+    }
+
+    return 0;
+}
+
+
+static void
+ngx_master_process_exit(ngx_cycle_t *cycle)
+{
+    ngx_uint_t  i;
+
+    ngx_delete_pidfile(cycle);
+
+    ngx_close_handle(ngx_cache_manager_mutex);
+    ngx_close_handle(ngx_stop_event);
+    ngx_close_handle(ngx_quit_event);
+    ngx_close_handle(ngx_reopen_event);
+    ngx_close_handle(ngx_reload_event);
+    ngx_close_handle(ngx_master_process_event);
+
+    ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "exit");
+
+    for (i = 0; cycle->modules[i]; i++) {
+        if (cycle->modules[i]->exit_master) {
+            cycle->modules[i]->exit_master(cycle);
+        }
+    }
+
+    ngx_destroy_pool(cycle->pool);
+
+    exit(0);
+}
+
+
+static void
+ngx_worker_process_cycle(ngx_cycle_t *cycle, char *mevn)
+{
+    char        wtevn[NGX_PROCESS_SYNC_NAME];
+    char        wqevn[NGX_PROCESS_SYNC_NAME];
+    char        wroevn[NGX_PROCESS_SYNC_NAME];
+    HANDLE      mev, events[3];
+    u_long      nev, ev;
+    ngx_err_t   err;
+    ngx_tid_t   wtid, cmtid, cltid;
+    ngx_log_t  *log;
+
+    log = cycle->log;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_CORE, log, 0, "worker started");
+
+    ngx_sprintf((u_char *) wtevn, "ngx_worker_term_%P%Z", ngx_pid);
+    events[0] = CreateEvent(NULL, 1, 0, wtevn);
+    if (events[0] == NULL) {
+        ngx_log_error(NGX_LOG_ALERT, log, ngx_errno,
+                      "CreateEvent(\"%s\") failed", wtevn);
+        goto failed;
+    }
+
+    ngx_sprintf((u_char *) wqevn, "ngx_worker_quit_%P%Z", ngx_pid);
+    events[1] = CreateEvent(NULL, 1, 0, wqevn);
+    if (events[1] == NULL) {
+        ngx_log_error(NGX_LOG_ALERT, log, ngx_errno,
+                      "CreateEvent(\"%s\") failed", wqevn);
+        goto failed;
+    }
+
+    ngx_sprintf((u_char *) wroevn, "ngx_worker_reopen_%P%Z", ngx_pid);
+    events[2] = CreateEvent(NULL, 1, 0, wroevn);
+    if (events[2] == NULL) {
+        ngx_log_error(NGX_LOG_ALERT, log, ngx_errno,
+                      "CreateEvent(\"%s\") failed", wroevn);
+        goto failed;
+    }
+
+    mev = OpenEvent(EVENT_MODIFY_STATE, 0, mevn);
+    if (mev == NULL) {
+        ngx_log_error(NGX_LOG_ALERT, log, ngx_errno,
+                      "OpenEvent(\"%s\") failed", mevn);
+        goto failed;
+    }
+
+    if (SetEvent(mev) == 0) {
+        ngx_log_error(NGX_LOG_ALERT, log, ngx_errno,
+                      "SetEvent(\"%s\") failed", mevn);
+        goto failed;
+    }
+
+
+    ngx_sprintf((u_char *) ngx_cache_manager_mutex_name,
+                "ngx_cache_manager_mutex_%s%Z", ngx_unique);
+
+    ngx_cache_manager_mutex = OpenMutex(SYNCHRONIZE, 0,
+                                        ngx_cache_manager_mutex_name);
+    if (ngx_cache_manager_mutex == NULL) {
+        ngx_log_error(NGX_LOG_ALERT, log, ngx_errno,
+                      "OpenMutex(\"%s\") failed", ngx_cache_manager_mutex_name);
+        goto failed;
+    }
+
+    ngx_cache_manager_event = CreateEvent(NULL, 1, 0, NULL);
+    if (ngx_cache_manager_event == NULL) {
+        ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
+                      "CreateEvent(\"ngx_cache_manager_event\") failed");
+        goto failed;
+    }
+
+
+    if (ngx_create_thread(&wtid, ngx_worker_thread, NULL, log) != 0) {
+        goto failed;
+    }
+
+    if (ngx_create_thread(&cmtid, ngx_cache_manager_thread, NULL, log) != 0) {
+        goto failed;
+    }
+
+    if (ngx_create_thread(&cltid, ngx_cache_loader_thread, NULL, log) != 0) {
+        goto failed;
+    }
+
+    for ( ;; ) {
+        ev = WaitForMultipleObjects(3, events, 0, INFINITE);
+
+        err = ngx_errno;
+        ngx_time_update();
+
+        ngx_log_debug1(NGX_LOG_DEBUG_CORE, log, 0,
+                       "worker WaitForMultipleObjects: %ul", ev);
+
+        if (ev == WAIT_OBJECT_0) {
+            ngx_terminate = 1;
+            ngx_log_error(NGX_LOG_NOTICE, log, 0, "exiting");
+
+            if (ResetEvent(events[0]) == 0) {
+                ngx_log_error(NGX_LOG_ALERT, log, 0,
+                              "ResetEvent(\"%s\") failed", wtevn);
+            }
+
+            break;
+        }
+
+        if (ev == WAIT_OBJECT_0 + 1) {
+            ngx_quit = 1;
+            ngx_log_error(NGX_LOG_NOTICE, log, 0, "gracefully shutting down");
+            break;
+        }
+
+        if (ev == WAIT_OBJECT_0 + 2) {
+            ngx_reopen = 1;
+            ngx_log_error(NGX_LOG_NOTICE, log, 0, "reopening logs");
+
+            if (ResetEvent(events[2]) == 0) {
+                ngx_log_error(NGX_LOG_ALERT, log, 0,
+                              "ResetEvent(\"%s\") failed", wroevn);
+            }
+
+            continue;
+        }
+
+        if (ev == WAIT_FAILED) {
+            ngx_log_error(NGX_LOG_ALERT, log, err,
+                          "WaitForMultipleObjects() failed");
+
+            goto failed;
+        }
+    }
+
+    /* wait threads */
+
+    if (SetEvent(ngx_cache_manager_event) == 0) {
+        ngx_log_error(NGX_LOG_ALERT, log, ngx_errno,
+                      "SetEvent(\"ngx_cache_manager_event\") failed");
+    }
+
+    events[1] = wtid;
+    events[2] = cmtid;
+
+    nev = 3;
+
+    for ( ;; ) {
+        ev = WaitForMultipleObjects(nev, events, 0, INFINITE);
+
+        err = ngx_errno;
+        ngx_time_update();
+
+        ngx_log_debug1(NGX_LOG_DEBUG_CORE, log, 0,
+                       "worker exit WaitForMultipleObjects: %ul", ev);
+
+        if (ev == WAIT_OBJECT_0) {
+            break;
+        }
+
+        if (ev == WAIT_OBJECT_0 + 1) {
+            if (nev == 2) {
+                break;
+            }
+
+            events[1] = events[2];
+            nev = 2;
+            continue;
+        }
+
+        if (ev == WAIT_OBJECT_0 + 2) {
+            nev = 2;
+            continue;
+        }
+
+        if (ev == WAIT_FAILED) {
+            ngx_log_error(NGX_LOG_ALERT, log, err,
+                          "WaitForMultipleObjects() failed");
+            break;
+        }
+    }
+
+    ngx_close_handle(ngx_cache_manager_event);
+    ngx_close_handle(events[0]);
+    ngx_close_handle(events[1]);
+    ngx_close_handle(events[2]);
+    ngx_close_handle(mev);
+
+    ngx_worker_process_exit(cycle);
+
+failed:
+
+    exit(2);
+}
+
+
+static ngx_thread_value_t __stdcall
+ngx_worker_thread(void *data)
+{
+    ngx_int_t     n;
+    ngx_time_t   *tp;
+    ngx_cycle_t  *cycle;
+
+    tp = ngx_timeofday();
+    srand((ngx_pid << 16) ^ (unsigned) tp->sec ^ tp->msec);
+
+    cycle = (ngx_cycle_t *) ngx_cycle;
+
+    for (n = 0; cycle->modules[n]; n++) {
+        if (cycle->modules[n]->init_process) {
+            if (cycle->modules[n]->init_process(cycle) == NGX_ERROR) {
+                /* fatal */
+                exit(2);
+            }
+        }
+    }
+
+    while (!ngx_quit) {
+
+        if (ngx_exiting) {
+            if (ngx_event_no_timers_left() == NGX_OK) {
+                break;
+            }
+        }
+
+        ngx_log_debug0(NGX_LOG_DEBUG_CORE, cycle->log, 0, "worker cycle");
+
+        ngx_process_events_and_timers(cycle);
+
+        if (ngx_terminate) {
+            return 0;
+        }
+
+        if (ngx_quit) {
+            ngx_quit = 0;
+
+            if (!ngx_exiting) {
+                ngx_exiting = 1;
+                ngx_set_shutdown_timer(cycle);
+                ngx_close_listening_sockets(cycle);
+                ngx_close_idle_connections(cycle);
+                ngx_event_process_posted(cycle, &ngx_posted_events);
+            }
+        }
+
+        if (ngx_reopen) {
+            ngx_reopen = 0;
+            ngx_reopen_files(cycle, -1);
+        }
+    }
+
+    ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "exiting");
+
+    return 0;
+}
+
+
+static void
+ngx_worker_process_exit(ngx_cycle_t *cycle)
+{
+    ngx_uint_t         i;
+    ngx_connection_t  *c;
+
+    ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "exit");
+
+    for (i = 0; cycle->modules[i]; i++) {
+        if (cycle->modules[i]->exit_process) {
+            cycle->modules[i]->exit_process(cycle);
+        }
+    }
+
+    if (ngx_exiting && !ngx_terminate) {
+        c = cycle->connections;
+        for (i = 0; i < cycle->connection_n; i++) {
+            if (c[i].fd != (ngx_socket_t) -1
+                && c[i].read
+                && !c[i].read->accept
+                && !c[i].read->channel
+                && !c[i].read->resolver)
+            {
+                ngx_log_error(NGX_LOG_ALERT, cycle->log, 0,
+                              "*%uA open socket #%d left in connection %ui",
+                              c[i].number, c[i].fd, i);
+            }
+        }
+    }
+
+    ngx_destroy_pool(cycle->pool);
+
+    exit(0);
+}
+
+
+static ngx_thread_value_t __stdcall
+ngx_cache_manager_thread(void *data)
+{
+    u_long        ev;
+    HANDLE        events[2];
+    ngx_err_t     err;
+    ngx_cycle_t  *cycle;
+
+    cycle = (ngx_cycle_t *) ngx_cycle;
+
+    events[0] = ngx_cache_manager_event;
+    events[1] = ngx_cache_manager_mutex;
+
+    for ( ;; ) {
+        ev = WaitForMultipleObjects(2, events, 0, INFINITE);
+
+        err = ngx_errno;
+        ngx_time_update();
+
+        ngx_log_debug1(NGX_LOG_DEBUG_CORE, cycle->log, 0,
+                       "cache manager WaitForMultipleObjects: %ul", ev);
+
+        if (ev == WAIT_FAILED) {
+            ngx_log_error(NGX_LOG_ALERT, cycle->log, err,
+                          "WaitForMultipleObjects() failed");
+        }
+
+        /*
+         * ev == WAIT_OBJECT_0
+         * ev == WAIT_OBJECT_0 + 1
+         * ev == WAIT_ABANDONED_0 + 1
+         */
+
+        if (ngx_terminate || ngx_quit || ngx_exiting) {
+            ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "exiting");
+            return 0;
+        }
+
+        break;
+    }
+
+    for ( ;; ) {
+
+        if (ngx_terminate || ngx_quit || ngx_exiting) {
+            ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "exiting");
+            break;
+        }
+
+        ngx_cache_manager_process_handler();
+    }
+
+    if (ReleaseMutex(ngx_cache_manager_mutex) == 0) {
+        ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
+                      "ReleaseMutex() failed");
+    }
+
+    return 0;
+}
+
+
+static void
+ngx_cache_manager_process_handler(void)
+{
+    u_long        ev;
+    ngx_uint_t    i;
+    ngx_msec_t    next, n;
+    ngx_path_t  **path;
+
+    next = 60 * 60 * 1000;
+
+    path = ngx_cycle->paths.elts;
+    for (i = 0; i < ngx_cycle->paths.nelts; i++) {
+
+        if (path[i]->manager) {
+            n = path[i]->manager(path[i]->data);
+
+            next = (n <= next) ? n : next;
+
+            ngx_time_update();
+        }
+    }
+
+    if (next == 0) {
+        next = 1;
+    }
+
+    ev = WaitForSingleObject(ngx_cache_manager_event, (u_long) next);
+
+    if (ev != WAIT_TIMEOUT) {
+
+        ngx_time_update();
+
+        ngx_log_debug1(NGX_LOG_DEBUG_CORE, ngx_cycle->log, 0,
+                       "cache manager WaitForSingleObject: %ul", ev);
+    }
+}
+
+
+static ngx_thread_value_t __stdcall
+ngx_cache_loader_thread(void *data)
+{
+    ngx_uint_t     i;
+    ngx_path_t   **path;
+    ngx_cycle_t   *cycle;
+
+    ngx_msleep(60000);
+
+    cycle = (ngx_cycle_t *) ngx_cycle;
+
+    path = cycle->paths.elts;
+    for (i = 0; i < cycle->paths.nelts; i++) {
+
+        if (ngx_terminate || ngx_quit || ngx_exiting) {
+            break;
+        }
+
+        if (path[i]->loader) {
+            path[i]->loader(path[i]->data);
+            ngx_time_update();
+        }
+    }
+
+    return 0;
+}
+
+
+void
+ngx_single_process_cycle(ngx_cycle_t *cycle)
+{
+    ngx_tid_t  tid;
+
+    ngx_console_init(cycle);
+
+    if (ngx_create_signal_events(cycle) != NGX_OK) {
+        exit(2);
+    }
+
+    if (ngx_create_thread(&tid, ngx_worker_thread, NULL, cycle->log) != 0) {
+        /* fatal */
+        exit(2);
+    }
+
+    /* STUB */
+    WaitForSingleObject(ngx_stop_event, INFINITE);
+}
+
+
+ngx_int_t
+ngx_os_signal_process(ngx_cycle_t *cycle, char *sig, ngx_pid_t pid)
+{
+    HANDLE     ev;
+    ngx_int_t  rc;
+    char       evn[NGX_PROCESS_SYNC_NAME];
+
+    ngx_sprintf((u_char *) evn, "Global\\ngx_%s_%P%Z", sig, pid);
+
+    ev = OpenEvent(EVENT_MODIFY_STATE, 0, evn);
+    if (ev == NULL) {
+        ngx_log_error(NGX_LOG_ERR, cycle->log, ngx_errno,
+                      "OpenEvent(\"%s\") failed", evn);
+        return 1;
+    }
+
+    if (SetEvent(ev) == 0) {
+        ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
+                      "SetEvent(\"%s\") failed", evn);
+        rc = 1;
+
+    } else {
+        rc = 0;
+    }
+
+    ngx_close_handle(ev);
+
+    return rc;
+}
+
+
+void
+ngx_close_handle(HANDLE h)
+{
+    if (CloseHandle(h) == 0) {
+        ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, ngx_errno,
+                      "CloseHandle(%p) failed", h);
+    }
+}
diff --git a/src/os/win32/ngx_process_cycle.h b/src/os/win32/ngx_process_cycle.h
new file mode 100644
index 0000000..95d2743
--- /dev/null
+++ b/src/os/win32/ngx_process_cycle.h
@@ -0,0 +1,44 @@
+
+/*
+ * Copyright (C) Igor Sysoev
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_PROCESS_CYCLE_H_INCLUDED_
+#define _NGX_PROCESS_CYCLE_H_INCLUDED_
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+
+#define NGX_PROCESS_SINGLE     0
+#define NGX_PROCESS_MASTER     1
+#define NGX_PROCESS_SIGNALLER  2
+#define NGX_PROCESS_WORKER     3
+
+
+void ngx_master_process_cycle(ngx_cycle_t *cycle);
+void ngx_single_process_cycle(ngx_cycle_t *cycle);
+void ngx_close_handle(HANDLE h);
+
+
+extern ngx_uint_t      ngx_process;
+extern ngx_uint_t      ngx_worker;
+extern ngx_pid_t       ngx_pid;
+extern ngx_uint_t      ngx_exiting;
+
+extern sig_atomic_t    ngx_quit;
+extern sig_atomic_t    ngx_terminate;
+extern sig_atomic_t    ngx_reopen;
+
+extern ngx_uint_t      ngx_inherited;
+extern ngx_pid_t       ngx_new_binary;
+
+
+extern HANDLE          ngx_master_process_event;
+extern char            ngx_master_process_event_name[];
+
+
+#endif /* _NGX_PROCESS_CYCLE_H_INCLUDED_ */
diff --git a/src/os/win32/ngx_service.c b/src/os/win32/ngx_service.c
new file mode 100644
index 0000000..835d9cf
--- /dev/null
+++ b/src/os/win32/ngx_service.c
@@ -0,0 +1,134 @@
+
+/*
+ * Copyright (C) Igor Sysoev
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+
+#define NGX_SERVICE_CONTROL_SHUTDOWN   128
+#define NGX_SERVICE_CONTROL_REOPEN     129
+
+
+SERVICE_TABLE_ENTRY st[] = {
+    { "nginx", service_main },
+    { NULL, NULL }
+};
+
+
+ngx_int_t
+ngx_service(ngx_log_t *log)
+{
+    /* primary thread */
+
+    /* StartServiceCtrlDispatcher() should be called within 30 seconds */
+
+    if (StartServiceCtrlDispatcher(st) == 0) {
+        ngx_log_error(NGX_LOG_EMERG, log, ngx_errno,
+                      "StartServiceCtrlDispatcher() failed");
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}
+
+
+void
+service_main(u_int argc, char **argv)
+{
+    SERVICE_STATUS         status;
+    SERVICE_STATUS_HANDLE  service;
+
+    /* thread spawned by SCM */
+
+    service = RegisterServiceCtrlHandlerEx("nginx", service_handler, ctx);
+    if (service == INVALID_HANDLE_VALUE) {
+        ngx_log_error(NGX_LOG_EMERG, log, ngx_errno,
+                      "RegisterServiceCtrlHandlerEx() failed");
+        return;
+    }
+
+    status.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
+    status.dwCurrentState = SERVICE_START_PENDING;
+    status.dwControlsAccepted = SERVICE_ACCEPT_STOP
+                                |SERVICE_ACCEPT_PARAMCHANGE;
+    status.dwWin32ExitCode = NO_ERROR;
+    status.dwServiceSpecificExitCode = 0;
+    status.dwCheckPoint = 1;
+    status.dwWaitHint = 2000;
+
+    /* SetServiceStatus() should be called within 80 seconds */
+
+    if (SetServiceStatus(service, &status) == 0) {
+        ngx_log_error(NGX_LOG_EMERG, log, ngx_errno,
+                      "SetServiceStatus() failed");
+        return;
+    }
+
+    /* init */
+
+    status.dwCurrentState = SERVICE_RUNNING;
+    status.dwCheckPoint = 0;
+    status.dwWaitHint = 0;
+
+    if (SetServiceStatus(service, &status) == 0) {
+        ngx_log_error(NGX_LOG_EMERG, log, ngx_errno,
+                      "SetServiceStatus() failed");
+        return;
+    }
+
+    /* call master or worker loop */
+
+    /*
+     * master should use event notification and look status
+     * single should use iocp to get notifications from service handler
+     */
+
+}
+
+
+u_int
+service_handler(u_int control, u_int type, void *data, void *ctx)
+{
+    /* primary thread */
+
+    switch (control) {
+
+    case SERVICE_CONTROL_INTERROGATE:
+        status = NGX_IOCP_INTERROGATE;
+        break;
+
+    case SERVICE_CONTROL_STOP:
+        status = NGX_IOCP_STOP;
+        break;
+
+    case SERVICE_CONTROL_PARAMCHANGE:
+        status = NGX_IOCP_RECONFIGURE;
+        break;
+
+    case NGX_SERVICE_CONTROL_SHUTDOWN:
+        status = NGX_IOCP_REOPEN;
+        break;
+
+    case NGX_SERVICE_CONTROL_REOPEN:
+        status = NGX_IOCP_REOPEN;
+        break;
+
+    default:
+        return ERROR_CALL_NOT_IMPLEMENTED;
+    }
+
+    if (ngx_single) {
+        if (PostQueuedCompletionStatus(iocp, ... status, ...) == 0) {
+            err = ngx_errno;
+            ngx_log_error(NGX_LOG_ALERT, log, err,
+                          "PostQueuedCompletionStatus() failed");
+            return err;
+        }
+
+    } else {
+        Event
+    }
+
+    return NO_ERROR;
+}
diff --git a/src/os/win32/ngx_shmem.c b/src/os/win32/ngx_shmem.c
new file mode 100644
index 0000000..c3ed699
--- /dev/null
+++ b/src/os/win32/ngx_shmem.c
@@ -0,0 +1,161 @@
+
+/*
+ * Copyright (C) Igor Sysoev
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+
+/*
+ * Base addresses selected by system for shared memory mappings are likely
+ * to be different on Windows Vista and later versions due to address space
+ * layout randomization.  This is however incompatible with storing absolute
+ * addresses within the shared memory.
+ *
+ * To make it possible to store absolute addresses we create mappings
+ * at the same address in all processes by starting mappings at predefined
+ * addresses.  The addresses were selected somewhat randomly in order to
+ * minimize the probability that some other library doing something similar
+ * conflicts with us.  The addresses are from the following typically free
+ * blocks:
+ *
+ * - 0x10000000 .. 0x70000000 (about 1.5 GB in total) on 32-bit platforms
+ * - 0x000000007fff0000 .. 0x000007f68e8b0000 (about 8 TB) on 64-bit platforms
+ *
+ * Additionally, we allow to change the mapping address once it was detected
+ * to be different from one originally used.  This is needed to support
+ * reconfiguration.
+ */
+
+
+#ifdef _WIN64
+#define NGX_SHMEM_BASE  0x0000047047e00000
+#else
+#define NGX_SHMEM_BASE  0x2efe0000
+#endif
+
+
+ngx_uint_t  ngx_allocation_granularity;
+
+
+ngx_int_t
+ngx_shm_alloc(ngx_shm_t *shm)
+{
+    u_char         *name;
+    uint64_t        size;
+    static u_char  *base = (u_char *) NGX_SHMEM_BASE;
+
+    name = ngx_alloc(shm->name.len + 2 + NGX_INT32_LEN, shm->log);
+    if (name == NULL) {
+        return NGX_ERROR;
+    }
+
+    (void) ngx_sprintf(name, "%V_%s%Z", &shm->name, ngx_unique);
+
+    ngx_set_errno(0);
+
+    size = shm->size;
+
+    shm->handle = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE,
+                                    (u_long) (size >> 32),
+                                    (u_long) (size & 0xffffffff),
+                                    (char *) name);
+
+    if (shm->handle == NULL) {
+        ngx_log_error(NGX_LOG_ALERT, shm->log, ngx_errno,
+                      "CreateFileMapping(%uz, %s) failed",
+                      shm->size, name);
+        ngx_free(name);
+
+        return NGX_ERROR;
+    }
+
+    ngx_free(name);
+
+    if (ngx_errno == ERROR_ALREADY_EXISTS) {
+        shm->exists = 1;
+    }
+
+    shm->addr = MapViewOfFileEx(shm->handle, FILE_MAP_WRITE, 0, 0, 0, base);
+
+    if (shm->addr != NULL) {
+        base += ngx_align(size, ngx_allocation_granularity);
+        return NGX_OK;
+    }
+
+    ngx_log_debug3(NGX_LOG_DEBUG_CORE, shm->log, ngx_errno,
+                   "MapViewOfFileEx(%uz, %p) of file mapping \"%V\" failed, "
+                   "retry without a base address",
+                   shm->size, base, &shm->name);
+
+    /*
+     * Order of shared memory zones may be different in the master process
+     * and worker processes after reconfiguration.  As a result, the above
+     * may fail due to a conflict with a previously created mapping remapped
+     * to a different address.  Additionally, there may be a conflict with
+     * some other uses of the memory.  In this case we retry without a base
+     * address to let the system assign the address itself.
+     */
+
+    shm->addr = MapViewOfFile(shm->handle, FILE_MAP_WRITE, 0, 0, 0);
+
+    if (shm->addr != NULL) {
+        return NGX_OK;
+    }
+
+    ngx_log_error(NGX_LOG_ALERT, shm->log, ngx_errno,
+                  "MapViewOfFile(%uz) of file mapping \"%V\" failed",
+                  shm->size, &shm->name);
+
+    if (CloseHandle(shm->handle) == 0) {
+        ngx_log_error(NGX_LOG_ALERT, shm->log, ngx_errno,
+                      "CloseHandle() of file mapping \"%V\" failed",
+                      &shm->name);
+    }
+
+    return NGX_ERROR;
+}
+
+
+ngx_int_t
+ngx_shm_remap(ngx_shm_t *shm, u_char *addr)
+{
+    if (UnmapViewOfFile(shm->addr) == 0) {
+        ngx_log_error(NGX_LOG_ALERT, shm->log, ngx_errno,
+                      "UnmapViewOfFile(%p) of file mapping \"%V\" failed",
+                      shm->addr, &shm->name);
+        return NGX_ERROR;
+    }
+
+    shm->addr = MapViewOfFileEx(shm->handle, FILE_MAP_WRITE, 0, 0, 0, addr);
+
+    if (shm->addr != NULL) {
+        return NGX_OK;
+    }
+
+    ngx_log_error(NGX_LOG_ALERT, shm->log, ngx_errno,
+                  "MapViewOfFileEx(%uz, %p) of file mapping \"%V\" failed",
+                  shm->size, addr, &shm->name);
+
+    return NGX_ERROR;
+}
+
+
+void
+ngx_shm_free(ngx_shm_t *shm)
+{
+    if (UnmapViewOfFile(shm->addr) == 0) {
+        ngx_log_error(NGX_LOG_ALERT, shm->log, ngx_errno,
+                      "UnmapViewOfFile(%p) of file mapping \"%V\" failed",
+                      shm->addr, &shm->name);
+    }
+
+    if (CloseHandle(shm->handle) == 0) {
+        ngx_log_error(NGX_LOG_ALERT, shm->log, ngx_errno,
+                      "CloseHandle() of file mapping \"%V\" failed",
+                      &shm->name);
+    }
+}
diff --git a/src/os/win32/ngx_shmem.h b/src/os/win32/ngx_shmem.h
new file mode 100644
index 0000000..ee47429
--- /dev/null
+++ b/src/os/win32/ngx_shmem.h
@@ -0,0 +1,33 @@
+
+/*
+ * Copyright (C) Igor Sysoev
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_SHMEM_H_INCLUDED_
+#define _NGX_SHMEM_H_INCLUDED_
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+
+typedef struct {
+    u_char      *addr;
+    size_t       size;
+    ngx_str_t    name;
+    HANDLE       handle;
+    ngx_log_t   *log;
+    ngx_uint_t   exists;   /* unsigned  exists:1;  */
+} ngx_shm_t;
+
+
+ngx_int_t ngx_shm_alloc(ngx_shm_t *shm);
+ngx_int_t ngx_shm_remap(ngx_shm_t *shm, u_char *addr);
+void ngx_shm_free(ngx_shm_t *shm);
+
+extern ngx_uint_t  ngx_allocation_granularity;
+
+
+#endif /* _NGX_SHMEM_H_INCLUDED_ */
diff --git a/src/os/win32/ngx_socket.c b/src/os/win32/ngx_socket.c
new file mode 100644
index 0000000..b1b4afb
--- /dev/null
+++ b/src/os/win32/ngx_socket.c
@@ -0,0 +1,49 @@
+
+/*
+ * Copyright (C) Igor Sysoev
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+
+int
+ngx_nonblocking(ngx_socket_t s)
+{
+    unsigned long  nb = 1;
+
+    return ioctlsocket(s, FIONBIO, &nb);
+}
+
+
+int
+ngx_blocking(ngx_socket_t s)
+{
+    unsigned long  nb = 0;
+
+    return ioctlsocket(s, FIONBIO, &nb);
+}
+
+
+int
+ngx_socket_nread(ngx_socket_t s, int *n)
+{
+    unsigned long  nread;
+
+    if (ioctlsocket(s, FIONREAD, &nread) == -1) {
+        return -1;
+    }
+
+    *n = nread;
+
+    return 0;
+}
+
+
+int
+ngx_tcp_push(ngx_socket_t s)
+{
+    return 0;
+}
diff --git a/src/os/win32/ngx_socket.h b/src/os/win32/ngx_socket.h
new file mode 100644
index 0000000..5b61389
--- /dev/null
+++ b/src/os/win32/ngx_socket.h
@@ -0,0 +1,255 @@
+
+/*
+ * Copyright (C) Igor Sysoev
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_SOCKET_H_INCLUDED_
+#define _NGX_SOCKET_H_INCLUDED_
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+
+#define NGX_WRITE_SHUTDOWN SD_SEND
+#define NGX_READ_SHUTDOWN  SD_RECEIVE
+#define NGX_RDWR_SHUTDOWN  SD_BOTH
+
+
+typedef SOCKET  ngx_socket_t;
+typedef int     socklen_t;
+
+
+#define ngx_socket(af, type, proto)                                          \
+    WSASocketW(af, type, proto, NULL, 0, WSA_FLAG_OVERLAPPED)
+
+#define ngx_socket_n        "WSASocketW()"
+
+int ngx_nonblocking(ngx_socket_t s);
+int ngx_blocking(ngx_socket_t s);
+
+#define ngx_nonblocking_n   "ioctlsocket(FIONBIO)"
+#define ngx_blocking_n      "ioctlsocket(!FIONBIO)"
+
+int ngx_socket_nread(ngx_socket_t s, int *n);
+#define ngx_socket_nread_n  "ioctlsocket(FIONREAD)"
+
+#define ngx_shutdown_socket    shutdown
+#define ngx_shutdown_socket_n  "shutdown()"
+
+#define ngx_close_socket    closesocket
+#define ngx_close_socket_n  "closesocket()"
+
+
+#ifndef WSAID_ACCEPTEX
+
+typedef BOOL (PASCAL FAR * LPFN_ACCEPTEX)(
+    IN SOCKET sListenSocket,
+    IN SOCKET sAcceptSocket,
+    IN PVOID lpOutputBuffer,
+    IN DWORD dwReceiveDataLength,
+    IN DWORD dwLocalAddressLength,
+    IN DWORD dwRemoteAddressLength,
+    OUT LPDWORD lpdwBytesReceived,
+    IN LPOVERLAPPED lpOverlapped
+    );
+
+#define WSAID_ACCEPTEX                                                       \
+    {0xb5367df1,0xcbac,0x11cf,{0x95,0xca,0x00,0x80,0x5f,0x48,0xa1,0x92}}
+
+#endif
+
+
+#ifndef WSAID_GETACCEPTEXSOCKADDRS
+
+typedef VOID (PASCAL FAR * LPFN_GETACCEPTEXSOCKADDRS)(
+    IN PVOID lpOutputBuffer,
+    IN DWORD dwReceiveDataLength,
+    IN DWORD dwLocalAddressLength,
+    IN DWORD dwRemoteAddressLength,
+    OUT struct sockaddr **LocalSockaddr,
+    OUT LPINT LocalSockaddrLength,
+    OUT struct sockaddr **RemoteSockaddr,
+    OUT LPINT RemoteSockaddrLength
+    );
+
+#define WSAID_GETACCEPTEXSOCKADDRS                                           \
+        {0xb5367df2,0xcbac,0x11cf,{0x95,0xca,0x00,0x80,0x5f,0x48,0xa1,0x92}}
+
+#endif
+
+
+#ifndef WSAID_TRANSMITFILE
+
+#ifndef TF_DISCONNECT
+
+#define TF_DISCONNECT           1
+#define TF_REUSE_SOCKET         2
+#define TF_WRITE_BEHIND         4
+#define TF_USE_DEFAULT_WORKER   0
+#define TF_USE_SYSTEM_THREAD    16
+#define TF_USE_KERNEL_APC       32
+
+typedef struct _TRANSMIT_FILE_BUFFERS {
+    LPVOID Head;
+    DWORD HeadLength;
+    LPVOID Tail;
+    DWORD TailLength;
+} TRANSMIT_FILE_BUFFERS, *PTRANSMIT_FILE_BUFFERS, FAR *LPTRANSMIT_FILE_BUFFERS;
+
+#endif
+
+typedef BOOL (PASCAL FAR * LPFN_TRANSMITFILE)(
+    IN SOCKET hSocket,
+    IN HANDLE hFile,
+    IN DWORD nNumberOfBytesToWrite,
+    IN DWORD nNumberOfBytesPerSend,
+    IN LPOVERLAPPED lpOverlapped,
+    IN LPTRANSMIT_FILE_BUFFERS lpTransmitBuffers,
+    IN DWORD dwReserved
+    );
+
+#define WSAID_TRANSMITFILE                                                   \
+    {0xb5367df0,0xcbac,0x11cf,{0x95,0xca,0x00,0x80,0x5f,0x48,0xa1,0x92}}
+
+#endif
+
+
+#ifndef WSAID_TRANSMITPACKETS
+
+/* OpenWatcom has a swapped TP_ELEMENT_FILE and TP_ELEMENT_MEMORY definition */
+
+#ifndef TP_ELEMENT_FILE
+
+#ifdef _MSC_VER
+#pragma warning(disable:4201) /* Nonstandard extension, nameless struct/union */
+#endif
+
+typedef struct _TRANSMIT_PACKETS_ELEMENT {
+    ULONG dwElFlags;
+#define TP_ELEMENT_MEMORY   1
+#define TP_ELEMENT_FILE     2
+#define TP_ELEMENT_EOP      4
+    ULONG cLength;
+    union {
+        struct {
+            LARGE_INTEGER nFileOffset;
+            HANDLE        hFile;
+        };
+        PVOID             pBuffer;
+    };
+} TRANSMIT_PACKETS_ELEMENT, *PTRANSMIT_PACKETS_ELEMENT,
+    FAR *LPTRANSMIT_PACKETS_ELEMENT;
+
+#ifdef _MSC_VER
+#pragma warning(default:4201)
+#endif
+
+#endif
+
+typedef BOOL (PASCAL FAR * LPFN_TRANSMITPACKETS) (
+    SOCKET hSocket,
+    TRANSMIT_PACKETS_ELEMENT *lpPacketArray,
+    DWORD nElementCount,
+    DWORD nSendSize,
+    LPOVERLAPPED lpOverlapped,
+    DWORD dwFlags
+    );
+
+#define WSAID_TRANSMITPACKETS                                                \
+    {0xd9689da0,0x1f90,0x11d3,{0x99,0x71,0x00,0xc0,0x4f,0x68,0xc8,0x76}}
+
+#endif
+
+
+#ifndef WSAID_CONNECTEX
+
+typedef BOOL (PASCAL FAR * LPFN_CONNECTEX) (
+    IN SOCKET s,
+    IN const struct sockaddr FAR *name,
+    IN int namelen,
+    IN PVOID lpSendBuffer OPTIONAL,
+    IN DWORD dwSendDataLength,
+    OUT LPDWORD lpdwBytesSent,
+    IN LPOVERLAPPED lpOverlapped
+    );
+
+#define WSAID_CONNECTEX \
+    {0x25a207b9,0xddf3,0x4660,{0x8e,0xe9,0x76,0xe5,0x8c,0x74,0x06,0x3e}}
+
+#endif
+
+
+#ifndef WSAID_DISCONNECTEX
+
+typedef BOOL (PASCAL FAR * LPFN_DISCONNECTEX) (
+    IN SOCKET s,
+    IN LPOVERLAPPED lpOverlapped,
+    IN DWORD  dwFlags,
+    IN DWORD  dwReserved
+    );
+
+#define WSAID_DISCONNECTEX                                                   \
+    {0x7fda2e11,0x8630,0x436f,{0xa0,0x31,0xf5,0x36,0xa6,0xee,0xc1,0x57}}
+
+#endif
+
+
+extern LPFN_ACCEPTEX              ngx_acceptex;
+extern LPFN_GETACCEPTEXSOCKADDRS  ngx_getacceptexsockaddrs;
+extern LPFN_TRANSMITFILE          ngx_transmitfile;
+extern LPFN_TRANSMITPACKETS       ngx_transmitpackets;
+extern LPFN_CONNECTEX             ngx_connectex;
+extern LPFN_DISCONNECTEX          ngx_disconnectex;
+
+
+#if (NGX_HAVE_POLL && !defined POLLIN)
+
+/*
+ * WSAPoll() is only available if _WIN32_WINNT >= 0x0600.
+ * If it is not available during compilation, we try to
+ * load it dynamically at runtime.
+ */
+
+#define NGX_LOAD_WSAPOLL 1
+
+#define POLLRDNORM  0x0100
+#define POLLRDBAND  0x0200
+#define POLLIN      (POLLRDNORM | POLLRDBAND)
+#define POLLPRI     0x0400
+
+#define POLLWRNORM  0x0010
+#define POLLOUT     (POLLWRNORM)
+#define POLLWRBAND  0x0020
+
+#define POLLERR     0x0001
+#define POLLHUP     0x0002
+#define POLLNVAL    0x0004
+
+typedef struct pollfd {
+
+    SOCKET  fd;
+    SHORT   events;
+    SHORT   revents;
+
+} WSAPOLLFD, *PWSAPOLLFD, FAR *LPWSAPOLLFD;
+
+typedef int (WSAAPI *ngx_wsapoll_pt)(
+    LPWSAPOLLFD fdArray,
+    ULONG fds,
+    INT timeout
+    );
+
+extern ngx_wsapoll_pt             WSAPoll;
+extern ngx_uint_t                 ngx_have_wsapoll;
+
+#endif
+
+
+int ngx_tcp_push(ngx_socket_t s);
+#define ngx_tcp_push_n            "tcp_push()"
+
+
+#endif /* _NGX_SOCKET_H_INCLUDED_ */
diff --git a/src/os/win32/ngx_stat.c b/src/os/win32/ngx_stat.c
new file mode 100644
index 0000000..51bcd96
--- /dev/null
+++ b/src/os/win32/ngx_stat.c
@@ -0,0 +1,34 @@
+
+/*
+ * Copyright (C) Igor Sysoev
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+
+int ngx_file_type(char *file, ngx_file_info_t *sb)
+{
+    sb->dwFileAttributes = GetFileAttributes(file);
+
+    if (sb->dwFileAttributes == INVALID_FILE_ATTRIBUTES) {
+        return -1;
+    }
+
+    return 0;
+}
+
+/*
+int ngx_stat(char *file, ngx_stat_t *sb)
+{
+    *sb = GetFileAttributes(file);
+
+    if (*sb == INVALID_FILE_ATTRIBUTES) {
+        return -1;
+    }
+
+    return 0;
+}
+*/
diff --git a/src/os/win32/ngx_thread.c b/src/os/win32/ngx_thread.c
new file mode 100644
index 0000000..a13de2d
--- /dev/null
+++ b/src/os/win32/ngx_thread.c
@@ -0,0 +1,30 @@
+
+/*
+ * Copyright (C) Igor Sysoev
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+
+ngx_err_t
+ngx_create_thread(ngx_tid_t *tid,
+    ngx_thread_value_t (__stdcall *func)(void *arg), void *arg, ngx_log_t *log)
+{
+    u_long     id;
+    ngx_err_t  err;
+
+    *tid = CreateThread(NULL, 0, func, arg, 0, &id);
+
+    if (*tid != NULL) {
+        ngx_log_error(NGX_LOG_NOTICE, log, 0,
+                      "create thread " NGX_TID_T_FMT, id);
+        return 0;
+    }
+
+    err = ngx_errno;
+    ngx_log_error(NGX_LOG_ALERT, log, err, "CreateThread() failed");
+    return err;
+}
diff --git a/src/os/win32/ngx_thread.h b/src/os/win32/ngx_thread.h
new file mode 100644
index 0000000..4012276
--- /dev/null
+++ b/src/os/win32/ngx_thread.h
@@ -0,0 +1,27 @@
+
+/*
+ * Copyright (C) Igor Sysoev
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_THREAD_H_INCLUDED_
+#define _NGX_THREAD_H_INCLUDED_
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+
+typedef HANDLE  ngx_tid_t;
+typedef DWORD   ngx_thread_value_t;
+
+
+ngx_err_t ngx_create_thread(ngx_tid_t *tid,
+    ngx_thread_value_t (__stdcall *func)(void *arg), void *arg, ngx_log_t *log);
+
+#define ngx_log_tid                 GetCurrentThreadId()
+#define NGX_TID_T_FMT               "%ud"
+
+
+#endif /* _NGX_THREAD_H_INCLUDED_ */
diff --git a/src/os/win32/ngx_time.c b/src/os/win32/ngx_time.c
new file mode 100644
index 0000000..79149b2
--- /dev/null
+++ b/src/os/win32/ngx_time.c
@@ -0,0 +1,83 @@
+
+/*
+ * Copyright (C) Igor Sysoev
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+
+void
+ngx_gettimeofday(struct timeval *tp)
+{
+    uint64_t  intervals;
+    FILETIME  ft;
+
+    GetSystemTimeAsFileTime(&ft);
+
+    /*
+     * A file time is a 64-bit value that represents the number
+     * of 100-nanosecond intervals that have elapsed since
+     * January 1, 1601 12:00 A.M. UTC.
+     *
+     * Between January 1, 1970 (Epoch) and January 1, 1601 there were
+     * 134774 days,
+     * 11644473600 seconds or
+     * 11644473600,000,000,0 100-nanosecond intervals.
+     *
+     * See also MSKB Q167296.
+     */
+
+    intervals = ((uint64_t) ft.dwHighDateTime << 32) | ft.dwLowDateTime;
+    intervals -= 116444736000000000;
+
+    tp->tv_sec = (long) (intervals / 10000000);
+    tp->tv_usec = (long) ((intervals % 10000000) / 10);
+}
+
+
+void
+ngx_libc_localtime(time_t s, struct tm *tm)
+{
+    struct tm  *t;
+
+    t = localtime(&s);
+    *tm = *t;
+}
+
+
+void
+ngx_libc_gmtime(time_t s, struct tm *tm)
+{
+    struct tm  *t;
+
+    t = gmtime(&s);
+    *tm = *t;
+}
+
+
+ngx_int_t
+ngx_gettimezone(void)
+{
+    u_long                 n;
+    TIME_ZONE_INFORMATION  tz;
+
+    n = GetTimeZoneInformation(&tz);
+
+    switch (n) {
+
+    case TIME_ZONE_ID_UNKNOWN:
+        return -tz.Bias;
+
+    case TIME_ZONE_ID_STANDARD:
+        return -(tz.Bias + tz.StandardBias);
+
+    case TIME_ZONE_ID_DAYLIGHT:
+        return -(tz.Bias + tz.DaylightBias);
+
+    default: /* TIME_ZONE_ID_INVALID */
+        return 0;
+    }
+}
diff --git a/src/os/win32/ngx_time.h b/src/os/win32/ngx_time.h
new file mode 100644
index 0000000..6c2f806
--- /dev/null
+++ b/src/os/win32/ngx_time.h
@@ -0,0 +1,51 @@
+
+/*
+ * Copyright (C) Igor Sysoev
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_TIME_H_INCLUDED_
+#define _NGX_TIME_H_INCLUDED_
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+
+typedef ngx_rbtree_key_t      ngx_msec_t;
+typedef ngx_rbtree_key_int_t  ngx_msec_int_t;
+
+typedef SYSTEMTIME            ngx_tm_t;
+typedef FILETIME              ngx_mtime_t;
+
+#define ngx_tm_sec            wSecond
+#define ngx_tm_min            wMinute
+#define ngx_tm_hour           wHour
+#define ngx_tm_mday           wDay
+#define ngx_tm_mon            wMonth
+#define ngx_tm_year           wYear
+#define ngx_tm_wday           wDayOfWeek
+
+#define ngx_tm_sec_t          u_short
+#define ngx_tm_min_t          u_short
+#define ngx_tm_hour_t         u_short
+#define ngx_tm_mday_t         u_short
+#define ngx_tm_mon_t          u_short
+#define ngx_tm_year_t         u_short
+#define ngx_tm_wday_t         u_short
+
+
+#define ngx_msleep            Sleep
+
+#define NGX_HAVE_GETTIMEZONE  1
+
+#define  ngx_timezone_update()
+
+ngx_int_t ngx_gettimezone(void);
+void ngx_libc_localtime(time_t s, struct tm *tm);
+void ngx_libc_gmtime(time_t s, struct tm *tm);
+void ngx_gettimeofday(struct timeval *tp);
+
+
+#endif /* _NGX_TIME_H_INCLUDED_ */
diff --git a/src/os/win32/ngx_udp_wsarecv.c b/src/os/win32/ngx_udp_wsarecv.c
new file mode 100644
index 0000000..5424375
--- /dev/null
+++ b/src/os/win32/ngx_udp_wsarecv.c
@@ -0,0 +1,149 @@
+
+/*
+ * Copyright (C) Igor Sysoev
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_event.h>
+
+
+ssize_t
+ngx_udp_wsarecv(ngx_connection_t *c, u_char *buf, size_t size)
+{
+    int           rc;
+    u_long        bytes, flags;
+    WSABUF        wsabuf[1];
+    ngx_err_t     err;
+    ngx_event_t  *rev;
+
+    wsabuf[0].buf = (char *) buf;
+    wsabuf[0].len = size;
+    flags = 0;
+    bytes = 0;
+
+    rc = WSARecv(c->fd, wsabuf, 1, &bytes, &flags, NULL, NULL);
+
+    ngx_log_debug4(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "WSARecv: fd:%d rc:%d %ul of %z", c->fd, rc, bytes, size);
+
+    rev = c->read;
+
+    if (rc == -1) {
+        rev->ready = 0;
+        err = ngx_socket_errno;
+
+        if (err == WSAEWOULDBLOCK) {
+            ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, err,
+                           "WSARecv() not ready");
+            return NGX_AGAIN;
+        }
+
+        rev->error = 1;
+        ngx_connection_error(c, err, "WSARecv() failed");
+
+        return NGX_ERROR;
+    }
+
+    return bytes;
+}
+
+
+ssize_t
+ngx_udp_overlapped_wsarecv(ngx_connection_t *c, u_char *buf, size_t size)
+{
+    int               rc;
+    u_long            bytes, flags;
+    WSABUF            wsabuf[1];
+    ngx_err_t         err;
+    ngx_event_t      *rev;
+    LPWSAOVERLAPPED   ovlp;
+
+    rev = c->read;
+
+    if (!rev->ready) {
+        ngx_log_error(NGX_LOG_ALERT, c->log, 0, "second wsa post");
+        return NGX_AGAIN;
+    }
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "rev->complete: %d", rev->complete);
+
+    if (rev->complete) {
+        rev->complete = 0;
+
+        if (ngx_event_flags & NGX_USE_IOCP_EVENT) {
+            if (rev->ovlp.error) {
+                ngx_connection_error(c, rev->ovlp.error, "WSARecv() failed");
+                return NGX_ERROR;
+            }
+
+            ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                           "WSARecv ovlp: fd:%d %ul of %z",
+                           c->fd, rev->available, size);
+
+            return rev->available;
+        }
+
+        if (WSAGetOverlappedResult(c->fd, (LPWSAOVERLAPPED) &rev->ovlp,
+                                   &bytes, 0, NULL)
+            == 0)
+        {
+            ngx_connection_error(c, ngx_socket_errno,
+                               "WSARecv() or WSAGetOverlappedResult() failed");
+            return NGX_ERROR;
+        }
+
+        ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "WSARecv: fd:%d %ul of %z", c->fd, bytes, size);
+
+        return bytes;
+    }
+
+    ovlp = (LPWSAOVERLAPPED) &rev->ovlp;
+    ngx_memzero(ovlp, sizeof(WSAOVERLAPPED));
+    wsabuf[0].buf = (char *) buf;
+    wsabuf[0].len = size;
+    flags = 0;
+    bytes = 0;
+
+    rc = WSARecv(c->fd, wsabuf, 1, &bytes, &flags, ovlp, NULL);
+
+    rev->complete = 0;
+
+    ngx_log_debug4(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "WSARecv ovlp: fd:%d rc:%d %ul of %z",
+                   c->fd, rc, bytes, size);
+
+    if (rc == -1) {
+        err = ngx_socket_errno;
+        if (err == WSA_IO_PENDING) {
+            rev->active = 1;
+            ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, err,
+                           "WSARecv() posted");
+            return NGX_AGAIN;
+        }
+
+        rev->error = 1;
+        ngx_connection_error(c, err, "WSARecv() failed");
+        return NGX_ERROR;
+    }
+
+    if (ngx_event_flags & NGX_USE_IOCP_EVENT) {
+
+        /*
+         * if a socket was bound with I/O completion port
+         * then GetQueuedCompletionStatus() would anyway return its status
+         * despite that WSARecv() was already complete
+         */
+
+        rev->active = 1;
+        return NGX_AGAIN;
+    }
+
+    rev->active = 0;
+
+    return bytes;
+}
diff --git a/src/os/win32/ngx_user.c b/src/os/win32/ngx_user.c
new file mode 100644
index 0000000..ea6da5a
--- /dev/null
+++ b/src/os/win32/ngx_user.c
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) Igor Sysoev
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+
+#if (NGX_CRYPT)
+
+ngx_int_t
+ngx_libc_crypt(ngx_pool_t *pool, u_char *key, u_char *salt, u_char **encrypted)
+{
+    /* STUB: a plain text password */
+
+    *encrypted = key;
+
+    return NGX_OK;
+}
+
+#endif /* NGX_CRYPT */
diff --git a/src/os/win32/ngx_user.h b/src/os/win32/ngx_user.h
new file mode 100644
index 0000000..61408e4
--- /dev/null
+++ b/src/os/win32/ngx_user.h
@@ -0,0 +1,25 @@
+
+/*
+ * Copyright (C) Igor Sysoev
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_USER_H_INCLUDED_
+#define _NGX_USER_H_INCLUDED_
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+
+/* STUB */
+#define ngx_uid_t  ngx_int_t
+#define ngx_gid_t  ngx_int_t
+
+
+ngx_int_t ngx_libc_crypt(ngx_pool_t *pool, u_char *key, u_char *salt,
+    u_char **encrypted);
+
+
+#endif /* _NGX_USER_H_INCLUDED_ */
diff --git a/src/os/win32/ngx_win32_config.h b/src/os/win32/ngx_win32_config.h
new file mode 100644
index 0000000..7045613
--- /dev/null
+++ b/src/os/win32/ngx_win32_config.h
@@ -0,0 +1,291 @@
+
+/*
+ * Copyright (C) Igor Sysoev
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#ifndef _NGX_WIN32_CONFIG_H_INCLUDED_
+#define _NGX_WIN32_CONFIG_H_INCLUDED_
+
+
+#undef  WIN32
+#define WIN32         0x0400
+#define _WIN32_WINNT  0x0501
+
+
+#define STRICT
+#define WIN32_LEAN_AND_MEAN
+
+/* enable getenv() and gmtime() in msvc8 */
+#define _CRT_SECURE_NO_WARNINGS
+#define _CRT_SECURE_NO_DEPRECATE
+
+/* enable gethostbyname() in msvc2015 */
+#if !(NGX_HAVE_INET6)
+#define _WINSOCK_DEPRECATED_NO_WARNINGS
+#endif
+
+/*
+ * we need to include <windows.h> explicitly before <winsock2.h> because
+ * the warning 4201 is enabled in <windows.h>
+ */
+#include <windows.h>
+
+#ifdef _MSC_VER
+#pragma warning(disable:4201)
+#endif
+
+#include <winsock2.h>
+#include <ws2tcpip.h>  /* ipv6 */
+#include <mswsock.h>
+#include <shellapi.h>
+#include <stddef.h>    /* offsetof() */
+
+#ifdef __MINGW64_VERSION_MAJOR
+
+/* GCC MinGW-w64 supports _FILE_OFFSET_BITS */
+#define _FILE_OFFSET_BITS 64
+
+#elif defined __GNUC__
+
+/* GCC MinGW's stdio.h includes sys/types.h */
+#define _OFF_T_
+#define __have_typedef_off_t
+
+#endif
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#ifdef __GNUC__
+#include <stdint.h>
+#endif
+#include <ctype.h>
+#include <locale.h>
+
+#ifdef __WATCOMC__
+#define _TIME_T_DEFINED
+typedef long  time_t;
+/* OpenWatcom defines time_t as "unsigned long" */
+#endif
+
+#include <time.h>      /* localtime(), strftime() */
+
+
+#ifdef _MSC_VER
+
+/* the end of the precompiled headers */
+#pragma hdrstop
+
+#pragma warning(default:4201)
+
+/* 'type cast': from function pointer to data pointer */
+#pragma warning(disable:4054)
+
+/* 'type cast': from data pointer to function pointer */
+#pragma warning(disable:4055)
+
+/* 'function' : different 'const' qualifiers */
+#pragma warning(disable:4090)
+
+/* unreferenced formal parameter */
+#pragma warning(disable:4100)
+
+/* FD_SET() and FD_CLR(): conditional expression is constant */
+#pragma warning(disable:4127)
+
+/* conversion from 'type1' to 'type2', possible loss of data */
+#pragma warning(disable:4244)
+
+/* conversion from 'size_t' to 'type', possible loss of data */
+#pragma warning(disable:4267)
+
+/* array is too small to include a terminating null character */
+#pragma warning(disable:4295)
+
+/* conversion from 'type1' to 'type2' of greater size */
+#pragma warning(disable:4306)
+
+#endif
+
+
+#ifdef __WATCOMC__
+
+/* symbol 'ngx_rbtree_min' has been defined, but not referenced */
+#pragma disable_message(202)
+
+#endif
+
+
+#ifdef __BORLANDC__
+
+/* the end of the precompiled headers */
+#pragma hdrstop
+
+/* functions containing (for|while|some if) are not expanded inline */
+#pragma warn -8027
+
+/* unreferenced formal parameter */
+#pragma warn -8057
+
+/* suspicious pointer arithmetic */
+#pragma warn -8072
+
+#endif
+
+
+#include <ngx_auto_config.h>
+
+
+#define ngx_inline          __inline
+#define ngx_cdecl           __cdecl
+
+
+#ifdef _MSC_VER
+typedef unsigned __int32    uint32_t;
+typedef __int32             int32_t;
+typedef unsigned __int16    uint16_t;
+#define ngx_libc_cdecl      __cdecl
+
+#elif defined __BORLANDC__
+typedef unsigned __int32    uint32_t;
+typedef __int32             int32_t;
+typedef unsigned __int16    uint16_t;
+#define ngx_libc_cdecl      __cdecl
+
+#else /* __WATCOMC__ */
+typedef unsigned int        uint32_t;
+typedef int                 int32_t;
+typedef unsigned short int  uint16_t;
+#define ngx_libc_cdecl
+
+#endif
+
+typedef __int64             int64_t;
+typedef unsigned __int64    uint64_t;
+
+#if __BORLANDC__
+typedef int                 intptr_t;
+typedef u_int               uintptr_t;
+#endif
+
+
+#ifndef __MINGW64_VERSION_MAJOR
+
+/* Windows defines off_t as long, which is 32-bit */
+typedef __int64             off_t;
+#define _OFF_T_DEFINED
+
+#endif
+
+
+#ifdef __WATCOMC__
+
+/* off_t is redefined by sys/types.h used by zlib.h */
+#define __TYPES_H_INCLUDED
+typedef int                 dev_t;
+typedef unsigned int        ino_t;
+
+#elif __BORLANDC__
+
+/* off_t is redefined by sys/types.h used by zlib.h */
+#define __TYPES_H
+
+typedef int                 dev_t;
+typedef unsigned int        ino_t;
+
+#endif
+
+
+#ifndef __GNUC__
+#ifdef _WIN64
+typedef __int64             ssize_t;
+#else
+typedef int                 ssize_t;
+#endif
+#endif
+
+
+typedef uint32_t            in_addr_t;
+typedef u_short             in_port_t;
+typedef int                 sig_atomic_t;
+
+
+#ifdef _WIN64
+
+#define NGX_PTR_SIZE            8
+#define NGX_SIZE_T_LEN          (sizeof("-9223372036854775808") - 1)
+#define NGX_MAX_SIZE_T_VALUE    9223372036854775807
+#define NGX_TIME_T_LEN          (sizeof("-9223372036854775808") - 1)
+#define NGX_TIME_T_SIZE         8
+#define NGX_MAX_TIME_T_VALUE    9223372036854775807
+
+#else
+
+#define NGX_PTR_SIZE            4
+#define NGX_SIZE_T_LEN          (sizeof("-2147483648") - 1)
+#define NGX_MAX_SIZE_T_VALUE    2147483647
+#define NGX_TIME_T_LEN          (sizeof("-2147483648") - 1)
+#define NGX_TIME_T_SIZE         4
+#define NGX_MAX_TIME_T_VALUE    2147483647
+
+#endif
+
+
+#define NGX_OFF_T_LEN           (sizeof("-9223372036854775807") - 1)
+#define NGX_MAX_OFF_T_VALUE     9223372036854775807
+#define NGX_SIG_ATOMIC_T_SIZE   4
+
+
+#define NGX_HAVE_LITTLE_ENDIAN  1
+#define NGX_HAVE_NONALIGNED     1
+
+
+#define NGX_WIN_NT        200000
+
+
+#define NGX_LISTEN_BACKLOG           511
+
+
+#ifndef NGX_HAVE_INHERITED_NONBLOCK
+#define NGX_HAVE_INHERITED_NONBLOCK  1
+#endif
+
+#ifndef NGX_HAVE_CASELESS_FILESYSTEM
+#define NGX_HAVE_CASELESS_FILESYSTEM  1
+#endif
+
+#ifndef NGX_HAVE_WIN32_TRANSMITPACKETS
+#define NGX_HAVE_WIN32_TRANSMITPACKETS  1
+#define NGX_HAVE_WIN32_TRANSMITFILE     0
+#endif
+
+#ifndef NGX_HAVE_WIN32_TRANSMITFILE
+#define NGX_HAVE_WIN32_TRANSMITFILE  1
+#endif
+
+#if (NGX_HAVE_WIN32_TRANSMITPACKETS) || (NGX_HAVE_WIN32_TRANSMITFILE)
+#define NGX_HAVE_SENDFILE  1
+#endif
+
+#ifndef NGX_HAVE_SO_SNDLOWAT
+/* setsockopt(SO_SNDLOWAT) returns error WSAENOPROTOOPT */
+#define NGX_HAVE_SO_SNDLOWAT         0
+#endif
+
+#ifndef NGX_HAVE_FIONREAD
+#define NGX_HAVE_FIONREAD            1
+#endif
+
+#define NGX_HAVE_GETADDRINFO         1
+
+#define ngx_random()                                                          \
+    ((long) (0x7fffffff & ( ((uint32_t) rand() << 16)                         \
+                          ^ ((uint32_t) rand() << 8)                          \
+                          ^ ((uint32_t) rand()) )))
+
+#define ngx_debug_init()
+
+
+#endif /* _NGX_WIN32_CONFIG_H_INCLUDED_ */
diff --git a/src/os/win32/ngx_win32_init.c b/src/os/win32/ngx_win32_init.c
new file mode 100644
index 0000000..de66a44
--- /dev/null
+++ b/src/os/win32/ngx_win32_init.c
@@ -0,0 +1,329 @@
+
+/*
+ * Copyright (C) Igor Sysoev
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <nginx.h>
+
+
+ngx_uint_t  ngx_win32_version;
+ngx_uint_t  ngx_ncpu;
+ngx_uint_t  ngx_max_wsabufs;
+ngx_int_t   ngx_max_sockets;
+ngx_uint_t  ngx_inherited_nonblocking = 1;
+ngx_uint_t  ngx_tcp_nodelay_and_tcp_nopush;
+
+char        ngx_unique[NGX_INT32_LEN + 1];
+
+
+ngx_os_io_t ngx_os_io = {
+    ngx_wsarecv,
+    ngx_wsarecv_chain,
+    ngx_udp_wsarecv,
+    ngx_wsasend,
+    NULL,
+    NULL,
+    ngx_wsasend_chain,
+    0
+};
+
+
+typedef struct {
+    WORD  wServicePackMinor;
+    WORD  wSuiteMask;
+    BYTE  wProductType;
+} ngx_osviex_stub_t;
+
+
+static u_int               osviex;
+static OSVERSIONINFOEX     osvi;
+
+/* Should these pointers be per protocol ? */
+LPFN_ACCEPTEX              ngx_acceptex;
+LPFN_GETACCEPTEXSOCKADDRS  ngx_getacceptexsockaddrs;
+LPFN_TRANSMITFILE          ngx_transmitfile;
+LPFN_TRANSMITPACKETS       ngx_transmitpackets;
+LPFN_CONNECTEX             ngx_connectex;
+LPFN_DISCONNECTEX          ngx_disconnectex;
+
+static GUID ax_guid = WSAID_ACCEPTEX;
+static GUID as_guid = WSAID_GETACCEPTEXSOCKADDRS;
+static GUID tf_guid = WSAID_TRANSMITFILE;
+static GUID tp_guid = WSAID_TRANSMITPACKETS;
+static GUID cx_guid = WSAID_CONNECTEX;
+static GUID dx_guid = WSAID_DISCONNECTEX;
+
+
+#if (NGX_LOAD_WSAPOLL)
+ngx_wsapoll_pt             WSAPoll;
+ngx_uint_t                 ngx_have_wsapoll;
+#endif
+
+
+ngx_int_t
+ngx_os_init(ngx_log_t *log)
+{
+    DWORD         bytes;
+    SOCKET        s;
+    WSADATA       wsd;
+    ngx_err_t     err;
+    ngx_time_t   *tp;
+    ngx_uint_t    n;
+    SYSTEM_INFO   si;
+
+    /* get Windows version */
+
+    ngx_memzero(&osvi, sizeof(OSVERSIONINFOEX));
+    osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX);
+
+#ifdef _MSC_VER
+#pragma warning(disable:4996)
+#endif
+
+    osviex = GetVersionEx((OSVERSIONINFO *) &osvi);
+
+    if (osviex == 0) {
+        osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX);
+        if (GetVersionEx((OSVERSIONINFO *) &osvi) == 0) {
+            ngx_log_error(NGX_LOG_EMERG, log, ngx_errno,
+                          "GetVersionEx() failed");
+            return NGX_ERROR;
+        }
+    }
+
+#ifdef _MSC_VER
+#pragma warning(default:4996)
+#endif
+
+    /*
+     *  Windows 3.1 Win32s   0xxxxx
+     *
+     *  Windows 95           140000
+     *  Windows 98           141000
+     *  Windows ME           149000
+     *  Windows NT 3.51      235100
+     *  Windows NT 4.0       240000
+     *  Windows NT 4.0 SP5   240050
+     *  Windows 2000         250000
+     *  Windows XP           250100
+     *  Windows 2003         250200
+     *  Windows Vista/2008   260000
+     *
+     *  Windows CE x.x       3xxxxx
+     */
+
+    ngx_win32_version = osvi.dwPlatformId * 100000
+                        + osvi.dwMajorVersion * 10000
+                        + osvi.dwMinorVersion * 100;
+
+    if (osviex) {
+        ngx_win32_version += osvi.wServicePackMajor * 10
+                             + osvi.wServicePackMinor;
+    }
+
+    GetSystemInfo(&si);
+    ngx_pagesize = si.dwPageSize;
+    ngx_allocation_granularity = si.dwAllocationGranularity;
+    ngx_ncpu = si.dwNumberOfProcessors;
+    ngx_cacheline_size = NGX_CPU_CACHE_LINE;
+
+    for (n = ngx_pagesize; n >>= 1; ngx_pagesize_shift++) { /* void */ }
+
+    /* delete default "C" locale for _wcsicmp() */
+    setlocale(LC_ALL, "");
+
+
+    /* init Winsock */
+
+    if (WSAStartup(MAKEWORD(2,2), &wsd) != 0) {
+        ngx_log_error(NGX_LOG_EMERG, log, ngx_socket_errno,
+                      "WSAStartup() failed");
+        return NGX_ERROR;
+    }
+
+    if (ngx_win32_version < NGX_WIN_NT) {
+        ngx_max_wsabufs = 16;
+        return NGX_OK;
+    }
+
+    /* STUB: ngx_uint_t max */
+    ngx_max_wsabufs = 1024 * 1024;
+
+    /*
+     * get AcceptEx(), GetAcceptExSockAddrs(), TransmitFile(),
+     * TransmitPackets(), ConnectEx(), and DisconnectEx() addresses
+     */
+
+    s = ngx_socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
+    if (s == (ngx_socket_t) -1) {
+        ngx_log_error(NGX_LOG_EMERG, log, ngx_socket_errno,
+                      ngx_socket_n " failed");
+        return NGX_ERROR;
+    }
+
+    if (WSAIoctl(s, SIO_GET_EXTENSION_FUNCTION_POINTER, &ax_guid, sizeof(GUID),
+                 &ngx_acceptex, sizeof(LPFN_ACCEPTEX), &bytes, NULL, NULL)
+        == -1)
+    {
+        ngx_log_error(NGX_LOG_NOTICE, log, ngx_socket_errno,
+                      "WSAIoctl(SIO_GET_EXTENSION_FUNCTION_POINTER, "
+                               "WSAID_ACCEPTEX) failed");
+    }
+
+    if (WSAIoctl(s, SIO_GET_EXTENSION_FUNCTION_POINTER, &as_guid, sizeof(GUID),
+                 &ngx_getacceptexsockaddrs, sizeof(LPFN_GETACCEPTEXSOCKADDRS),
+                 &bytes, NULL, NULL)
+        == -1)
+    {
+        ngx_log_error(NGX_LOG_NOTICE, log, ngx_socket_errno,
+                      "WSAIoctl(SIO_GET_EXTENSION_FUNCTION_POINTER, "
+                               "WSAID_GETACCEPTEXSOCKADDRS) failed");
+    }
+
+    if (WSAIoctl(s, SIO_GET_EXTENSION_FUNCTION_POINTER, &tf_guid, sizeof(GUID),
+                 &ngx_transmitfile, sizeof(LPFN_TRANSMITFILE), &bytes,
+                 NULL, NULL)
+        == -1)
+    {
+        ngx_log_error(NGX_LOG_NOTICE, log, ngx_socket_errno,
+                      "WSAIoctl(SIO_GET_EXTENSION_FUNCTION_POINTER, "
+                               "WSAID_TRANSMITFILE) failed");
+    }
+
+    if (WSAIoctl(s, SIO_GET_EXTENSION_FUNCTION_POINTER, &tp_guid, sizeof(GUID),
+                 &ngx_transmitpackets, sizeof(LPFN_TRANSMITPACKETS), &bytes,
+                 NULL, NULL)
+        == -1)
+    {
+        ngx_log_error(NGX_LOG_NOTICE, log, ngx_socket_errno,
+                      "WSAIoctl(SIO_GET_EXTENSION_FUNCTION_POINTER, "
+                               "WSAID_TRANSMITPACKETS) failed");
+    }
+
+    if (WSAIoctl(s, SIO_GET_EXTENSION_FUNCTION_POINTER, &cx_guid, sizeof(GUID),
+                 &ngx_connectex, sizeof(LPFN_CONNECTEX), &bytes,
+                 NULL, NULL)
+        == -1)
+    {
+        ngx_log_error(NGX_LOG_NOTICE, log, ngx_socket_errno,
+                      "WSAIoctl(SIO_GET_EXTENSION_FUNCTION_POINTER, "
+                               "WSAID_CONNECTEX) failed");
+    }
+
+    if (WSAIoctl(s, SIO_GET_EXTENSION_FUNCTION_POINTER, &dx_guid, sizeof(GUID),
+                 &ngx_disconnectex, sizeof(LPFN_DISCONNECTEX), &bytes,
+                 NULL, NULL)
+        == -1)
+    {
+        ngx_log_error(NGX_LOG_NOTICE, log, ngx_socket_errno,
+                      "WSAIoctl(SIO_GET_EXTENSION_FUNCTION_POINTER, "
+                               "WSAID_DISCONNECTEX) failed");
+    }
+
+    if (ngx_close_socket(s) == -1) {
+        ngx_log_error(NGX_LOG_ALERT, log, ngx_socket_errno,
+                      ngx_close_socket_n " failed");
+    }
+
+#if (NGX_LOAD_WSAPOLL)
+    {
+    HMODULE  hmod;
+
+    hmod = GetModuleHandle("ws2_32.dll");
+    if (hmod == NULL) {
+        ngx_log_error(NGX_LOG_NOTICE, log, ngx_errno,
+                      "GetModuleHandle(\"ws2_32.dll\") failed");
+        goto nopoll;
+    }
+
+    WSAPoll = (ngx_wsapoll_pt) (void *) GetProcAddress(hmod, "WSAPoll");
+    if (WSAPoll == NULL) {
+        ngx_log_error(NGX_LOG_NOTICE, log, ngx_errno,
+                      "GetProcAddress(\"WSAPoll\") failed");
+        goto nopoll;
+    }
+
+    ngx_have_wsapoll = 1;
+
+    }
+
+nopoll:
+
+#endif
+
+    if (GetEnvironmentVariable("ngx_unique", ngx_unique, NGX_INT32_LEN + 1)
+        != 0)
+    {
+        ngx_process = NGX_PROCESS_WORKER;
+
+    } else {
+        err = ngx_errno;
+
+        if (err != ERROR_ENVVAR_NOT_FOUND) {
+            ngx_log_error(NGX_LOG_EMERG, log, err,
+                          "GetEnvironmentVariable(\"ngx_unique\") failed");
+            return NGX_ERROR;
+        }
+
+        ngx_sprintf((u_char *) ngx_unique, "%P%Z", ngx_pid);
+    }
+
+    tp = ngx_timeofday();
+    srand((ngx_pid << 16) ^ (unsigned) tp->sec ^ tp->msec);
+
+    return NGX_OK;
+}
+
+
+void
+ngx_os_status(ngx_log_t *log)
+{
+    ngx_osviex_stub_t  *osviex_stub;
+
+    ngx_log_error(NGX_LOG_NOTICE, log, 0, NGINX_VER_BUILD);
+
+    if (osviex) {
+
+        /*
+         * the MSVC 6.0 SP2 defines wSuiteMask and wProductType
+         * as WORD wReserved[2]
+         */
+        osviex_stub = (ngx_osviex_stub_t *) &osvi.wServicePackMinor;
+
+        ngx_log_error(NGX_LOG_INFO, log, 0,
+                      "OS: %ui build:%ud, \"%s\", suite:%Xd, type:%ud",
+                      ngx_win32_version, osvi.dwBuildNumber, osvi.szCSDVersion,
+                      osviex_stub->wSuiteMask, osviex_stub->wProductType);
+
+    } else {
+        if (osvi.dwPlatformId == VER_PLATFORM_WIN32_WINDOWS) {
+
+            /* Win9x build */
+
+            ngx_log_error(NGX_LOG_INFO, log, 0,
+                          "OS: %ui build:%ud.%ud.%ud, \"%s\"",
+                          ngx_win32_version,
+                          osvi.dwBuildNumber >> 24,
+                          (osvi.dwBuildNumber >> 16) & 0xff,
+                          osvi.dwBuildNumber & 0xffff,
+                          osvi.szCSDVersion);
+
+        } else {
+
+            /*
+             * VER_PLATFORM_WIN32_NT
+             *
+             * we do not currently support VER_PLATFORM_WIN32_CE
+             * and we do not support VER_PLATFORM_WIN32s at all
+             */
+
+            ngx_log_error(NGX_LOG_INFO, log, 0, "OS: %ui build:%ud, \"%s\"",
+                          ngx_win32_version, osvi.dwBuildNumber,
+                          osvi.szCSDVersion);
+        }
+    }
+}
diff --git a/src/os/win32/ngx_wsarecv.c b/src/os/win32/ngx_wsarecv.c
new file mode 100644
index 0000000..b01405e
--- /dev/null
+++ b/src/os/win32/ngx_wsarecv.c
@@ -0,0 +1,215 @@
+
+/*
+ * Copyright (C) Igor Sysoev
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_event.h>
+
+
+ssize_t
+ngx_wsarecv(ngx_connection_t *c, u_char *buf, size_t size)
+{
+    int           rc;
+    u_long        bytes, flags;
+    WSABUF        wsabuf[1];
+    ngx_err_t     err;
+    ngx_int_t     n;
+    ngx_event_t  *rev;
+
+    wsabuf[0].buf = (char *) buf;
+    wsabuf[0].len = size;
+    flags = 0;
+    bytes = 0;
+
+    rc = WSARecv(c->fd, wsabuf, 1, &bytes, &flags, NULL, NULL);
+
+    ngx_log_debug4(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "WSARecv: fd:%d rc:%d %ul of %z", c->fd, rc, bytes, size);
+
+    rev = c->read;
+
+    if (rc == -1) {
+        rev->ready = 0;
+        err = ngx_socket_errno;
+
+        if (err == WSAEWOULDBLOCK) {
+            ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, err,
+                           "WSARecv() not ready");
+            return NGX_AGAIN;
+        }
+
+        n = ngx_connection_error(c, err, "WSARecv() failed");
+
+        if (n == NGX_ERROR) {
+            rev->error = 1;
+        }
+
+        return n;
+    }
+
+#if (NGX_HAVE_FIONREAD)
+
+    if (rev->available >= 0 && bytes > 0) {
+        rev->available -= bytes;
+
+        /*
+         * negative rev->available means some additional bytes
+         * were received between kernel notification and WSARecv(),
+         * and therefore ev->ready can be safely reset even for
+         * edge-triggered event methods
+         */
+
+        if (rev->available < 0) {
+            rev->available = 0;
+            rev->ready = 0;
+        }
+
+        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "WSARecv: avail:%d", rev->available);
+
+    } else if (bytes == size) {
+
+        if (ngx_socket_nread(c->fd, &rev->available) == -1) {
+            n = ngx_connection_error(c, ngx_socket_errno,
+                                     ngx_socket_nread_n " failed");
+
+            if (n == NGX_ERROR) {
+                rev->ready = 0;
+                rev->error = 1;
+            }
+
+            return n;
+        }
+
+        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "WSARecv: avail:%d", rev->available);
+    }
+
+#endif
+
+    if (bytes < size) {
+        rev->ready = 0;
+    }
+
+    if (bytes == 0) {
+        rev->ready = 0;
+        rev->eof = 1;
+    }
+
+    return bytes;
+}
+
+
+ssize_t
+ngx_overlapped_wsarecv(ngx_connection_t *c, u_char *buf, size_t size)
+{
+    int               rc;
+    u_long            bytes, flags;
+    WSABUF            wsabuf[1];
+    ngx_err_t         err;
+    ngx_int_t         n;
+    ngx_event_t      *rev;
+    LPWSAOVERLAPPED   ovlp;
+
+    rev = c->read;
+
+    if (!rev->ready) {
+        ngx_log_error(NGX_LOG_ALERT, c->log, 0, "second wsa post");
+        return NGX_AGAIN;
+    }
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "rev->complete: %d", rev->complete);
+
+    if (rev->complete) {
+        rev->complete = 0;
+
+        if (ngx_event_flags & NGX_USE_IOCP_EVENT) {
+            if (rev->ovlp.error) {
+                ngx_connection_error(c, rev->ovlp.error, "WSARecv() failed");
+                return NGX_ERROR;
+            }
+
+            ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                           "WSARecv ovlp: fd:%d %ul of %z",
+                           c->fd, rev->available, size);
+
+            return rev->available;
+        }
+
+        if (WSAGetOverlappedResult(c->fd, (LPWSAOVERLAPPED) &rev->ovlp,
+                                   &bytes, 0, NULL)
+            == 0)
+        {
+            ngx_connection_error(c, ngx_socket_errno,
+                               "WSARecv() or WSAGetOverlappedResult() failed");
+            return NGX_ERROR;
+        }
+
+        ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "WSARecv: fd:%d %ul of %z", c->fd, bytes, size);
+
+        return bytes;
+    }
+
+    ovlp = (LPWSAOVERLAPPED) &rev->ovlp;
+    ngx_memzero(ovlp, sizeof(WSAOVERLAPPED));
+    wsabuf[0].buf = (char *) buf;
+    wsabuf[0].len = size;
+    flags = 0;
+    bytes = 0;
+
+    rc = WSARecv(c->fd, wsabuf, 1, &bytes, &flags, ovlp, NULL);
+
+    rev->complete = 0;
+
+    ngx_log_debug4(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "WSARecv ovlp: fd:%d rc:%d %ul of %z",
+                   c->fd, rc, bytes, size);
+
+    if (rc == -1) {
+        err = ngx_socket_errno;
+        if (err == WSA_IO_PENDING) {
+            rev->active = 1;
+            ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, err,
+                           "WSARecv() posted");
+            return NGX_AGAIN;
+        }
+
+        n = ngx_connection_error(c, err, "WSARecv() failed");
+
+        if (n == NGX_ERROR) {
+            rev->error = 1;
+        }
+
+        return n;
+    }
+
+    if (ngx_event_flags & NGX_USE_IOCP_EVENT) {
+
+        /*
+         * if a socket was bound with I/O completion port
+         * then GetQueuedCompletionStatus() would anyway return its status
+         * despite that WSARecv() was already complete
+         */
+
+        rev->active = 1;
+        return NGX_AGAIN;
+    }
+
+    if (bytes == 0) {
+        rev->eof = 1;
+        rev->ready = 0;
+
+    } else {
+        rev->ready = 1;
+    }
+
+    rev->active = 0;
+
+    return bytes;
+}
diff --git a/src/os/win32/ngx_wsarecv_chain.c b/src/os/win32/ngx_wsarecv_chain.c
new file mode 100644
index 0000000..e60389b
--- /dev/null
+++ b/src/os/win32/ngx_wsarecv_chain.c
@@ -0,0 +1,147 @@
+
+/*
+ * Copyright (C) Igor Sysoev
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_event.h>
+
+
+#define NGX_WSABUFS  64
+
+
+ssize_t
+ngx_wsarecv_chain(ngx_connection_t *c, ngx_chain_t *chain, off_t limit)
+{
+    int           rc;
+    u_char       *prev;
+    u_long        bytes, flags;
+    size_t        n, size;
+    ngx_err_t     err;
+    ngx_array_t   vec;
+    ngx_event_t  *rev;
+    LPWSABUF      wsabuf;
+    WSABUF        wsabufs[NGX_WSABUFS];
+
+    prev = NULL;
+    wsabuf = NULL;
+    flags = 0;
+    size = 0;
+    bytes = 0;
+
+    vec.elts = wsabufs;
+    vec.nelts = 0;
+    vec.size = sizeof(WSABUF);
+    vec.nalloc = NGX_WSABUFS;
+    vec.pool = c->pool;
+
+    /* coalesce the neighbouring bufs */
+
+    while (chain) {
+        n = chain->buf->end - chain->buf->last;
+
+        if (limit) {
+            if (size >= (size_t) limit) {
+                break;
+            }
+
+            if (size + n > (size_t) limit) {
+                n = (size_t) limit - size;
+            }
+        }
+
+        if (prev == chain->buf->last) {
+            wsabuf->len += n;
+
+        } else {
+            if (vec.nelts == vec.nalloc) {
+                break;
+            }
+
+            wsabuf = ngx_array_push(&vec);
+            if (wsabuf == NULL) {
+                return NGX_ERROR;
+            }
+
+            wsabuf->buf = (char *) chain->buf->last;
+            wsabuf->len = n;
+        }
+
+        size += n;
+        prev = chain->buf->end;
+        chain = chain->next;
+    }
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "WSARecv: %d:%d", vec.nelts, wsabuf->len);
+
+
+    rc = WSARecv(c->fd, vec.elts, vec.nelts, &bytes, &flags, NULL, NULL);
+
+    rev = c->read;
+
+    if (rc == -1) {
+        rev->ready = 0;
+        err = ngx_socket_errno;
+
+        if (err == WSAEWOULDBLOCK) {
+            ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, err,
+                           "WSARecv() not ready");
+            return NGX_AGAIN;
+        }
+
+        rev->error = 1;
+        ngx_connection_error(c, err, "WSARecv() failed");
+        return NGX_ERROR;
+    }
+
+#if (NGX_HAVE_FIONREAD)
+
+    if (rev->available >= 0 && bytes > 0) {
+        rev->available -= bytes;
+
+        /*
+         * negative rev->available means some additional bytes
+         * were received between kernel notification and WSARecv(),
+         * and therefore ev->ready can be safely reset even for
+         * edge-triggered event methods
+         */
+
+        if (rev->available < 0) {
+            rev->available = 0;
+            rev->ready = 0;
+        }
+
+        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "WSARecv: avail:%d", rev->available);
+
+    } else if (bytes == size) {
+
+        if (ngx_socket_nread(c->fd, &rev->available) == -1) {
+            rev->ready = 0;
+            rev->error = 1;
+            ngx_connection_error(c, ngx_socket_errno,
+                                 ngx_socket_nread_n " failed");
+            return NGX_ERROR;
+        }
+
+        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "WSARecv: avail:%d", rev->available);
+    }
+
+#endif
+
+    if (bytes < size) {
+        rev->ready = 0;
+    }
+
+    if (bytes == 0) {
+        rev->ready = 0;
+        rev->eof = 1;
+    }
+
+    return bytes;
+}
diff --git a/src/os/win32/ngx_wsasend.c b/src/os/win32/ngx_wsasend.c
new file mode 100644
index 0000000..d6a23d1
--- /dev/null
+++ b/src/os/win32/ngx_wsasend.c
@@ -0,0 +1,185 @@
+
+/*
+ * Copyright (C) Igor Sysoev
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_event.h>
+
+
+ssize_t
+ngx_wsasend(ngx_connection_t *c, u_char *buf, size_t size)
+{
+    int           n;
+    u_long        sent;
+    ngx_err_t     err;
+    ngx_event_t  *wev;
+    WSABUF        wsabuf;
+
+    wev = c->write;
+
+    if (!wev->ready) {
+        return NGX_AGAIN;
+    }
+
+    /*
+     * WSABUF must be 4-byte aligned otherwise
+     * WSASend() will return undocumented WSAEINVAL error.
+     */
+
+    wsabuf.buf = (char *) buf;
+    wsabuf.len = size;
+
+    sent = 0;
+
+    n = WSASend(c->fd, &wsabuf, 1, &sent, 0, NULL, NULL);
+
+    ngx_log_debug4(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "WSASend: fd:%d, %d, %ul of %uz", c->fd, n, sent, size);
+
+    if (n == 0) {
+        if (sent < size) {
+            wev->ready = 0;
+        }
+
+        c->sent += sent;
+
+        return sent;
+    }
+
+    err = ngx_socket_errno;
+
+    if (err == WSAEWOULDBLOCK) {
+        ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, err, "WSASend() not ready");
+        wev->ready = 0;
+        return NGX_AGAIN;
+    }
+
+    wev->error = 1;
+    ngx_connection_error(c, err, "WSASend() failed");
+
+    return NGX_ERROR;
+}
+
+
+ssize_t
+ngx_overlapped_wsasend(ngx_connection_t *c, u_char *buf, size_t size)
+{
+    int               n;
+    u_long            sent;
+    ngx_err_t         err;
+    ngx_event_t      *wev;
+    LPWSAOVERLAPPED   ovlp;
+    WSABUF            wsabuf;
+
+    wev = c->write;
+
+    if (!wev->ready) {
+        return NGX_AGAIN;
+    }
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "wev->complete: %d", wev->complete);
+
+    if (!wev->complete) {
+
+        /* post the overlapped WSASend() */
+
+        /*
+         * WSABUFs must be 4-byte aligned otherwise
+         * WSASend() will return undocumented WSAEINVAL error.
+         */
+
+        wsabuf.buf = (char *) buf;
+        wsabuf.len = size;
+
+        sent = 0;
+
+        ovlp = (LPWSAOVERLAPPED) &c->write->ovlp;
+        ngx_memzero(ovlp, sizeof(WSAOVERLAPPED));
+
+        n = WSASend(c->fd, &wsabuf, 1, &sent, 0, ovlp, NULL);
+
+        ngx_log_debug4(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "WSASend: fd:%d, %d, %ul of %uz", c->fd, n, sent, size);
+
+        wev->complete = 0;
+
+        if (n == 0) {
+            if (ngx_event_flags & NGX_USE_IOCP_EVENT) {
+
+                /*
+                 * if a socket was bound with I/O completion port then
+                 * GetQueuedCompletionStatus() would anyway return its status
+                 * despite that WSASend() was already complete
+                 */
+
+                wev->active = 1;
+                return NGX_AGAIN;
+            }
+
+            if (sent < size) {
+                wev->ready = 0;
+            }
+
+            c->sent += sent;
+
+            return sent;
+        }
+
+        err = ngx_socket_errno;
+
+        if (err == WSA_IO_PENDING) {
+            ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, err,
+                           "WSASend() posted");
+            wev->active = 1;
+            return NGX_AGAIN;
+        }
+
+        wev->error = 1;
+        ngx_connection_error(c, err, "WSASend() failed");
+
+        return NGX_ERROR;
+    }
+
+    /* the overlapped WSASend() complete */
+
+    wev->complete = 0;
+    wev->active = 0;
+
+    if (ngx_event_flags & NGX_USE_IOCP_EVENT) {
+
+        if (wev->ovlp.error) {
+            ngx_connection_error(c, wev->ovlp.error, "WSASend() failed");
+            return NGX_ERROR;
+        }
+
+        sent = wev->available;
+
+    } else {
+        if (WSAGetOverlappedResult(c->fd, (LPWSAOVERLAPPED) &wev->ovlp,
+                                   &sent, 0, NULL)
+            == 0)
+        {
+            ngx_connection_error(c, ngx_socket_errno,
+                           "WSASend() or WSAGetOverlappedResult() failed");
+
+            return NGX_ERROR;
+        }
+    }
+
+    ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "WSAGetOverlappedResult: fd:%d, %ul of %uz",
+                   c->fd, sent, size);
+
+    if (sent < size) {
+        wev->ready = 0;
+    }
+
+    c->sent += sent;
+
+    return sent;
+}
diff --git a/src/os/win32/ngx_wsasend_chain.c b/src/os/win32/ngx_wsasend_chain.c
new file mode 100644
index 0000000..cd50e71
--- /dev/null
+++ b/src/os/win32/ngx_wsasend_chain.c
@@ -0,0 +1,296 @@
+
+/*
+ * Copyright (C) Igor Sysoev
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_event.h>
+
+
+#define NGX_WSABUFS  64
+
+
+ngx_chain_t *
+ngx_wsasend_chain(ngx_connection_t *c, ngx_chain_t *in, off_t limit)
+{
+    int           rc;
+    u_char       *prev;
+    u_long        size, sent, send, prev_send;
+    ngx_err_t     err;
+    ngx_event_t  *wev;
+    ngx_array_t   vec;
+    ngx_chain_t  *cl;
+    LPWSABUF      wsabuf;
+    WSABUF        wsabufs[NGX_WSABUFS];
+
+    wev = c->write;
+
+    if (!wev->ready) {
+        return in;
+    }
+
+    /* the maximum limit size is the maximum u_long value - the page size */
+
+    if (limit == 0 || limit > (off_t) (NGX_MAX_UINT32_VALUE - ngx_pagesize)) {
+        limit = NGX_MAX_UINT32_VALUE - ngx_pagesize;
+    }
+
+    send = 0;
+
+    /*
+     * WSABUFs must be 4-byte aligned otherwise
+     * WSASend() will return undocumented WSAEINVAL error.
+     */
+
+    vec.elts = wsabufs;
+    vec.size = sizeof(WSABUF);
+    vec.nalloc = ngx_min(NGX_WSABUFS, ngx_max_wsabufs);
+    vec.pool = c->pool;
+
+    for ( ;; ) {
+        prev = NULL;
+        wsabuf = NULL;
+        prev_send = send;
+
+        vec.nelts = 0;
+
+        /* create the WSABUF and coalesce the neighbouring bufs */
+
+        for (cl = in; cl && send < limit; cl = cl->next) {
+
+            if (ngx_buf_special(cl->buf)) {
+                continue;
+            }
+
+            size = cl->buf->last - cl->buf->pos;
+
+            if (send + size > limit) {
+                size = (u_long) (limit - send);
+            }
+
+            if (prev == cl->buf->pos) {
+                wsabuf->len += cl->buf->last - cl->buf->pos;
+
+            } else {
+                if (vec.nelts == vec.nalloc) {
+                    break;
+                }
+
+                wsabuf = ngx_array_push(&vec);
+                if (wsabuf == NULL) {
+                    return NGX_CHAIN_ERROR;
+                }
+
+                wsabuf->buf = (char *) cl->buf->pos;
+                wsabuf->len = cl->buf->last - cl->buf->pos;
+            }
+
+            prev = cl->buf->last;
+            send += size;
+        }
+
+        sent = 0;
+
+        rc = WSASend(c->fd, vec.elts, vec.nelts, &sent, 0, NULL, NULL);
+
+        if (rc == -1) {
+            err = ngx_errno;
+
+            if (err == WSAEWOULDBLOCK) {
+                ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, err,
+                               "WSASend() not ready");
+
+            } else {
+                wev->error = 1;
+                ngx_connection_error(c, err, "WSASend() failed");
+                return NGX_CHAIN_ERROR;
+            }
+        }
+
+        ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "WSASend: fd:%d, s:%ul", c->fd, sent);
+
+        c->sent += sent;
+
+        in = ngx_chain_update_sent(in, sent);
+
+        if (send - prev_send != sent) {
+            wev->ready = 0;
+            return in;
+        }
+
+        if (send >= limit || in == NULL) {
+            return in;
+        }
+    }
+}
+
+
+ngx_chain_t *
+ngx_overlapped_wsasend_chain(ngx_connection_t *c, ngx_chain_t *in, off_t limit)
+{
+    int               rc;
+    u_char           *prev;
+    u_long            size, send, sent;
+    ngx_err_t         err;
+    ngx_event_t      *wev;
+    ngx_array_t       vec;
+    ngx_chain_t      *cl;
+    LPWSAOVERLAPPED   ovlp;
+    LPWSABUF          wsabuf;
+    WSABUF            wsabufs[NGX_WSABUFS];
+
+    wev = c->write;
+
+    if (!wev->ready) {
+        return in;
+    }
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "wev->complete: %d", wev->complete);
+
+    if (!wev->complete) {
+
+        /* post the overlapped WSASend() */
+
+        /* the maximum limit size is the maximum u_long value - the page size */
+
+        if (limit == 0 || limit > (off_t) (NGX_MAX_UINT32_VALUE - ngx_pagesize))
+        {
+            limit = NGX_MAX_UINT32_VALUE - ngx_pagesize;
+        }
+
+        /*
+         * WSABUFs must be 4-byte aligned otherwise
+         * WSASend() will return undocumented WSAEINVAL error.
+         */
+
+        vec.elts = wsabufs;
+        vec.nelts = 0;
+        vec.size = sizeof(WSABUF);
+        vec.nalloc = ngx_min(NGX_WSABUFS, ngx_max_wsabufs);
+        vec.pool = c->pool;
+
+        send = 0;
+        prev = NULL;
+        wsabuf = NULL;
+
+        /* create the WSABUF and coalesce the neighbouring bufs */
+
+        for (cl = in; cl && send < limit; cl = cl->next) {
+
+            if (ngx_buf_special(cl->buf)) {
+                continue;
+            }
+
+            size = cl->buf->last - cl->buf->pos;
+
+            if (send + size > limit) {
+                size = (u_long) (limit - send);
+            }
+
+            if (prev == cl->buf->pos) {
+                wsabuf->len += cl->buf->last - cl->buf->pos;
+
+            } else {
+                if (vec.nelts == vec.nalloc) {
+                    break;
+                }
+
+                wsabuf = ngx_array_push(&vec);
+                if (wsabuf == NULL) {
+                    return NGX_CHAIN_ERROR;
+                }
+
+                wsabuf->buf = (char *) cl->buf->pos;
+                wsabuf->len = cl->buf->last - cl->buf->pos;
+            }
+
+            prev = cl->buf->last;
+            send += size;
+        }
+
+        ovlp = (LPWSAOVERLAPPED) &c->write->ovlp;
+        ngx_memzero(ovlp, sizeof(WSAOVERLAPPED));
+
+        rc = WSASend(c->fd, vec.elts, vec.nelts, &sent, 0, ovlp, NULL);
+
+        wev->complete = 0;
+
+        if (rc == -1) {
+            err = ngx_errno;
+
+            if (err == WSA_IO_PENDING) {
+                ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, err,
+                               "WSASend() posted");
+                wev->active = 1;
+                return in;
+
+            } else {
+                wev->error = 1;
+                ngx_connection_error(c, err, "WSASend() failed");
+                return NGX_CHAIN_ERROR;
+            }
+
+        } else if (ngx_event_flags & NGX_USE_IOCP_EVENT) {
+
+            /*
+             * if a socket was bound with I/O completion port then
+             * GetQueuedCompletionStatus() would anyway return its status
+             * despite that WSASend() was already complete
+             */
+
+            wev->active = 1;
+            return in;
+        }
+
+        ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "WSASend: fd:%d, s:%ul", c->fd, sent);
+
+    } else {
+
+        /* the overlapped WSASend() complete */
+
+        wev->complete = 0;
+        wev->active = 0;
+
+        if (ngx_event_flags & NGX_USE_IOCP_EVENT) {
+            if (wev->ovlp.error) {
+                ngx_connection_error(c, wev->ovlp.error, "WSASend() failed");
+                return NGX_CHAIN_ERROR;
+            }
+
+            sent = wev->available;
+
+        } else {
+            if (WSAGetOverlappedResult(c->fd, (LPWSAOVERLAPPED) &wev->ovlp,
+                                       &sent, 0, NULL)
+                == 0)
+            {
+                ngx_connection_error(c, ngx_socket_errno,
+                               "WSASend() or WSAGetOverlappedResult() failed");
+
+                return NGX_CHAIN_ERROR;
+            }
+        }
+    }
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "WSASend ovlp: fd:%d, s:%ul", c->fd, sent);
+
+    c->sent += sent;
+
+    in = ngx_chain_update_sent(in, sent);
+
+    if (in) {
+        wev->ready = 0;
+
+    } else {
+        wev->ready = 1;
+    }
+
+    return in;
+}
diff --git a/src/stream/ngx_stream.c b/src/stream/ngx_stream.c
index 3304c84..b6eeb23 100644
--- a/src/stream/ngx_stream.c
+++ b/src/stream/ngx_stream.c
@@ -16,16 +16,34 @@ static ngx_int_t ngx_stream_init_phases(ngx_conf_t *cf,
     ngx_stream_core_main_conf_t *cmcf);
 static ngx_int_t ngx_stream_init_phase_handlers(ngx_conf_t *cf,
     ngx_stream_core_main_conf_t *cmcf);
-static ngx_int_t ngx_stream_add_ports(ngx_conf_t *cf, ngx_array_t *ports,
-    ngx_stream_listen_t *listen);
-static char *ngx_stream_optimize_servers(ngx_conf_t *cf, ngx_array_t *ports);
+
+static ngx_int_t ngx_stream_add_addresses(ngx_conf_t *cf,
+    ngx_stream_core_srv_conf_t *cscf, ngx_stream_conf_port_t *port,
+    ngx_stream_listen_opt_t *lsopt);
+static ngx_int_t ngx_stream_add_address(ngx_conf_t *cf,
+    ngx_stream_core_srv_conf_t *cscf, ngx_stream_conf_port_t *port,
+    ngx_stream_listen_opt_t *lsopt);
+static ngx_int_t ngx_stream_add_server(ngx_conf_t *cf,
+    ngx_stream_core_srv_conf_t *cscf, ngx_stream_conf_addr_t *addr);
+
+static ngx_int_t ngx_stream_optimize_servers(ngx_conf_t *cf,
+    ngx_stream_core_main_conf_t *cmcf, ngx_array_t *ports);
+static ngx_int_t ngx_stream_server_names(ngx_conf_t *cf,
+    ngx_stream_core_main_conf_t *cmcf, ngx_stream_conf_addr_t *addr);
+static ngx_int_t ngx_stream_cmp_conf_addrs(const void *one, const void *two);
+static int ngx_libc_cdecl ngx_stream_cmp_dns_wildcards(const void *one,
+    const void *two);
+
+static ngx_int_t ngx_stream_init_listening(ngx_conf_t *cf,
+    ngx_stream_conf_port_t *port);
+static ngx_listening_t *ngx_stream_add_listening(ngx_conf_t *cf,
+    ngx_stream_conf_addr_t *addr);
 static ngx_int_t ngx_stream_add_addrs(ngx_conf_t *cf, ngx_stream_port_t *stport,
     ngx_stream_conf_addr_t *addr);
 #if (NGX_HAVE_INET6)
 static ngx_int_t ngx_stream_add_addrs6(ngx_conf_t *cf,
     ngx_stream_port_t *stport, ngx_stream_conf_addr_t *addr);
 #endif
-static ngx_int_t ngx_stream_cmp_conf_addrs(const void *one, const void *two);
 
 
 ngx_uint_t  ngx_stream_max_module;
@@ -74,10 +92,8 @@ static char *
 ngx_stream_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
 {
     char                          *rv;
-    ngx_uint_t                     i, m, mi, s;
+    ngx_uint_t                     mi, m, s;
     ngx_conf_t                     pcf;
-    ngx_array_t                    ports;
-    ngx_stream_listen_t           *listen;
     ngx_stream_module_t           *module;
     ngx_stream_conf_ctx_t         *ctx;
     ngx_stream_core_srv_conf_t   **cscfp;
@@ -251,21 +267,13 @@ ngx_stream_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
         return NGX_CONF_ERROR;
     }
 
-    if (ngx_array_init(&ports, cf->temp_pool, 4, sizeof(ngx_stream_conf_port_t))
-        != NGX_OK)
-    {
-        return NGX_CONF_ERROR;
-    }
-
-    listen = cmcf->listen.elts;
+    /* optimize the lists of ports, addresses and server names */
 
-    for (i = 0; i < cmcf->listen.nelts; i++) {
-        if (ngx_stream_add_ports(cf, &ports, &listen[i]) != NGX_OK) {
-            return NGX_CONF_ERROR;
-        }
+    if (ngx_stream_optimize_servers(cf, cmcf, cmcf->ports) != NGX_OK) {
+        return NGX_CONF_ERROR;
     }
 
-    return ngx_stream_optimize_servers(cf, &ports);
+    return NGX_CONF_OK;
 }
 
 
@@ -377,73 +385,295 @@ ngx_stream_init_phase_handlers(ngx_conf_t *cf,
 }
 
 
-static ngx_int_t
-ngx_stream_add_ports(ngx_conf_t *cf, ngx_array_t *ports,
-    ngx_stream_listen_t *listen)
+ngx_int_t
+ngx_stream_add_listen(ngx_conf_t *cf, ngx_stream_core_srv_conf_t *cscf,
+    ngx_stream_listen_opt_t *lsopt)
 {
-    in_port_t                p;
-    ngx_uint_t               i;
-    struct sockaddr         *sa;
-    ngx_stream_conf_port_t  *port;
-    ngx_stream_conf_addr_t  *addr;
+    in_port_t                     p;
+    ngx_uint_t                    i;
+    struct sockaddr              *sa;
+    ngx_stream_conf_port_t       *port;
+    ngx_stream_core_main_conf_t  *cmcf;
+
+    cmcf = ngx_stream_conf_get_module_main_conf(cf, ngx_stream_core_module);
+
+    if (cmcf->ports == NULL) {
+        cmcf->ports = ngx_array_create(cf->temp_pool, 2,
+                                       sizeof(ngx_stream_conf_port_t));
+        if (cmcf->ports == NULL) {
+            return NGX_ERROR;
+        }
+    }
 
-    sa = listen->sockaddr;
+    sa = lsopt->sockaddr;
     p = ngx_inet_get_port(sa);
 
-    port = ports->elts;
-    for (i = 0; i < ports->nelts; i++) {
+    port = cmcf->ports->elts;
+    for (i = 0; i < cmcf->ports->nelts; i++) {
 
-        if (p == port[i].port
-            && listen->type == port[i].type
-            && sa->sa_family == port[i].family)
+        if (p != port[i].port
+            || lsopt->type != port[i].type
+            || sa->sa_family != port[i].family)
         {
-            /* a port is already in the port list */
-
-            port = &port[i];
-            goto found;
+            continue;
         }
+
+        /* a port is already in the port list */
+
+        return ngx_stream_add_addresses(cf, cscf, &port[i], lsopt);
     }
 
     /* add a port to the port list */
 
-    port = ngx_array_push(ports);
+    port = ngx_array_push(cmcf->ports);
     if (port == NULL) {
         return NGX_ERROR;
     }
 
     port->family = sa->sa_family;
-    port->type = listen->type;
+    port->type = lsopt->type;
     port->port = p;
+    port->addrs.elts = NULL;
 
-    if (ngx_array_init(&port->addrs, cf->temp_pool, 2,
-                       sizeof(ngx_stream_conf_addr_t))
-        != NGX_OK)
-    {
-        return NGX_ERROR;
+    return ngx_stream_add_address(cf, cscf, port, lsopt);
+}
+
+
+static ngx_int_t
+ngx_stream_add_addresses(ngx_conf_t *cf, ngx_stream_core_srv_conf_t *cscf,
+    ngx_stream_conf_port_t *port, ngx_stream_listen_opt_t *lsopt)
+{
+    ngx_uint_t               i, default_server, proxy_protocol,
+                             protocols, protocols_prev;
+    ngx_stream_conf_addr_t  *addr;
+#if (NGX_STREAM_SSL)
+    ngx_uint_t               ssl;
+#endif
+
+    /*
+     * we cannot compare whole sockaddr struct's as kernel
+     * may fill some fields in inherited sockaddr struct's
+     */
+
+    addr = port->addrs.elts;
+
+    for (i = 0; i < port->addrs.nelts; i++) {
+
+        if (ngx_cmp_sockaddr(lsopt->sockaddr, lsopt->socklen,
+                             addr[i].opt.sockaddr,
+                             addr[i].opt.socklen, 0)
+            != NGX_OK)
+        {
+            continue;
+        }
+
+        /* the address is already in the address list */
+
+        if (ngx_stream_add_server(cf, cscf, &addr[i]) != NGX_OK) {
+            return NGX_ERROR;
+        }
+
+        /* preserve default_server bit during listen options overwriting */
+        default_server = addr[i].opt.default_server;
+
+        proxy_protocol = lsopt->proxy_protocol || addr[i].opt.proxy_protocol;
+        protocols = lsopt->proxy_protocol;
+        protocols_prev = addr[i].opt.proxy_protocol;
+
+#if (NGX_STREAM_SSL)
+        ssl = lsopt->ssl || addr[i].opt.ssl;
+        protocols |= lsopt->ssl << 1;
+        protocols_prev |= addr[i].opt.ssl << 1;
+#endif
+
+        if (lsopt->set) {
+
+            if (addr[i].opt.set) {
+                ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
+                                   "duplicate listen options for %V",
+                                   &addr[i].opt.addr_text);
+                return NGX_ERROR;
+            }
+
+            addr[i].opt = *lsopt;
+        }
+
+        /* check the duplicate "default" server for this address:port */
+
+        if (lsopt->default_server) {
+
+            if (default_server) {
+                ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
+                                   "a duplicate default server for %V",
+                                   &addr[i].opt.addr_text);
+                return NGX_ERROR;
+            }
+
+            default_server = 1;
+            addr[i].default_server = cscf;
+        }
+
+        /* check for conflicting protocol options */
+
+        if ((protocols | protocols_prev) != protocols_prev) {
+
+            /* options added */
+
+            if ((addr[i].opt.set && !lsopt->set)
+                || addr[i].protocols_changed
+                || (protocols | protocols_prev) != protocols)
+            {
+                ngx_conf_log_error(NGX_LOG_WARN, cf, 0,
+                                   "protocol options redefined for %V",
+                                   &addr[i].opt.addr_text);
+            }
+
+            addr[i].protocols = protocols_prev;
+            addr[i].protocols_set = 1;
+            addr[i].protocols_changed = 1;
+
+        } else if ((protocols_prev | protocols) != protocols) {
+
+            /* options removed */
+
+            if (lsopt->set
+                || (addr[i].protocols_set && protocols != addr[i].protocols))
+            {
+                ngx_conf_log_error(NGX_LOG_WARN, cf, 0,
+                                   "protocol options redefined for %V",
+                                   &addr[i].opt.addr_text);
+            }
+
+            addr[i].protocols = protocols;
+            addr[i].protocols_set = 1;
+            addr[i].protocols_changed = 1;
+
+        } else {
+
+            /* the same options */
+
+            if ((lsopt->set && addr[i].protocols_changed)
+                || (addr[i].protocols_set && protocols != addr[i].protocols))
+            {
+                ngx_conf_log_error(NGX_LOG_WARN, cf, 0,
+                                   "protocol options redefined for %V",
+                                   &addr[i].opt.addr_text);
+            }
+
+            addr[i].protocols = protocols;
+            addr[i].protocols_set = 1;
+        }
+
+        addr[i].opt.default_server = default_server;
+        addr[i].opt.proxy_protocol = proxy_protocol;
+#if (NGX_STREAM_SSL)
+        addr[i].opt.ssl = ssl;
+#endif
+
+        return NGX_OK;
     }
 
-found:
+    /* add the address to the addresses list that bound to this port */
+
+    return ngx_stream_add_address(cf, cscf, port, lsopt);
+}
+
+
+/*
+ * add the server address, the server names and the server core module
+ * configurations to the port list
+ */
+
+static ngx_int_t
+ngx_stream_add_address(ngx_conf_t *cf, ngx_stream_core_srv_conf_t *cscf,
+    ngx_stream_conf_port_t *port, ngx_stream_listen_opt_t *lsopt)
+{
+    ngx_stream_conf_addr_t  *addr;
+
+    if (port->addrs.elts == NULL) {
+        if (ngx_array_init(&port->addrs, cf->temp_pool, 4,
+                           sizeof(ngx_stream_conf_addr_t))
+            != NGX_OK)
+        {
+            return NGX_ERROR;
+        }
+    }
 
     addr = ngx_array_push(&port->addrs);
     if (addr == NULL) {
         return NGX_ERROR;
     }
 
-    addr->opt = *listen;
+    addr->opt = *lsopt;
+    addr->protocols = 0;
+    addr->protocols_set = 0;
+    addr->protocols_changed = 0;
+    addr->hash.buckets = NULL;
+    addr->hash.size = 0;
+    addr->wc_head = NULL;
+    addr->wc_tail = NULL;
+#if (NGX_PCRE)
+    addr->nregex = 0;
+    addr->regex = NULL;
+#endif
+    addr->default_server = cscf;
+    addr->servers.elts = NULL;
+
+    return ngx_stream_add_server(cf, cscf, addr);
+}
+
+
+/* add the server core module configuration to the address:port */
+
+static ngx_int_t
+ngx_stream_add_server(ngx_conf_t *cf, ngx_stream_core_srv_conf_t *cscf,
+    ngx_stream_conf_addr_t *addr)
+{
+    ngx_uint_t                    i;
+    ngx_stream_core_srv_conf_t  **server;
+
+    if (addr->servers.elts == NULL) {
+        if (ngx_array_init(&addr->servers, cf->temp_pool, 4,
+                           sizeof(ngx_stream_core_srv_conf_t *))
+            != NGX_OK)
+        {
+            return NGX_ERROR;
+        }
+
+    } else {
+        server = addr->servers.elts;
+        for (i = 0; i < addr->servers.nelts; i++) {
+            if (server[i] == cscf) {
+                ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
+                                   "a duplicate listen %V",
+                                   &addr->opt.addr_text);
+                return NGX_ERROR;
+            }
+        }
+    }
+
+    server = ngx_array_push(&addr->servers);
+    if (server == NULL) {
+        return NGX_ERROR;
+    }
+
+    *server = cscf;
 
     return NGX_OK;
 }
 
 
-static char *
-ngx_stream_optimize_servers(ngx_conf_t *cf, ngx_array_t *ports)
+static ngx_int_t
+ngx_stream_optimize_servers(ngx_conf_t *cf, ngx_stream_core_main_conf_t *cmcf,
+    ngx_array_t *ports)
 {
-    ngx_uint_t                   i, p, last, bind_wildcard;
-    ngx_listening_t             *ls;
-    ngx_stream_port_t           *stport;
-    ngx_stream_conf_port_t      *port;
-    ngx_stream_conf_addr_t      *addr;
-    ngx_stream_core_srv_conf_t  *cscf;
+    ngx_uint_t               p, a;
+    ngx_stream_conf_port_t  *port;
+    ngx_stream_conf_addr_t  *addr;
+
+    if (ports == NULL) {
+        return NGX_OK;
+    }
 
     port = ports->elts;
     for (p = 0; p < ports->nelts; p++) {
@@ -451,103 +681,373 @@ ngx_stream_optimize_servers(ngx_conf_t *cf, ngx_array_t *ports)
         ngx_sort(port[p].addrs.elts, (size_t) port[p].addrs.nelts,
                  sizeof(ngx_stream_conf_addr_t), ngx_stream_cmp_conf_addrs);
 
-        addr = port[p].addrs.elts;
-        last = port[p].addrs.nelts;
-
         /*
-         * if there is the binding to the "*:port" then we need to bind()
-         * to the "*:port" only and ignore the other bindings
+         * check whether all name-based servers have the same
+         * configuration as a default server for given address:port
          */
 
-        if (addr[last - 1].opt.wildcard) {
-            addr[last - 1].opt.bind = 1;
-            bind_wildcard = 1;
+        addr = port[p].addrs.elts;
+        for (a = 0; a < port[p].addrs.nelts; a++) {
 
-        } else {
-            bind_wildcard = 0;
+            if (addr[a].servers.nelts > 1
+#if (NGX_PCRE)
+                || addr[a].default_server->captures
+#endif
+               )
+            {
+                if (ngx_stream_server_names(cf, cmcf, &addr[a]) != NGX_OK) {
+                    return NGX_ERROR;
+                }
+            }
         }
 
-        i = 0;
+        if (ngx_stream_init_listening(cf, &port[p]) != NGX_OK) {
+            return NGX_ERROR;
+        }
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_stream_server_names(ngx_conf_t *cf, ngx_stream_core_main_conf_t *cmcf,
+    ngx_stream_conf_addr_t *addr)
+{
+    ngx_int_t                     rc;
+    ngx_uint_t                    n, s;
+    ngx_hash_init_t               hash;
+    ngx_hash_keys_arrays_t        ha;
+    ngx_stream_server_name_t     *name;
+    ngx_stream_core_srv_conf_t  **cscfp;
+#if (NGX_PCRE)
+    ngx_uint_t                    regex, i;
+
+    regex = 0;
+#endif
+
+    ngx_memzero(&ha, sizeof(ngx_hash_keys_arrays_t));
 
-        while (i < last) {
+    ha.temp_pool = ngx_create_pool(NGX_DEFAULT_POOL_SIZE, cf->log);
+    if (ha.temp_pool == NULL) {
+        return NGX_ERROR;
+    }
+
+    ha.pool = cf->pool;
+
+    if (ngx_hash_keys_array_init(&ha, NGX_HASH_LARGE) != NGX_OK) {
+        goto failed;
+    }
+
+    cscfp = addr->servers.elts;
 
-            if (bind_wildcard && !addr[i].opt.bind) {
-                i++;
+    for (s = 0; s < addr->servers.nelts; s++) {
+
+        name = cscfp[s]->server_names.elts;
+
+        for (n = 0; n < cscfp[s]->server_names.nelts; n++) {
+
+#if (NGX_PCRE)
+            if (name[n].regex) {
+                regex++;
                 continue;
             }
+#endif
 
-            ls = ngx_create_listening(cf, addr[i].opt.sockaddr,
-                                      addr[i].opt.socklen);
-            if (ls == NULL) {
-                return NGX_CONF_ERROR;
+            rc = ngx_hash_add_key(&ha, &name[n].name, name[n].server,
+                                  NGX_HASH_WILDCARD_KEY);
+
+            if (rc == NGX_ERROR) {
+                goto failed;
             }
 
-            ls->addr_ntop = 1;
-            ls->handler = ngx_stream_init_connection;
-            ls->pool_size = 256;
-            ls->type = addr[i].opt.type;
+            if (rc == NGX_DECLINED) {
+                ngx_log_error(NGX_LOG_EMERG, cf->log, 0,
+                              "invalid server name or wildcard \"%V\" on %V",
+                              &name[n].name, &addr->opt.addr_text);
+                goto failed;
+            }
 
-            cscf = addr->opt.ctx->srv_conf[ngx_stream_core_module.ctx_index];
+            if (rc == NGX_BUSY) {
+                ngx_log_error(NGX_LOG_WARN, cf->log, 0,
+                              "conflicting server name \"%V\" on %V, ignored",
+                              &name[n].name, &addr->opt.addr_text);
+            }
+        }
+    }
 
-            ls->logp = cscf->error_log;
-            ls->log.data = &ls->addr_text;
-            ls->log.handler = ngx_accept_log_error;
+    hash.key = ngx_hash_key_lc;
+    hash.max_size = cmcf->server_names_hash_max_size;
+    hash.bucket_size = cmcf->server_names_hash_bucket_size;
+    hash.name = "server_names_hash";
+    hash.pool = cf->pool;
 
-            ls->backlog = addr[i].opt.backlog;
-            ls->rcvbuf = addr[i].opt.rcvbuf;
-            ls->sndbuf = addr[i].opt.sndbuf;
+    if (ha.keys.nelts) {
+        hash.hash = &addr->hash;
+        hash.temp_pool = NULL;
 
-            ls->wildcard = addr[i].opt.wildcard;
+        if (ngx_hash_init(&hash, ha.keys.elts, ha.keys.nelts) != NGX_OK) {
+            goto failed;
+        }
+    }
+
+    if (ha.dns_wc_head.nelts) {
+
+        ngx_qsort(ha.dns_wc_head.elts, (size_t) ha.dns_wc_head.nelts,
+                  sizeof(ngx_hash_key_t), ngx_stream_cmp_dns_wildcards);
+
+        hash.hash = NULL;
+        hash.temp_pool = ha.temp_pool;
+
+        if (ngx_hash_wildcard_init(&hash, ha.dns_wc_head.elts,
+                                   ha.dns_wc_head.nelts)
+            != NGX_OK)
+        {
+            goto failed;
+        }
+
+        addr->wc_head = (ngx_hash_wildcard_t *) hash.hash;
+    }
+
+    if (ha.dns_wc_tail.nelts) {
+
+        ngx_qsort(ha.dns_wc_tail.elts, (size_t) ha.dns_wc_tail.nelts,
+                  sizeof(ngx_hash_key_t), ngx_stream_cmp_dns_wildcards);
+
+        hash.hash = NULL;
+        hash.temp_pool = ha.temp_pool;
+
+        if (ngx_hash_wildcard_init(&hash, ha.dns_wc_tail.elts,
+                                   ha.dns_wc_tail.nelts)
+            != NGX_OK)
+        {
+            goto failed;
+        }
+
+        addr->wc_tail = (ngx_hash_wildcard_t *) hash.hash;
+    }
+
+    ngx_destroy_pool(ha.temp_pool);
+
+#if (NGX_PCRE)
+
+    if (regex == 0) {
+        return NGX_OK;
+    }
+
+    addr->nregex = regex;
+    addr->regex = ngx_palloc(cf->pool,
+                             regex * sizeof(ngx_stream_server_name_t));
+    if (addr->regex == NULL) {
+        return NGX_ERROR;
+    }
+
+    i = 0;
+
+    for (s = 0; s < addr->servers.nelts; s++) {
+
+        name = cscfp[s]->server_names.elts;
+
+        for (n = 0; n < cscfp[s]->server_names.nelts; n++) {
+            if (name[n].regex) {
+                addr->regex[i++] = name[n];
+            }
+        }
+    }
 
-            ls->keepalive = addr[i].opt.so_keepalive;
-#if (NGX_HAVE_KEEPALIVE_TUNABLE)
-            ls->keepidle = addr[i].opt.tcp_keepidle;
-            ls->keepintvl = addr[i].opt.tcp_keepintvl;
-            ls->keepcnt = addr[i].opt.tcp_keepcnt;
 #endif
 
+    return NGX_OK;
+
+failed:
+
+    ngx_destroy_pool(ha.temp_pool);
+
+    return NGX_ERROR;
+}
+
+
+static ngx_int_t
+ngx_stream_cmp_conf_addrs(const void *one, const void *two)
+{
+    ngx_stream_conf_addr_t  *first, *second;
+
+    first = (ngx_stream_conf_addr_t *) one;
+    second = (ngx_stream_conf_addr_t *) two;
+
+    if (first->opt.wildcard) {
+        /* a wildcard address must be the last resort, shift it to the end */
+        return 1;
+    }
+
+    if (second->opt.wildcard) {
+        /* a wildcard address must be the last resort, shift it to the end */
+        return -1;
+    }
+
+    if (first->opt.bind && !second->opt.bind) {
+        /* shift explicit bind()ed addresses to the start */
+        return -1;
+    }
+
+    if (!first->opt.bind && second->opt.bind) {
+        /* shift explicit bind()ed addresses to the start */
+        return 1;
+    }
+
+    /* do not sort by default */
+
+    return 0;
+}
+
+
+static int ngx_libc_cdecl
+ngx_stream_cmp_dns_wildcards(const void *one, const void *two)
+{
+    ngx_hash_key_t  *first, *second;
+
+    first = (ngx_hash_key_t *) one;
+    second = (ngx_hash_key_t *) two;
+
+    return ngx_dns_strcmp(first->key.data, second->key.data);
+}
+
+
+static ngx_int_t
+ngx_stream_init_listening(ngx_conf_t *cf, ngx_stream_conf_port_t *port)
+{
+    ngx_uint_t               i, last, bind_wildcard;
+    ngx_listening_t         *ls;
+    ngx_stream_port_t       *stport;
+    ngx_stream_conf_addr_t  *addr;
+
+    addr = port->addrs.elts;
+    last = port->addrs.nelts;
+
+    /*
+     * If there is a binding to an "*:port" then we need to bind() to
+     * the "*:port" only and ignore other implicit bindings.  The bindings
+     * have been already sorted: explicit bindings are on the start, then
+     * implicit bindings go, and wildcard binding is in the end.
+     */
+
+    if (addr[last - 1].opt.wildcard) {
+        addr[last - 1].opt.bind = 1;
+        bind_wildcard = 1;
+
+    } else {
+        bind_wildcard = 0;
+    }
+
+    i = 0;
+
+    while (i < last) {
+
+        if (bind_wildcard && !addr[i].opt.bind) {
+            i++;
+            continue;
+        }
+
+        ls = ngx_stream_add_listening(cf, &addr[i]);
+        if (ls == NULL) {
+            return NGX_ERROR;
+        }
+
+        stport = ngx_pcalloc(cf->pool, sizeof(ngx_stream_port_t));
+        if (stport == NULL) {
+            return NGX_ERROR;
+        }
+
+        ls->servers = stport;
+
+        stport->naddrs = i + 1;
+
+        switch (ls->sockaddr->sa_family) {
+
 #if (NGX_HAVE_INET6)
-            ls->ipv6only = addr[i].opt.ipv6only;
+        case AF_INET6:
+            if (ngx_stream_add_addrs6(cf, stport, addr) != NGX_OK) {
+                return NGX_ERROR;
+            }
+            break;
 #endif
+        default: /* AF_INET */
+            if (ngx_stream_add_addrs(cf, stport, addr) != NGX_OK) {
+                return NGX_ERROR;
+            }
+            break;
+        }
 
-#if (NGX_HAVE_TCP_FASTOPEN)
-            ls->fastopen = addr[i].opt.fastopen;
+        addr++;
+        last--;
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_listening_t *
+ngx_stream_add_listening(ngx_conf_t *cf, ngx_stream_conf_addr_t *addr)
+{
+    ngx_listening_t             *ls;
+    ngx_stream_core_srv_conf_t  *cscf;
+
+    ls = ngx_create_listening(cf, addr->opt.sockaddr, addr->opt.socklen);
+    if (ls == NULL) {
+        return NULL;
+    }
+
+    ls->addr_ntop = 1;
+
+    ls->handler = ngx_stream_init_connection;
+
+    ls->pool_size = 256;
+
+    cscf = addr->default_server;
+
+    ls->logp = cscf->error_log;
+    ls->log.data = &ls->addr_text;
+    ls->log.handler = ngx_accept_log_error;
+
+    ls->type = addr->opt.type;
+    ls->backlog = addr->opt.backlog;
+    ls->rcvbuf = addr->opt.rcvbuf;
+    ls->sndbuf = addr->opt.sndbuf;
+
+    ls->keepalive = addr->opt.so_keepalive;
+#if (NGX_HAVE_KEEPALIVE_TUNABLE)
+    ls->keepidle = addr->opt.tcp_keepidle;
+    ls->keepintvl = addr->opt.tcp_keepintvl;
+    ls->keepcnt = addr->opt.tcp_keepcnt;
 #endif
 
-#if (NGX_HAVE_REUSEPORT)
-            ls->reuseport = addr[i].opt.reuseport;
+#if (NGX_HAVE_DEFERRED_ACCEPT && defined SO_ACCEPTFILTER)
+    ls->accept_filter = addr->opt.accept_filter;
 #endif
 
-            stport = ngx_palloc(cf->pool, sizeof(ngx_stream_port_t));
-            if (stport == NULL) {
-                return NGX_CONF_ERROR;
-            }
+#if (NGX_HAVE_DEFERRED_ACCEPT && defined TCP_DEFER_ACCEPT)
+    ls->deferred_accept = addr->opt.deferred_accept;
+#endif
 
-            ls->servers = stport;
+#if (NGX_HAVE_INET6)
+    ls->ipv6only = addr->opt.ipv6only;
+#endif
 
-            stport->naddrs = i + 1;
+#if (NGX_HAVE_SETFIB)
+    ls->setfib = addr->opt.setfib;
+#endif
 
-            switch (ls->sockaddr->sa_family) {
-#if (NGX_HAVE_INET6)
-            case AF_INET6:
-                if (ngx_stream_add_addrs6(cf, stport, addr) != NGX_OK) {
-                    return NGX_CONF_ERROR;
-                }
-                break;
+#if (NGX_HAVE_TCP_FASTOPEN)
+    ls->fastopen = addr->opt.fastopen;
 #endif
-            default: /* AF_INET */
-                if (ngx_stream_add_addrs(cf, stport, addr) != NGX_OK) {
-                    return NGX_CONF_ERROR;
-                }
-                break;
-            }
 
-            addr++;
-            last--;
-        }
-    }
+#if (NGX_HAVE_REUSEPORT)
+    ls->reuseport = addr->opt.reuseport;
+#endif
 
-    return NGX_CONF_OK;
+    ls->wildcard = addr->opt.wildcard;
+
+    return ls;
 }
 
 
@@ -555,9 +1055,10 @@ static ngx_int_t
 ngx_stream_add_addrs(ngx_conf_t *cf, ngx_stream_port_t *stport,
     ngx_stream_conf_addr_t *addr)
 {
-    ngx_uint_t             i;
-    struct sockaddr_in    *sin;
-    ngx_stream_in_addr_t  *addrs;
+    ngx_uint_t                   i;
+    struct sockaddr_in          *sin;
+    ngx_stream_in_addr_t        *addrs;
+    ngx_stream_virtual_names_t  *vn;
 
     stport->addrs = ngx_pcalloc(cf->pool,
                                 stport->naddrs * sizeof(ngx_stream_in_addr_t));
@@ -571,13 +1072,39 @@ ngx_stream_add_addrs(ngx_conf_t *cf, ngx_stream_port_t *stport,
 
         sin = (struct sockaddr_in *) addr[i].opt.sockaddr;
         addrs[i].addr = sin->sin_addr.s_addr;
-
-        addrs[i].conf.ctx = addr[i].opt.ctx;
+        addrs[i].conf.default_server = addr[i].default_server;
 #if (NGX_STREAM_SSL)
         addrs[i].conf.ssl = addr[i].opt.ssl;
 #endif
         addrs[i].conf.proxy_protocol = addr[i].opt.proxy_protocol;
-        addrs[i].conf.addr_text = addr[i].opt.addr_text;
+
+        if (addr[i].hash.buckets == NULL
+            && (addr[i].wc_head == NULL
+                || addr[i].wc_head->hash.buckets == NULL)
+            && (addr[i].wc_tail == NULL
+                || addr[i].wc_tail->hash.buckets == NULL)
+#if (NGX_PCRE)
+            && addr[i].nregex == 0
+#endif
+            )
+        {
+            continue;
+        }
+
+        vn = ngx_palloc(cf->pool, sizeof(ngx_stream_virtual_names_t));
+        if (vn == NULL) {
+            return NGX_ERROR;
+        }
+
+        addrs[i].conf.virtual_names = vn;
+
+        vn->names.hash = addr[i].hash;
+        vn->names.wc_head = addr[i].wc_head;
+        vn->names.wc_tail = addr[i].wc_tail;
+#if (NGX_PCRE)
+        vn->nregex = addr[i].nregex;
+        vn->regex = addr[i].regex;
+#endif
     }
 
     return NGX_OK;
@@ -590,9 +1117,10 @@ static ngx_int_t
 ngx_stream_add_addrs6(ngx_conf_t *cf, ngx_stream_port_t *stport,
     ngx_stream_conf_addr_t *addr)
 {
-    ngx_uint_t              i;
-    struct sockaddr_in6    *sin6;
-    ngx_stream_in6_addr_t  *addrs6;
+    ngx_uint_t                   i;
+    struct sockaddr_in6         *sin6;
+    ngx_stream_in6_addr_t       *addrs6;
+    ngx_stream_virtual_names_t  *vn;
 
     stport->addrs = ngx_pcalloc(cf->pool,
                                 stport->naddrs * sizeof(ngx_stream_in6_addr_t));
@@ -606,50 +1134,42 @@ ngx_stream_add_addrs6(ngx_conf_t *cf, ngx_stream_port_t *stport,
 
         sin6 = (struct sockaddr_in6 *) addr[i].opt.sockaddr;
         addrs6[i].addr6 = sin6->sin6_addr;
-
-        addrs6[i].conf.ctx = addr[i].opt.ctx;
+        addrs6[i].conf.default_server = addr[i].default_server;
 #if (NGX_STREAM_SSL)
         addrs6[i].conf.ssl = addr[i].opt.ssl;
 #endif
         addrs6[i].conf.proxy_protocol = addr[i].opt.proxy_protocol;
-        addrs6[i].conf.addr_text = addr[i].opt.addr_text;
-    }
-
-    return NGX_OK;
-}
 
+        if (addr[i].hash.buckets == NULL
+            && (addr[i].wc_head == NULL
+                || addr[i].wc_head->hash.buckets == NULL)
+            && (addr[i].wc_tail == NULL
+                || addr[i].wc_tail->hash.buckets == NULL)
+#if (NGX_PCRE)
+            && addr[i].nregex == 0
 #endif
+            )
+        {
+            continue;
+        }
 
+        vn = ngx_palloc(cf->pool, sizeof(ngx_stream_virtual_names_t));
+        if (vn == NULL) {
+            return NGX_ERROR;
+        }
 
-static ngx_int_t
-ngx_stream_cmp_conf_addrs(const void *one, const void *two)
-{
-    ngx_stream_conf_addr_t  *first, *second;
-
-    first = (ngx_stream_conf_addr_t *) one;
-    second = (ngx_stream_conf_addr_t *) two;
-
-    if (first->opt.wildcard) {
-        /* a wildcard must be the last resort, shift it to the end */
-        return 1;
-    }
-
-    if (second->opt.wildcard) {
-        /* a wildcard must be the last resort, shift it to the end */
-        return -1;
-    }
-
-    if (first->opt.bind && !second->opt.bind) {
-        /* shift explicit bind()ed addresses to the start */
-        return -1;
-    }
+        addrs6[i].conf.virtual_names = vn;
 
-    if (!first->opt.bind && second->opt.bind) {
-        /* shift explicit bind()ed addresses to the start */
-        return 1;
+        vn->names.hash = addr[i].hash;
+        vn->names.wc_head = addr[i].wc_head;
+        vn->names.wc_tail = addr[i].wc_tail;
+#if (NGX_PCRE)
+        vn->nregex = addr[i].nregex;
+        vn->regex = addr[i].regex;
+#endif
     }
 
-    /* do not sort by default */
-
-    return 0;
+    return NGX_OK;
 }
+
+#endif
diff --git a/src/stream/ngx_stream.h b/src/stream/ngx_stream.h
index 46c3622..dc05dc5 100644
--- a/src/stream/ngx_stream.h
+++ b/src/stream/ngx_stream.h
@@ -45,74 +45,39 @@ typedef struct {
     socklen_t                      socklen;
     ngx_str_t                      addr_text;
 
-    /* server ctx */
-    ngx_stream_conf_ctx_t         *ctx;
-
+    unsigned                       set:1;
+    unsigned                       default_server:1;
     unsigned                       bind:1;
     unsigned                       wildcard:1;
     unsigned                       ssl:1;
 #if (NGX_HAVE_INET6)
     unsigned                       ipv6only:1;
 #endif
+    unsigned                       deferred_accept:1;
     unsigned                       reuseport:1;
     unsigned                       so_keepalive:2;
     unsigned                       proxy_protocol:1;
-#if (NGX_HAVE_KEEPALIVE_TUNABLE)
-    int                            tcp_keepidle;
-    int                            tcp_keepintvl;
-    int                            tcp_keepcnt;
-#endif
+
     int                            backlog;
     int                            rcvbuf;
     int                            sndbuf;
+    int                            type;
+#if (NGX_HAVE_SETFIB)
+    int                            setfib;
+#endif
 #if (NGX_HAVE_TCP_FASTOPEN)
     int                            fastopen;
 #endif
-    int                            type;
-} ngx_stream_listen_t;
-
-
-typedef struct {
-    ngx_stream_conf_ctx_t         *ctx;
-    ngx_str_t                      addr_text;
-    unsigned                       ssl:1;
-    unsigned                       proxy_protocol:1;
-} ngx_stream_addr_conf_t;
-
-typedef struct {
-    in_addr_t                      addr;
-    ngx_stream_addr_conf_t         conf;
-} ngx_stream_in_addr_t;
-
-
-#if (NGX_HAVE_INET6)
-
-typedef struct {
-    struct in6_addr                addr6;
-    ngx_stream_addr_conf_t         conf;
-} ngx_stream_in6_addr_t;
-
+#if (NGX_HAVE_KEEPALIVE_TUNABLE)
+    int                            tcp_keepidle;
+    int                            tcp_keepintvl;
+    int                            tcp_keepcnt;
 #endif
 
-
-typedef struct {
-    /* ngx_stream_in_addr_t or ngx_stream_in6_addr_t */
-    void                          *addrs;
-    ngx_uint_t                     naddrs;
-} ngx_stream_port_t;
-
-
-typedef struct {
-    int                            family;
-    int                            type;
-    in_port_t                      port;
-    ngx_array_t                    addrs; /* array of ngx_stream_conf_addr_t */
-} ngx_stream_conf_port_t;
-
-
-typedef struct {
-    ngx_stream_listen_t            opt;
-} ngx_stream_conf_addr_t;
+#if (NGX_HAVE_DEFERRED_ACCEPT && defined SO_ACCEPTFILTER)
+    char                          *accept_filter;
+#endif
+} ngx_stream_listen_opt_t;
 
 
 typedef enum {
@@ -153,7 +118,6 @@ typedef struct {
 
 typedef struct {
     ngx_array_t                    servers;     /* ngx_stream_core_srv_conf_t */
-    ngx_array_t                    listen;      /* ngx_stream_listen_t */
 
     ngx_stream_phase_engine_t      phase_engine;
 
@@ -163,16 +127,24 @@ typedef struct {
     ngx_array_t                    prefix_variables; /* ngx_stream_variable_t */
     ngx_uint_t                     ncaptures;
 
+    ngx_uint_t                     server_names_hash_max_size;
+    ngx_uint_t                     server_names_hash_bucket_size;
+
     ngx_uint_t                     variables_hash_max_size;
     ngx_uint_t                     variables_hash_bucket_size;
 
     ngx_hash_keys_arrays_t        *variables_keys;
 
+    ngx_array_t                   *ports;
+
     ngx_stream_phase_t             phases[NGX_STREAM_LOG_PHASE + 1];
 } ngx_stream_core_main_conf_t;
 
 
 typedef struct {
+    /* array of the ngx_stream_server_name_t, "server_name" directive */
+    ngx_array_t                    server_names;
+
     ngx_stream_content_handler_pt  handler;
 
     ngx_stream_conf_ctx_t         *ctx;
@@ -180,6 +152,8 @@ typedef struct {
     u_char                        *file_name;
     ngx_uint_t                     line;
 
+    ngx_str_t                      server_name;
+
     ngx_flag_t                     tcp_nodelay;
     size_t                         preread_buffer_size;
     ngx_msec_t                     preread_timeout;
@@ -191,10 +165,98 @@ typedef struct {
 
     ngx_msec_t                     proxy_protocol_timeout;
 
-    ngx_uint_t                     listen;  /* unsigned  listen:1; */
+    unsigned                       listen:1;
+#if (NGX_PCRE)
+    unsigned                       captures:1;
+#endif
 } ngx_stream_core_srv_conf_t;
 
 
+/* list of structures to find core_srv_conf quickly at run time */
+
+
+typedef struct {
+#if (NGX_PCRE)
+    ngx_stream_regex_t            *regex;
+#endif
+    ngx_stream_core_srv_conf_t    *server;   /* virtual name server conf */
+    ngx_str_t                      name;
+} ngx_stream_server_name_t;
+
+
+typedef struct {
+    ngx_hash_combined_t            names;
+
+    ngx_uint_t                     nregex;
+    ngx_stream_server_name_t      *regex;
+} ngx_stream_virtual_names_t;
+
+
+typedef struct {
+    /* the default server configuration for this address:port */
+    ngx_stream_core_srv_conf_t    *default_server;
+
+    ngx_stream_virtual_names_t    *virtual_names;
+
+    unsigned                       ssl:1;
+    unsigned                       proxy_protocol:1;
+} ngx_stream_addr_conf_t;
+
+
+typedef struct {
+    in_addr_t                      addr;
+    ngx_stream_addr_conf_t         conf;
+} ngx_stream_in_addr_t;
+
+
+#if (NGX_HAVE_INET6)
+
+typedef struct {
+    struct in6_addr                addr6;
+    ngx_stream_addr_conf_t         conf;
+} ngx_stream_in6_addr_t;
+
+#endif
+
+
+typedef struct {
+    /* ngx_stream_in_addr_t or ngx_stream_in6_addr_t */
+    void                          *addrs;
+    ngx_uint_t                     naddrs;
+} ngx_stream_port_t;
+
+
+typedef struct {
+    int                            family;
+    int                            type;
+    in_port_t                      port;
+    ngx_array_t                    addrs; /* array of ngx_stream_conf_addr_t */
+} ngx_stream_conf_port_t;
+
+
+typedef struct {
+    ngx_stream_listen_opt_t        opt;
+
+    unsigned                       protocols:3;
+    unsigned                       protocols_set:1;
+    unsigned                       protocols_changed:1;
+
+    ngx_hash_t                     hash;
+    ngx_hash_wildcard_t           *wc_head;
+    ngx_hash_wildcard_t           *wc_tail;
+
+#if (NGX_PCRE)
+    ngx_uint_t                     nregex;
+    ngx_stream_server_name_t      *regex;
+#endif
+
+    /* the default server configuration for this address:port */
+    ngx_stream_core_srv_conf_t    *default_server;
+    ngx_array_t                    servers;
+                                      /* array of ngx_stream_core_srv_conf_t */
+} ngx_stream_conf_addr_t;
+
+
 struct ngx_stream_session_s {
     uint32_t                       signature;         /* "STRM" */
 
@@ -210,6 +272,8 @@ struct ngx_stream_session_s {
     void                         **main_conf;
     void                         **srv_conf;
 
+    ngx_stream_virtual_names_t    *virtual_names;
+
     ngx_stream_upstream_t         *upstream;
     ngx_array_t                   *upstream_states;
                                            /* of ngx_stream_upstream_state_t */
@@ -283,6 +347,9 @@ typedef struct {
 #define NGX_STREAM_WRITE_BUFFERED  0x10
 
 
+ngx_int_t ngx_stream_add_listen(ngx_conf_t *cf,
+    ngx_stream_core_srv_conf_t *cscf, ngx_stream_listen_opt_t *lsopt);
+
 void ngx_stream_core_run_phases(ngx_stream_session_t *s);
 ngx_int_t ngx_stream_core_generic_phase(ngx_stream_session_t *s,
     ngx_stream_phase_handler_t *ph);
@@ -291,6 +358,10 @@ ngx_int_t ngx_stream_core_preread_phase(ngx_stream_session_t *s,
 ngx_int_t ngx_stream_core_content_phase(ngx_stream_session_t *s,
     ngx_stream_phase_handler_t *ph);
 
+ngx_int_t ngx_stream_validate_host(ngx_str_t *host, ngx_pool_t *pool,
+    ngx_uint_t alloc);
+ngx_int_t ngx_stream_find_virtual_server(ngx_stream_session_t *s,
+    ngx_str_t *host, ngx_stream_core_srv_conf_t **cscfp);
 
 void ngx_stream_init_connection(ngx_connection_t *c);
 void ngx_stream_session_handler(ngx_event_t *rev);
diff --git a/src/stream/ngx_stream_access_module.c b/src/stream/ngx_stream_access_module.c
index a3020d4..070c226 100644
--- a/src/stream/ngx_stream_access_module.c
+++ b/src/stream/ngx_stream_access_module.c
@@ -144,7 +144,7 @@ ngx_stream_access_handler(ngx_stream_session_t *s)
         p = sin6->sin6_addr.s6_addr;
 
         if (ascf->rules && IN6_IS_ADDR_V4MAPPED(&sin6->sin6_addr)) {
-            addr = p[12] << 24;
+            addr = (in_addr_t) p[12] << 24;
             addr += p[13] << 16;
             addr += p[14] << 8;
             addr += p[15];
diff --git a/src/stream/ngx_stream_core_module.c b/src/stream/ngx_stream_core_module.c
index d96d27a..3093963 100644
--- a/src/stream/ngx_stream_core_module.c
+++ b/src/stream/ngx_stream_core_module.c
@@ -10,6 +10,11 @@
 #include <ngx_stream.h>
 
 
+static ngx_uint_t ngx_stream_preread_can_peek(ngx_connection_t *c);
+static ngx_int_t ngx_stream_preread_peek(ngx_stream_session_t *s,
+    ngx_stream_phase_handler_t *ph);
+static ngx_int_t ngx_stream_preread(ngx_stream_session_t *s,
+    ngx_stream_phase_handler_t *ph);
 static ngx_int_t ngx_stream_core_preconfiguration(ngx_conf_t *cf);
 static void *ngx_stream_core_create_main_conf(ngx_conf_t *cf);
 static char *ngx_stream_core_init_main_conf(ngx_conf_t *cf, void *conf);
@@ -22,6 +27,8 @@ static char *ngx_stream_core_server(ngx_conf_t *cf, ngx_command_t *cmd,
     void *conf);
 static char *ngx_stream_core_listen(ngx_conf_t *cf, ngx_command_t *cmd,
     void *conf);
+static char *ngx_stream_core_server_name(ngx_conf_t *cf, ngx_command_t *cmd,
+    void *conf);
 static char *ngx_stream_core_resolver(ngx_conf_t *cf, ngx_command_t *cmd,
     void *conf);
 
@@ -42,6 +49,20 @@ static ngx_command_t  ngx_stream_core_commands[] = {
       offsetof(ngx_stream_core_main_conf_t, variables_hash_bucket_size),
       NULL },
 
+    { ngx_string("server_names_hash_max_size"),
+      NGX_STREAM_MAIN_CONF|NGX_CONF_TAKE1,
+      ngx_conf_set_num_slot,
+      NGX_STREAM_MAIN_CONF_OFFSET,
+      offsetof(ngx_stream_core_main_conf_t, server_names_hash_max_size),
+      NULL },
+
+    { ngx_string("server_names_hash_bucket_size"),
+      NGX_STREAM_MAIN_CONF|NGX_CONF_TAKE1,
+      ngx_conf_set_num_slot,
+      NGX_STREAM_MAIN_CONF_OFFSET,
+      offsetof(ngx_stream_core_main_conf_t, server_names_hash_bucket_size),
+      NULL },
+
     { ngx_string("server"),
       NGX_STREAM_MAIN_CONF|NGX_CONF_BLOCK|NGX_CONF_NOARGS,
       ngx_stream_core_server,
@@ -56,6 +77,13 @@ static ngx_command_t  ngx_stream_core_commands[] = {
       0,
       NULL },
 
+    { ngx_string("server_name"),
+      NGX_STREAM_SRV_CONF|NGX_CONF_1MORE,
+      ngx_stream_core_server_name,
+      NGX_STREAM_SRV_CONF_OFFSET,
+      0,
+      NULL },
+
     { ngx_string("error_log"),
       NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_1MORE,
       ngx_stream_core_error_log,
@@ -203,8 +231,6 @@ ngx_int_t
 ngx_stream_core_preread_phase(ngx_stream_session_t *s,
     ngx_stream_phase_handler_t *ph)
 {
-    size_t                       size;
-    ssize_t                      n;
     ngx_int_t                    rc;
     ngx_connection_t            *c;
     ngx_stream_core_srv_conf_t  *cscf;
@@ -217,57 +243,34 @@ ngx_stream_core_preread_phase(ngx_stream_session_t *s,
 
     if (c->read->timedout) {
         rc = NGX_STREAM_OK;
-
-    } else if (c->read->timer_set) {
-        rc = NGX_AGAIN;
-
-    } else {
-        rc = ph->handler(s);
+        goto done;
     }
 
-    while (rc == NGX_AGAIN) {
-
-        if (c->buffer == NULL) {
-            c->buffer = ngx_create_temp_buf(c->pool, cscf->preread_buffer_size);
-            if (c->buffer == NULL) {
-                rc = NGX_ERROR;
-                break;
-            }
-        }
-
-        size = c->buffer->end - c->buffer->last;
-
-        if (size == 0) {
-            ngx_log_error(NGX_LOG_ERR, c->log, 0, "preread buffer full");
-            rc = NGX_STREAM_BAD_REQUEST;
-            break;
-        }
-
-        if (c->read->eof) {
-            rc = NGX_STREAM_OK;
-            break;
-        }
-
-        if (!c->read->ready) {
-            break;
-        }
-
-        n = c->recv(c, c->buffer->last, size);
+    if (!c->read->timer_set) {
+        rc = ph->handler(s);
 
-        if (n == NGX_ERROR || n == 0) {
-            rc = NGX_STREAM_OK;
-            break;
+        if (rc != NGX_AGAIN) {
+            goto done;
         }
+    }
 
-        if (n == NGX_AGAIN) {
-            break;
+    if (c->buffer == NULL) {
+        c->buffer = ngx_create_temp_buf(c->pool, cscf->preread_buffer_size);
+        if (c->buffer == NULL) {
+            rc = NGX_ERROR;
+            goto done;
         }
+    }
 
-        c->buffer->last += n;
+    if (ngx_stream_preread_can_peek(c)) {
+        rc = ngx_stream_preread_peek(s, ph);
 
-        rc = ph->handler(s);
+    } else {
+        rc = ngx_stream_preread(s, ph);
     }
 
+done:
+
     if (rc == NGX_AGAIN) {
         if (ngx_handle_read_event(c->read, 0) != NGX_OK) {
             ngx_stream_finalize_session(s, NGX_STREAM_INTERNAL_SERVER_ERROR);
@@ -311,6 +314,129 @@ ngx_stream_core_preread_phase(ngx_stream_session_t *s,
 }
 
 
+static ngx_uint_t
+ngx_stream_preread_can_peek(ngx_connection_t *c)
+{
+#if (NGX_STREAM_SSL)
+    if (c->ssl) {
+        return 0;
+    }
+#endif
+
+    if ((ngx_event_flags & NGX_USE_CLEAR_EVENT) == 0) {
+        return 0;
+    }
+
+#if (NGX_HAVE_KQUEUE)
+    if (ngx_event_flags & NGX_USE_KQUEUE_EVENT) {
+        return 1;
+    }
+#endif
+
+#if (NGX_HAVE_EPOLLRDHUP)
+    if ((ngx_event_flags & NGX_USE_EPOLL_EVENT) && ngx_use_epoll_rdhup) {
+        return 1;
+    }
+#endif
+
+    return 0;
+}
+
+
+static ngx_int_t
+ngx_stream_preread_peek(ngx_stream_session_t *s, ngx_stream_phase_handler_t *ph)
+{
+    ssize_t            n;
+    ngx_int_t          rc;
+    ngx_err_t          err;
+    ngx_connection_t  *c;
+
+    c = s->connection;
+
+    n = recv(c->fd, (char *) c->buffer->last,
+             c->buffer->end - c->buffer->last, MSG_PEEK);
+
+    err = ngx_socket_errno;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_STREAM, c->log, 0, "stream recv(): %z", n);
+
+    if (n == -1) {
+        if (err == NGX_EAGAIN) {
+            c->read->ready = 0;
+            return NGX_AGAIN;
+        }
+
+        ngx_connection_error(c, err, "recv() failed");
+        return NGX_STREAM_OK;
+    }
+
+    if (n == 0) {
+        return NGX_STREAM_OK;
+    }
+
+    c->buffer->last += n;
+
+    rc = ph->handler(s);
+
+    if (rc != NGX_AGAIN) {
+        c->buffer->last = c->buffer->pos;
+        return rc;
+    }
+
+    if (c->buffer->last == c->buffer->end) {
+        ngx_log_error(NGX_LOG_ERR, c->log, 0, "preread buffer full");
+        return NGX_STREAM_BAD_REQUEST;
+    }
+
+    if (c->read->pending_eof) {
+        return NGX_STREAM_OK;
+    }
+
+    c->buffer->last = c->buffer->pos;
+
+    return NGX_AGAIN;
+}
+
+
+static ngx_int_t
+ngx_stream_preread(ngx_stream_session_t *s, ngx_stream_phase_handler_t *ph)
+{
+    ssize_t            n;
+    ngx_int_t          rc;
+    ngx_connection_t  *c;
+
+    c = s->connection;
+
+    while (c->read->ready) {
+
+        n = c->recv(c, c->buffer->last, c->buffer->end - c->buffer->last);
+
+        if (n == NGX_AGAIN) {
+            return NGX_AGAIN;
+        }
+
+        if (n == NGX_ERROR || n == 0) {
+            return NGX_STREAM_OK;
+        }
+
+        c->buffer->last += n;
+
+        rc = ph->handler(s);
+
+        if (rc != NGX_AGAIN) {
+            return rc;
+        }
+
+        if (c->buffer->last == c->buffer->end) {
+            ngx_log_error(NGX_LOG_ERR, c->log, 0, "preread buffer full");
+            return NGX_STREAM_BAD_REQUEST;
+        }
+    }
+
+    return NGX_AGAIN;
+}
+
+
 ngx_int_t
 ngx_stream_core_content_phase(ngx_stream_session_t *s,
     ngx_stream_phase_handler_t *ph)
@@ -338,6 +464,149 @@ ngx_stream_core_content_phase(ngx_stream_session_t *s,
 }
 
 
+ngx_int_t
+ngx_stream_validate_host(ngx_str_t *host, ngx_pool_t *pool, ngx_uint_t alloc)
+{
+    u_char  *h, ch;
+    size_t   i, dot_pos, host_len;
+
+    enum {
+        sw_usual = 0,
+        sw_literal,
+        sw_rest
+    } state;
+
+    dot_pos = host->len;
+    host_len = host->len;
+
+    h = host->data;
+
+    state = sw_usual;
+
+    for (i = 0; i < host->len; i++) {
+        ch = h[i];
+
+        switch (ch) {
+
+        case '.':
+            if (dot_pos == i - 1) {
+                return NGX_DECLINED;
+            }
+            dot_pos = i;
+            break;
+
+        case ':':
+            if (state == sw_usual) {
+                host_len = i;
+                state = sw_rest;
+            }
+            break;
+
+        case '[':
+            if (i == 0) {
+                state = sw_literal;
+            }
+            break;
+
+        case ']':
+            if (state == sw_literal) {
+                host_len = i + 1;
+                state = sw_rest;
+            }
+            break;
+
+        default:
+
+            if (ngx_path_separator(ch)) {
+                return NGX_DECLINED;
+            }
+
+            if (ch <= 0x20 || ch == 0x7f) {
+                return NGX_DECLINED;
+            }
+
+            if (ch >= 'A' && ch <= 'Z') {
+                alloc = 1;
+            }
+
+            break;
+        }
+    }
+
+    if (dot_pos == host_len - 1) {
+        host_len--;
+    }
+
+    if (host_len == 0) {
+        return NGX_DECLINED;
+    }
+
+    if (alloc) {
+        host->data = ngx_pnalloc(pool, host_len);
+        if (host->data == NULL) {
+            return NGX_ERROR;
+        }
+
+        ngx_strlow(host->data, h, host_len);
+    }
+
+    host->len = host_len;
+
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_stream_find_virtual_server(ngx_stream_session_t *s,
+    ngx_str_t *host, ngx_stream_core_srv_conf_t **cscfp)
+{
+    ngx_stream_core_srv_conf_t  *cscf;
+
+    if (s->virtual_names == NULL) {
+        return NGX_DECLINED;
+    }
+
+    cscf = ngx_hash_find_combined(&s->virtual_names->names,
+                                  ngx_hash_key(host->data, host->len),
+                                  host->data, host->len);
+
+    if (cscf) {
+        *cscfp = cscf;
+        return NGX_OK;
+    }
+
+#if (NGX_PCRE)
+
+    if (host->len && s->virtual_names->nregex) {
+        ngx_int_t                  n;
+        ngx_uint_t                 i;
+        ngx_stream_server_name_t  *sn;
+
+        sn = s->virtual_names->regex;
+
+        for (i = 0; i < s->virtual_names->nregex; i++) {
+
+            n = ngx_stream_regex_exec(s, sn[i].regex, host);
+
+            if (n == NGX_DECLINED) {
+                continue;
+            }
+
+            if (n == NGX_OK) {
+                *cscfp = sn[i].server;
+                return NGX_OK;
+            }
+
+            return NGX_ERROR;
+        }
+    }
+
+#endif /* NGX_PCRE */
+
+    return NGX_DECLINED;
+}
+
+
 static ngx_int_t
 ngx_stream_core_preconfiguration(ngx_conf_t *cf)
 {
@@ -362,11 +631,8 @@ ngx_stream_core_create_main_conf(ngx_conf_t *cf)
         return NULL;
     }
 
-    if (ngx_array_init(&cmcf->listen, cf->pool, 4, sizeof(ngx_stream_listen_t))
-        != NGX_OK)
-    {
-        return NULL;
-    }
+    cmcf->server_names_hash_max_size = NGX_CONF_UNSET_UINT;
+    cmcf->server_names_hash_bucket_size = NGX_CONF_UNSET_UINT;
 
     cmcf->variables_hash_max_size = NGX_CONF_UNSET_UINT;
     cmcf->variables_hash_bucket_size = NGX_CONF_UNSET_UINT;
@@ -380,6 +646,14 @@ ngx_stream_core_init_main_conf(ngx_conf_t *cf, void *conf)
 {
     ngx_stream_core_main_conf_t *cmcf = conf;
 
+    ngx_conf_init_uint_value(cmcf->server_names_hash_max_size, 512);
+    ngx_conf_init_uint_value(cmcf->server_names_hash_bucket_size,
+                             ngx_cacheline_size);
+
+    cmcf->server_names_hash_bucket_size =
+            ngx_align(cmcf->server_names_hash_bucket_size, ngx_cacheline_size);
+
+
     ngx_conf_init_uint_value(cmcf->variables_hash_max_size, 1024);
     ngx_conf_init_uint_value(cmcf->variables_hash_bucket_size, 64);
 
@@ -411,6 +685,13 @@ ngx_stream_core_create_srv_conf(ngx_conf_t *cf)
      *     cscf->error_log = NULL;
      */
 
+    if (ngx_array_init(&cscf->server_names, cf->temp_pool, 4,
+                       sizeof(ngx_stream_server_name_t))
+        != NGX_OK)
+    {
+        return NULL;
+    }
+
     cscf->file_name = cf->conf_file->file.name.data;
     cscf->line = cf->conf_file->line;
     cscf->resolver_timeout = NGX_CONF_UNSET_MSEC;
@@ -429,6 +710,9 @@ ngx_stream_core_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child)
     ngx_stream_core_srv_conf_t *prev = parent;
     ngx_stream_core_srv_conf_t *conf = child;
 
+    ngx_str_t                  name;
+    ngx_stream_server_name_t  *sn;
+
     ngx_conf_merge_msec_value(conf->resolver_timeout,
                               prev->resolver_timeout, 30000);
 
@@ -476,6 +760,37 @@ ngx_stream_core_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child)
     ngx_conf_merge_msec_value(conf->preread_timeout,
                               prev->preread_timeout, 30000);
 
+    if (conf->server_names.nelts == 0) {
+        /* the array has 4 empty preallocated elements, so push cannot fail */
+        sn = ngx_array_push(&conf->server_names);
+#if (NGX_PCRE)
+        sn->regex = NULL;
+#endif
+        sn->server = conf;
+        ngx_str_set(&sn->name, "");
+    }
+
+    sn = conf->server_names.elts;
+    name = sn[0].name;
+
+#if (NGX_PCRE)
+    if (sn->regex) {
+        name.len++;
+        name.data--;
+    } else
+#endif
+
+    if (name.data[0] == '.') {
+        name.len--;
+        name.data++;
+    }
+
+    conf->server_name.len = name.len;
+    conf->server_name.data = ngx_pstrdup(cf->pool, &name);
+    if (conf->server_name.data == NULL) {
+        return NGX_CONF_ERROR;
+    }
+
     return NGX_CONF_OK;
 }
 
@@ -575,11 +890,10 @@ ngx_stream_core_listen(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
 {
     ngx_stream_core_srv_conf_t  *cscf = conf;
 
-    ngx_str_t                    *value, size;
-    ngx_url_t                     u;
-    ngx_uint_t                    i, n, backlog;
-    ngx_stream_listen_t          *ls, *als;
-    ngx_stream_core_main_conf_t  *cmcf;
+    ngx_str_t                *value, size;
+    ngx_url_t                 u;
+    ngx_uint_t                n, i, backlog;
+    ngx_stream_listen_opt_t   lsopt;
 
     cscf->listen = 1;
 
@@ -600,51 +914,67 @@ ngx_stream_core_listen(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
         return NGX_CONF_ERROR;
     }
 
-    cmcf = ngx_stream_conf_get_module_main_conf(cf, ngx_stream_core_module);
-
-    ls = ngx_array_push_n(&cmcf->listen, u.naddrs);
-    if (ls == NULL) {
-        return NGX_CONF_ERROR;
-    }
-
-    ngx_memzero(ls, sizeof(ngx_stream_listen_t));
-
-    ls->backlog = NGX_LISTEN_BACKLOG;
-    ls->rcvbuf = -1;
-    ls->sndbuf = -1;
-    ls->type = SOCK_STREAM;
-    ls->ctx = cf->ctx;
+    ngx_memzero(&lsopt, sizeof(ngx_stream_listen_opt_t));
 
+    lsopt.backlog = NGX_LISTEN_BACKLOG;
+    lsopt.type = SOCK_STREAM;
+    lsopt.rcvbuf = -1;
+    lsopt.sndbuf = -1;
+#if (NGX_HAVE_SETFIB)
+    lsopt.setfib = -1;
+#endif
 #if (NGX_HAVE_TCP_FASTOPEN)
-    ls->fastopen = -1;
+    lsopt.fastopen = -1;
 #endif
-
 #if (NGX_HAVE_INET6)
-    ls->ipv6only = 1;
+    lsopt.ipv6only = 1;
 #endif
 
     backlog = 0;
 
     for (i = 2; i < cf->args->nelts; i++) {
 
+        if (ngx_strcmp(value[i].data, "default_server") == 0) {
+            lsopt.default_server = 1;
+            continue;
+        }
+
 #if !(NGX_WIN32)
         if (ngx_strcmp(value[i].data, "udp") == 0) {
-            ls->type = SOCK_DGRAM;
+            lsopt.type = SOCK_DGRAM;
             continue;
         }
 #endif
 
         if (ngx_strcmp(value[i].data, "bind") == 0) {
-            ls->bind = 1;
+            lsopt.set = 1;
+            lsopt.bind = 1;
+            continue;
+        }
+
+#if (NGX_HAVE_SETFIB)
+        if (ngx_strncmp(value[i].data, "setfib=", 7) == 0) {
+            lsopt.setfib = ngx_atoi(value[i].data + 7, value[i].len - 7);
+            lsopt.set = 1;
+            lsopt.bind = 1;
+
+            if (lsopt.setfib == NGX_ERROR) {
+                ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
+                                   "invalid setfib \"%V\"", &value[i]);
+                return NGX_CONF_ERROR;
+            }
+
             continue;
         }
+#endif
 
 #if (NGX_HAVE_TCP_FASTOPEN)
         if (ngx_strncmp(value[i].data, "fastopen=", 9) == 0) {
-            ls->fastopen = ngx_atoi(value[i].data + 9, value[i].len - 9);
-            ls->bind = 1;
+            lsopt.fastopen = ngx_atoi(value[i].data + 9, value[i].len - 9);
+            lsopt.set = 1;
+            lsopt.bind = 1;
 
-            if (ls->fastopen == NGX_ERROR) {
+            if (lsopt.fastopen == NGX_ERROR) {
                 ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                                    "invalid fastopen \"%V\"", &value[i]);
                 return NGX_CONF_ERROR;
@@ -655,10 +985,11 @@ ngx_stream_core_listen(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
 #endif
 
         if (ngx_strncmp(value[i].data, "backlog=", 8) == 0) {
-            ls->backlog = ngx_atoi(value[i].data + 8, value[i].len - 8);
-            ls->bind = 1;
+            lsopt.backlog = ngx_atoi(value[i].data + 8, value[i].len - 8);
+            lsopt.set = 1;
+            lsopt.bind = 1;
 
-            if (ls->backlog == NGX_ERROR || ls->backlog == 0) {
+            if (lsopt.backlog == NGX_ERROR || lsopt.backlog == 0) {
                 ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                                    "invalid backlog \"%V\"", &value[i]);
                 return NGX_CONF_ERROR;
@@ -673,10 +1004,11 @@ ngx_stream_core_listen(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
             size.len = value[i].len - 7;
             size.data = value[i].data + 7;
 
-            ls->rcvbuf = ngx_parse_size(&size);
-            ls->bind = 1;
+            lsopt.rcvbuf = ngx_parse_size(&size);
+            lsopt.set = 1;
+            lsopt.bind = 1;
 
-            if (ls->rcvbuf == NGX_ERROR) {
+            if (lsopt.rcvbuf == NGX_ERROR) {
                 ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                                    "invalid rcvbuf \"%V\"", &value[i]);
                 return NGX_CONF_ERROR;
@@ -689,10 +1021,11 @@ ngx_stream_core_listen(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
             size.len = value[i].len - 7;
             size.data = value[i].data + 7;
 
-            ls->sndbuf = ngx_parse_size(&size);
-            ls->bind = 1;
+            lsopt.sndbuf = ngx_parse_size(&size);
+            lsopt.set = 1;
+            lsopt.bind = 1;
 
-            if (ls->sndbuf == NGX_ERROR) {
+            if (lsopt.sndbuf == NGX_ERROR) {
                 ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                                    "invalid sndbuf \"%V\"", &value[i]);
                 return NGX_CONF_ERROR;
@@ -701,13 +1034,40 @@ ngx_stream_core_listen(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
             continue;
         }
 
+        if (ngx_strncmp(value[i].data, "accept_filter=", 14) == 0) {
+#if (NGX_HAVE_DEFERRED_ACCEPT && defined SO_ACCEPTFILTER)
+            lsopt.accept_filter = (char *) &value[i].data[14];
+            lsopt.set = 1;
+            lsopt.bind = 1;
+#else
+            ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
+                               "accept filters \"%V\" are not supported "
+                               "on this platform, ignored",
+                               &value[i]);
+#endif
+            continue;
+        }
+
+        if (ngx_strcmp(value[i].data, "deferred") == 0) {
+#if (NGX_HAVE_DEFERRED_ACCEPT && defined TCP_DEFER_ACCEPT)
+            lsopt.deferred_accept = 1;
+            lsopt.set = 1;
+            lsopt.bind = 1;
+#else
+            ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
+                               "the deferred accept is not supported "
+                               "on this platform, ignored");
+#endif
+            continue;
+        }
+
         if (ngx_strncmp(value[i].data, "ipv6only=o", 10) == 0) {
 #if (NGX_HAVE_INET6 && defined IPV6_V6ONLY)
             if (ngx_strcmp(&value[i].data[10], "n") == 0) {
-                ls->ipv6only = 1;
+                lsopt.ipv6only = 1;
 
             } else if (ngx_strcmp(&value[i].data[10], "ff") == 0) {
-                ls->ipv6only = 0;
+                lsopt.ipv6only = 0;
 
             } else {
                 ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
@@ -716,11 +1076,13 @@ ngx_stream_core_listen(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
                 return NGX_CONF_ERROR;
             }
 
-            ls->bind = 1;
+            lsopt.set = 1;
+            lsopt.bind = 1;
+
             continue;
 #else
             ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
-                               "bind ipv6only is not supported "
+                               "ipv6only is not supported "
                                "on this platform");
             return NGX_CONF_ERROR;
 #endif
@@ -728,8 +1090,9 @@ ngx_stream_core_listen(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
 
         if (ngx_strcmp(value[i].data, "reuseport") == 0) {
 #if (NGX_HAVE_REUSEPORT)
-            ls->reuseport = 1;
-            ls->bind = 1;
+            lsopt.reuseport = 1;
+            lsopt.set = 1;
+            lsopt.bind = 1;
 #else
             ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                                "reuseport is not supported "
@@ -740,17 +1103,7 @@ ngx_stream_core_listen(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
 
         if (ngx_strcmp(value[i].data, "ssl") == 0) {
 #if (NGX_STREAM_SSL)
-            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->ssl = 1;
-
+            lsopt.ssl = 1;
             continue;
 #else
             ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
@@ -763,10 +1116,10 @@ ngx_stream_core_listen(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
         if (ngx_strncmp(value[i].data, "so_keepalive=", 13) == 0) {
 
             if (ngx_strcmp(&value[i].data[13], "on") == 0) {
-                ls->so_keepalive = 1;
+                lsopt.so_keepalive = 1;
 
             } else if (ngx_strcmp(&value[i].data[13], "off") == 0) {
-                ls->so_keepalive = 2;
+                lsopt.so_keepalive = 2;
 
             } else {
 
@@ -785,8 +1138,8 @@ ngx_stream_core_listen(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
                 if (p > s.data) {
                     s.len = p - s.data;
 
-                    ls->tcp_keepidle = ngx_parse_time(&s, 1);
-                    if (ls->tcp_keepidle == (time_t) NGX_ERROR) {
+                    lsopt.tcp_keepidle = ngx_parse_time(&s, 1);
+                    if (lsopt.tcp_keepidle == (time_t) NGX_ERROR) {
                         goto invalid_so_keepalive;
                     }
                 }
@@ -801,8 +1154,8 @@ ngx_stream_core_listen(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
                 if (p > s.data) {
                     s.len = p - s.data;
 
-                    ls->tcp_keepintvl = ngx_parse_time(&s, 1);
-                    if (ls->tcp_keepintvl == (time_t) NGX_ERROR) {
+                    lsopt.tcp_keepintvl = ngx_parse_time(&s, 1);
+                    if (lsopt.tcp_keepintvl == (time_t) NGX_ERROR) {
                         goto invalid_so_keepalive;
                     }
                 }
@@ -812,19 +1165,19 @@ ngx_stream_core_listen(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
                 if (s.data < end) {
                     s.len = end - s.data;
 
-                    ls->tcp_keepcnt = ngx_atoi(s.data, s.len);
-                    if (ls->tcp_keepcnt == NGX_ERROR) {
+                    lsopt.tcp_keepcnt = ngx_atoi(s.data, s.len);
+                    if (lsopt.tcp_keepcnt == NGX_ERROR) {
                         goto invalid_so_keepalive;
                     }
                 }
 
-                if (ls->tcp_keepidle == 0 && ls->tcp_keepintvl == 0
-                    && ls->tcp_keepcnt == 0)
+                if (lsopt.tcp_keepidle == 0 && lsopt.tcp_keepintvl == 0
+                    && lsopt.tcp_keepcnt == 0)
                 {
                     goto invalid_so_keepalive;
                 }
 
-                ls->so_keepalive = 1;
+                lsopt.so_keepalive = 1;
 
 #else
 
@@ -836,7 +1189,8 @@ ngx_stream_core_listen(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
 #endif
             }
 
-            ls->bind = 1;
+            lsopt.set = 1;
+            lsopt.bind = 1;
 
             continue;
 
@@ -851,68 +1205,176 @@ ngx_stream_core_listen(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
         }
 
         if (ngx_strcmp(value[i].data, "proxy_protocol") == 0) {
-            ls->proxy_protocol = 1;
+            lsopt.proxy_protocol = 1;
             continue;
         }
 
         ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
-                           "the invalid \"%V\" parameter", &value[i]);
+                           "invalid parameter \"%V\"", &value[i]);
         return NGX_CONF_ERROR;
     }
 
-    if (ls->type == SOCK_DGRAM) {
+    if (lsopt.type == SOCK_DGRAM) {
+#if (NGX_HAVE_TCP_FASTOPEN)
+        if (lsopt.fastopen != -1) {
+            return "\"fastopen\" parameter is incompatible with \"udp\"";
+        }
+#endif
+
         if (backlog) {
             return "\"backlog\" parameter is incompatible with \"udp\"";
         }
 
+#if (NGX_HAVE_DEFERRED_ACCEPT && defined SO_ACCEPTFILTER)
+        if (lsopt.accept_filter) {
+            return "\"accept_filter\" parameter is incompatible with \"udp\"";
+        }
+#endif
+
+#if (NGX_HAVE_DEFERRED_ACCEPT && defined TCP_DEFER_ACCEPT)
+        if (lsopt.deferred_accept) {
+            return "\"deferred\" parameter is incompatible with \"udp\"";
+        }
+#endif
+
 #if (NGX_STREAM_SSL)
-        if (ls->ssl) {
+        if (lsopt.ssl) {
             return "\"ssl\" parameter is incompatible with \"udp\"";
         }
 #endif
 
-        if (ls->so_keepalive) {
+        if (lsopt.so_keepalive) {
             return "\"so_keepalive\" parameter is incompatible with \"udp\"";
         }
 
-        if (ls->proxy_protocol) {
+        if (lsopt.proxy_protocol) {
             return "\"proxy_protocol\" parameter is incompatible with \"udp\"";
         }
+    }
 
-#if (NGX_HAVE_TCP_FASTOPEN)
-        if (ls->fastopen != -1) {
-            return "\"fastopen\" parameter is incompatible with \"udp\"";
+    for (n = 0; n < u.naddrs; n++) {
+
+        for (i = 0; i < n; i++) {
+            if (ngx_cmp_sockaddr(u.addrs[n].sockaddr, u.addrs[n].socklen,
+                                 u.addrs[i].sockaddr, u.addrs[i].socklen, 1)
+                == NGX_OK)
+            {
+                goto next;
+            }
         }
-#endif
+
+        lsopt.sockaddr = u.addrs[n].sockaddr;
+        lsopt.socklen = u.addrs[n].socklen;
+        lsopt.addr_text = u.addrs[n].name;
+        lsopt.wildcard = ngx_inet_wildcard(lsopt.sockaddr);
+
+        if (ngx_stream_add_listen(cf, cscf, &lsopt) != NGX_OK) {
+            return NGX_CONF_ERROR;
+        }
+
+    next:
+        continue;
     }
 
-    als = cmcf->listen.elts;
+    return NGX_CONF_OK;
+}
 
-    for (n = 0; n < u.naddrs; n++) {
-        ls[n] = ls[0];
 
-        ls[n].sockaddr = u.addrs[n].sockaddr;
-        ls[n].socklen = u.addrs[n].socklen;
-        ls[n].addr_text = u.addrs[n].name;
-        ls[n].wildcard = ngx_inet_wildcard(ls[n].sockaddr);
+static char *
+ngx_stream_core_server_name(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
+{
+    ngx_stream_core_srv_conf_t *cscf = conf;
 
-        for (i = 0; i < cmcf->listen.nelts - u.naddrs + n; i++) {
-            if (ls[n].type != als[i].type) {
-                continue;
-            }
+    u_char                     ch;
+    ngx_str_t                 *value;
+    ngx_uint_t                 i;
+    ngx_stream_server_name_t  *sn;
 
-            if (ngx_cmp_sockaddr(als[i].sockaddr, als[i].socklen,
-                                 ls[n].sockaddr, ls[n].socklen, 1)
-                != NGX_OK)
-            {
-                continue;
-            }
+    value = cf->args->elts;
+
+    for (i = 1; i < cf->args->nelts; i++) {
+
+        ch = value[i].data[0];
+
+        if ((ch == '*' && (value[i].len < 3 || value[i].data[1] != '.'))
+            || (ch == '.' && value[i].len < 2))
+        {
+            ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
+                               "server name \"%V\" is invalid", &value[i]);
+            return NGX_CONF_ERROR;
+        }
+
+        if (ngx_strchr(value[i].data, '/')) {
+            ngx_conf_log_error(NGX_LOG_WARN, cf, 0,
+                               "server name \"%V\" has suspicious symbols",
+                               &value[i]);
+        }
+
+        sn = ngx_array_push(&cscf->server_names);
+        if (sn == NULL) {
+            return NGX_CONF_ERROR;
+        }
+
+#if (NGX_PCRE)
+        sn->regex = NULL;
+#endif
+        sn->server = cscf;
+
+        if (ngx_strcasecmp(value[i].data, (u_char *) "$hostname") == 0) {
+            sn->name = cf->cycle->hostname;
+
+        } else {
+            sn->name = value[i];
+        }
+
+        if (value[i].data[0] != '~') {
+            ngx_strlow(sn->name.data, sn->name.data, sn->name.len);
+            continue;
+        }
+
+#if (NGX_PCRE)
+        {
+        u_char               *p;
+        ngx_regex_compile_t   rc;
+        u_char                errstr[NGX_MAX_CONF_ERRSTR];
 
+        if (value[i].len == 1) {
             ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
-                               "duplicate \"%V\" address and port pair",
-                               &ls[n].addr_text);
+                               "empty regex in server name \"%V\"", &value[i]);
+            return NGX_CONF_ERROR;
+        }
+
+        value[i].len--;
+        value[i].data++;
+
+        ngx_memzero(&rc, sizeof(ngx_regex_compile_t));
+
+        rc.pattern = value[i];
+        rc.err.len = NGX_MAX_CONF_ERRSTR;
+        rc.err.data = errstr;
+
+        for (p = value[i].data; p < value[i].data + value[i].len; p++) {
+            if (*p >= 'A' && *p <= 'Z') {
+                rc.options = NGX_REGEX_CASELESS;
+                break;
+            }
+        }
+
+        sn->regex = ngx_stream_regex_compile(cf, &rc);
+        if (sn->regex == NULL) {
             return NGX_CONF_ERROR;
         }
+
+        sn->name = value[i];
+        cscf->captures = (rc.captures > 0);
+        }
+#else
+        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
+                           "using regex \"%V\" "
+                           "requires PCRE library", &value[i]);
+
+        return NGX_CONF_ERROR;
+#endif
     }
 
     return NGX_CONF_OK;
diff --git a/src/stream/ngx_stream_geo_module.c b/src/stream/ngx_stream_geo_module.c
index 4b4cad8..2324bef 100644
--- a/src/stream/ngx_stream_geo_module.c
+++ b/src/stream/ngx_stream_geo_module.c
@@ -190,7 +190,7 @@ ngx_stream_geo_cidr_variable(ngx_stream_session_t *s,
         p = inaddr6->s6_addr;
 
         if (IN6_IS_ADDR_V4MAPPED(inaddr6)) {
-            inaddr = p[12] << 24;
+            inaddr = (in_addr_t) p[12] << 24;
             inaddr += p[13] << 16;
             inaddr += p[14] << 8;
             inaddr += p[15];
@@ -263,7 +263,7 @@ ngx_stream_geo_range_variable(ngx_stream_session_t *s,
             if (IN6_IS_ADDR_V4MAPPED(inaddr6)) {
                 p = inaddr6->s6_addr;
 
-                inaddr = p[12] << 24;
+                inaddr = (in_addr_t) p[12] << 24;
                 inaddr += p[13] << 16;
                 inaddr += p[14] << 8;
                 inaddr += p[15];
@@ -1209,7 +1209,7 @@ ngx_stream_geo_value(ngx_conf_t *cf, ngx_stream_geo_conf_ctx_t *ctx,
         return gvvn->value;
     }
 
-    val = ngx_palloc(ctx->pool, sizeof(ngx_stream_variable_value_t));
+    val = ngx_pcalloc(ctx->pool, sizeof(ngx_stream_variable_value_t));
     if (val == NULL) {
         return NULL;
     }
@@ -1221,8 +1221,6 @@ ngx_stream_geo_value(ngx_conf_t *cf, ngx_stream_geo_conf_ctx_t *ctx,
     }
 
     val->valid = 1;
-    val->no_cacheable = 0;
-    val->not_found = 0;
 
     gvvn = ngx_palloc(ctx->temp_pool,
                       sizeof(ngx_stream_geo_variable_value_node_t));
diff --git a/src/stream/ngx_stream_geoip_module.c b/src/stream/ngx_stream_geoip_module.c
index 6507b71..3ee8f0e 100644
--- a/src/stream/ngx_stream_geoip_module.c
+++ b/src/stream/ngx_stream_geoip_module.c
@@ -236,7 +236,7 @@ ngx_stream_geoip_addr(ngx_stream_session_t *s, ngx_stream_geoip_conf_t *gcf)
         if (IN6_IS_ADDR_V4MAPPED(inaddr6)) {
             p = inaddr6->s6_addr;
 
-            inaddr = p[12] << 24;
+            inaddr = (in_addr_t) p[12] << 24;
             inaddr += p[13] << 16;
             inaddr += p[14] << 8;
             inaddr += p[15];
diff --git a/src/stream/ngx_stream_handler.c b/src/stream/ngx_stream_handler.c
index 669b6a1..a7ffc6e 100644
--- a/src/stream/ngx_stream_handler.c
+++ b/src/stream/ngx_stream_handler.c
@@ -30,6 +30,7 @@ ngx_stream_init_connection(ngx_connection_t *c)
     struct sockaddr_in           *sin;
     ngx_stream_in_addr_t         *addr;
     ngx_stream_session_t         *s;
+    ngx_stream_conf_ctx_t        *ctx;
     ngx_stream_addr_conf_t       *addr_conf;
 #if (NGX_HAVE_INET6)
     struct sockaddr_in6          *sin6;
@@ -121,9 +122,12 @@ ngx_stream_init_connection(ngx_connection_t *c)
         return;
     }
 
+    ctx = addr_conf->default_server->ctx;
+
     s->signature = NGX_STREAM_MODULE;
-    s->main_conf = addr_conf->ctx->main_conf;
-    s->srv_conf = addr_conf->ctx->srv_conf;
+    s->main_conf = ctx->main_conf;
+    s->srv_conf = ctx->srv_conf;
+    s->virtual_names = addr_conf->virtual_names;
 
 #if (NGX_STREAM_SSL)
     s->ssl = addr_conf->ssl;
@@ -144,7 +148,7 @@ ngx_stream_init_connection(ngx_connection_t *c)
 
     ngx_log_error(NGX_LOG_INFO, c->log, 0, "*%uA %sclient %*s connected to %V",
                   c->number, c->type == SOCK_DGRAM ? "udp " : "",
-                  len, text, &addr_conf->addr_text);
+                  len, text, &c->listening->addr_text);
 
     c->log->connection = c->number;
     c->log->handler = ngx_stream_log_error;
diff --git a/src/stream/ngx_stream_pass_module.c b/src/stream/ngx_stream_pass_module.c
new file mode 100644
index 0000000..2c1c60c
--- /dev/null
+++ b/src/stream/ngx_stream_pass_module.c
@@ -0,0 +1,327 @@
+
+/*
+ * Copyright (C) Roman Arutyunyan
+ * Copyright (C) Nginx, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_stream.h>
+
+
+#define NGX_STREAM_PASS_MAX_PASSES  10
+
+
+typedef struct {
+    ngx_addr_t                  *addr;
+    ngx_stream_complex_value_t  *addr_value;
+} ngx_stream_pass_srv_conf_t;
+
+
+static void ngx_stream_pass_handler(ngx_stream_session_t *s);
+static ngx_int_t ngx_stream_pass_check_cycle(ngx_connection_t *c);
+static void ngx_stream_pass_cleanup(void *data);
+static ngx_int_t ngx_stream_pass_match(ngx_listening_t *ls, ngx_addr_t *addr);
+static void *ngx_stream_pass_create_srv_conf(ngx_conf_t *cf);
+static char *ngx_stream_pass(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
+
+
+static ngx_command_t  ngx_stream_pass_commands[] = {
+
+    { ngx_string("pass"),
+      NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1,
+      ngx_stream_pass,
+      NGX_STREAM_SRV_CONF_OFFSET,
+      0,
+      NULL },
+
+      ngx_null_command
+};
+
+
+static ngx_stream_module_t  ngx_stream_pass_module_ctx = {
+    NULL,                                  /* preconfiguration */
+    NULL,                                  /* postconfiguration */
+
+    NULL,                                  /* create main configuration */
+    NULL,                                  /* init main configuration */
+
+    ngx_stream_pass_create_srv_conf,       /* create server configuration */
+    NULL                                   /* merge server configuration */
+};
+
+
+ngx_module_t  ngx_stream_pass_module = {
+    NGX_MODULE_V1,
+    &ngx_stream_pass_module_ctx,           /* module context */
+    ngx_stream_pass_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 void
+ngx_stream_pass_handler(ngx_stream_session_t *s)
+{
+    ngx_url_t                    u;
+    ngx_str_t                    url;
+    ngx_addr_t                  *addr;
+    ngx_uint_t                   i;
+    ngx_listening_t             *ls;
+    ngx_connection_t            *c;
+    ngx_stream_pass_srv_conf_t  *pscf;
+
+    c = s->connection;
+
+    c->log->action = "passing connection to port";
+
+    if (c->buffer && c->buffer->pos != c->buffer->last) {
+        ngx_log_error(NGX_LOG_ERR, c->log, 0,
+                      "cannot pass connection with preread data");
+        goto failed;
+    }
+
+    pscf = ngx_stream_get_module_srv_conf(s, ngx_stream_pass_module);
+
+    addr = pscf->addr;
+
+    if (addr == NULL) {
+        if (ngx_stream_complex_value(s, pscf->addr_value, &url) != NGX_OK) {
+            goto failed;
+        }
+
+        ngx_memzero(&u, sizeof(ngx_url_t));
+
+        u.url = url;
+        u.no_resolve = 1;
+
+        if (ngx_parse_url(c->pool, &u) != NGX_OK) {
+            if (u.err) {
+                ngx_log_error(NGX_LOG_ERR, c->log, 0,
+                              "%s in pass \"%V\"", u.err, &u.url);
+            }
+
+            goto failed;
+        }
+
+        if (u.naddrs == 0) {
+            ngx_log_error(NGX_LOG_ERR, c->log, 0,
+                          "no addresses in pass \"%V\"", &u.url);
+            goto failed;
+        }
+
+        if (u.no_port) {
+            ngx_log_error(NGX_LOG_ERR, c->log, 0,
+                          "no port in pass \"%V\"", &u.url);
+            goto failed;
+        }
+
+        addr = &u.addrs[0];
+    }
+
+    ngx_log_debug1(NGX_LOG_DEBUG_STREAM, c->log, 0,
+                   "stream pass addr: \"%V\"", &addr->name);
+
+    if (ngx_stream_pass_check_cycle(c) != NGX_OK) {
+        goto failed;
+    }
+
+    ls = ngx_cycle->listening.elts;
+
+    for (i = 0; i < ngx_cycle->listening.nelts; i++) {
+
+        if (ngx_stream_pass_match(&ls[i], addr) != NGX_OK) {
+            continue;
+        }
+
+        c->listening = &ls[i];
+
+        c->data = NULL;
+        c->buffer = NULL;
+
+        *c->log = c->listening->log;
+        c->log->handler = NULL;
+        c->log->data = NULL;
+
+        c->local_sockaddr = addr->sockaddr;
+        c->local_socklen = addr->socklen;
+
+        c->listening->handler(c);
+
+        return;
+    }
+
+    ngx_log_error(NGX_LOG_ERR, c->log, 0,
+                  "port not found for \"%V\"", &addr->name);
+
+    ngx_stream_finalize_session(s, NGX_STREAM_OK);
+
+    return;
+
+failed:
+
+    ngx_stream_finalize_session(s, NGX_STREAM_INTERNAL_SERVER_ERROR);
+}
+
+
+static ngx_int_t
+ngx_stream_pass_check_cycle(ngx_connection_t *c)
+{
+    ngx_uint_t          *num;
+    ngx_pool_cleanup_t  *cln;
+
+    for (cln = c->pool->cleanup; cln; cln = cln->next) {
+        if (cln->handler != ngx_stream_pass_cleanup) {
+            continue;
+        }
+
+        num = cln->data;
+
+        if (++(*num) > NGX_STREAM_PASS_MAX_PASSES) {
+            ngx_log_error(NGX_LOG_ERR, c->log, 0, "stream pass cycle");
+            return NGX_ERROR;
+        }
+
+        return NGX_OK;
+    }
+
+    cln = ngx_pool_cleanup_add(c->pool, sizeof(ngx_uint_t));
+    if (cln == NULL) {
+        return NGX_ERROR;
+    }
+
+    cln->handler = ngx_stream_pass_cleanup;
+
+    num = cln->data;
+    *num = 1;
+
+    return NGX_OK;
+}
+
+
+static void
+ngx_stream_pass_cleanup(void *data)
+{
+    return;
+}
+
+
+static ngx_int_t
+ngx_stream_pass_match(ngx_listening_t *ls, ngx_addr_t *addr)
+{
+    if (!ls->wildcard) {
+        return ngx_cmp_sockaddr(ls->sockaddr, ls->socklen,
+                                addr->sockaddr, addr->socklen, 1);
+    }
+
+    if (ls->sockaddr->sa_family == addr->sockaddr->sa_family
+        && ngx_inet_get_port(ls->sockaddr) == ngx_inet_get_port(addr->sockaddr))
+    {
+        return NGX_OK;
+    }
+
+    return NGX_DECLINED;
+}
+
+
+static void *
+ngx_stream_pass_create_srv_conf(ngx_conf_t *cf)
+{
+    ngx_stream_pass_srv_conf_t  *conf;
+
+    conf = ngx_pcalloc(cf->pool, sizeof(ngx_stream_pass_srv_conf_t));
+    if (conf == NULL) {
+        return NULL;
+    }
+
+    /*
+     * set by ngx_pcalloc():
+     *
+     *     conf->addr = NULL;
+     *     conf->addr_value = NULL;
+     */
+
+    return conf;
+}
+
+
+static char *
+ngx_stream_pass(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
+{
+    ngx_stream_pass_srv_conf_t *pscf = conf;
+
+    ngx_url_t                            u;
+    ngx_str_t                           *value, *url;
+    ngx_stream_complex_value_t           cv;
+    ngx_stream_core_srv_conf_t          *cscf;
+    ngx_stream_compile_complex_value_t   ccv;
+
+    if (pscf->addr || pscf->addr_value) {
+        return "is duplicate";
+    }
+
+    cscf = ngx_stream_conf_get_module_srv_conf(cf, ngx_stream_core_module);
+
+    cscf->handler = ngx_stream_pass_handler;
+
+    value = cf->args->elts;
+
+    url = &value[1];
+
+    ngx_memzero(&ccv, sizeof(ngx_stream_compile_complex_value_t));
+
+    ccv.cf = cf;
+    ccv.value = url;
+    ccv.complex_value = &cv;
+
+    if (ngx_stream_compile_complex_value(&ccv) != NGX_OK) {
+        return NGX_CONF_ERROR;
+    }
+
+    if (cv.lengths) {
+        pscf->addr_value = ngx_palloc(cf->pool,
+                                      sizeof(ngx_stream_complex_value_t));
+        if (pscf->addr_value == NULL) {
+            return NGX_CONF_ERROR;
+        }
+
+        *pscf->addr_value = cv;
+
+        return NGX_CONF_OK;
+    }
+
+    ngx_memzero(&u, sizeof(ngx_url_t));
+
+    u.url = *url;
+    u.no_resolve = 1;
+
+    if (ngx_parse_url(cf->pool, &u) != NGX_OK) {
+        if (u.err) {
+            ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
+                               "%s in \"%V\" of the \"pass\" directive",
+                               u.err, &u.url);
+        }
+
+        return NGX_CONF_ERROR;
+    }
+
+    if (u.naddrs == 0) {
+        return "has no addresses";
+    }
+
+    if (u.no_port) {
+        return "has no port";
+    }
+
+    pscf->addr = &u.addrs[0];
+
+    return NGX_CONF_OK;
+}
diff --git a/src/stream/ngx_stream_proxy_module.c b/src/stream/ngx_stream_proxy_module.c
index 934e7d8..ed275c0 100644
--- a/src/stream/ngx_stream_proxy_module.c
+++ b/src/stream/ngx_stream_proxy_module.c
@@ -103,6 +103,8 @@ static void ngx_stream_proxy_ssl_handshake(ngx_connection_t *pc);
 static void ngx_stream_proxy_ssl_save_session(ngx_connection_t *c);
 static ngx_int_t ngx_stream_proxy_ssl_name(ngx_stream_session_t *s);
 static ngx_int_t ngx_stream_proxy_ssl_certificate(ngx_stream_session_t *s);
+static ngx_int_t ngx_stream_proxy_merge_ssl(ngx_conf_t *cf,
+    ngx_stream_proxy_srv_conf_t *conf, ngx_stream_proxy_srv_conf_t *prev);
 static ngx_int_t ngx_stream_proxy_set_ssl(ngx_conf_t *cf,
     ngx_stream_proxy_srv_conf_t *pscf);
 
@@ -801,7 +803,7 @@ ngx_stream_proxy_init_upstream(ngx_stream_session_t *s)
 
 #if (NGX_STREAM_SSL)
 
-    if (pc->type == SOCK_STREAM && pscf->ssl) {
+    if (pc->type == SOCK_STREAM && pscf->ssl_enable) {
 
         if (u->proxy_protocol) {
             if (ngx_stream_proxy_send_proxy_protocol(s) != NGX_OK) {
@@ -892,7 +894,7 @@ ngx_stream_proxy_init_upstream(ngx_stream_session_t *s)
             return;
         }
 
-        p = ngx_pnalloc(c->pool, NGX_PROXY_PROTOCOL_MAX_HEADER);
+        p = ngx_pnalloc(c->pool, NGX_PROXY_PROTOCOL_V1_MAX_HEADER);
         if (p == NULL) {
             ngx_stream_proxy_finalize(s, NGX_STREAM_INTERNAL_SERVER_ERROR);
             return;
@@ -900,7 +902,8 @@ ngx_stream_proxy_init_upstream(ngx_stream_session_t *s)
 
         cl->buf->pos = p;
 
-        p = ngx_proxy_protocol_write(c, p, p + NGX_PROXY_PROTOCOL_MAX_HEADER);
+        p = ngx_proxy_protocol_write(c, p,
+                                     p + NGX_PROXY_PROTOCOL_V1_MAX_HEADER);
         if (p == NULL) {
             ngx_stream_proxy_finalize(s, NGX_STREAM_INTERNAL_SERVER_ERROR);
             return;
@@ -944,14 +947,15 @@ ngx_stream_proxy_send_proxy_protocol(ngx_stream_session_t *s)
     ngx_connection_t             *c, *pc;
     ngx_stream_upstream_t        *u;
     ngx_stream_proxy_srv_conf_t  *pscf;
-    u_char                        buf[NGX_PROXY_PROTOCOL_MAX_HEADER];
+    u_char                        buf[NGX_PROXY_PROTOCOL_V1_MAX_HEADER];
 
     c = s->connection;
 
     ngx_log_debug0(NGX_LOG_DEBUG_STREAM, c->log, 0,
                    "stream proxy send PROXY protocol header");
 
-    p = ngx_proxy_protocol_write(c, buf, buf + NGX_PROXY_PROTOCOL_MAX_HEADER);
+    p = ngx_proxy_protocol_write(c, buf,
+                                 buf + NGX_PROXY_PROTOCOL_V1_MAX_HEADER);
     if (p == NULL) {
         ngx_stream_proxy_finalize(s, NGX_STREAM_INTERNAL_SERVER_ERROR);
         return NGX_ERROR;
@@ -1069,8 +1073,10 @@ ngx_stream_proxy_ssl_init_connection(ngx_stream_session_t *s)
         }
     }
 
-    if (pscf->ssl_certificate && (pscf->ssl_certificate->lengths
-                                  || pscf->ssl_certificate_key->lengths))
+    if (pscf->ssl_certificate
+        && pscf->ssl_certificate->value.len
+        && (pscf->ssl_certificate->lengths
+            || pscf->ssl_certificate_key->lengths))
     {
         if (ngx_stream_proxy_ssl_certificate(s) != NGX_OK) {
             ngx_stream_proxy_finalize(s, NGX_STREAM_INTERNAL_SERVER_ERROR);
@@ -1669,9 +1675,8 @@ ngx_stream_proxy_process(ngx_stream_session_t *s, ngx_uint_t from_upstream,
 
         size = b->end - b->last;
 
-        if (size && src->read->ready && !src->read->delayed
-            && !src->read->error)
-        {
+        if (size && src->read->ready && !src->read->delayed) {
+
             if (limit_rate) {
                 limit = (off_t) limit_rate * (ngx_time() - u->start_sec + 1)
                         - *received;
@@ -1735,7 +1740,7 @@ ngx_stream_proxy_process(ngx_stream_session_t *s, ngx_uint_t from_upstream,
 
                 cl->buf->temporary = (n ? 1 : 0);
                 cl->buf->last_buf = src->read->eof;
-                cl->buf->flush = 1;
+                cl->buf->flush = !src->read->eof;
 
                 (*packets)++;
                 *received += n;
@@ -2148,14 +2153,19 @@ ngx_stream_proxy_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child)
 
 #if (NGX_STREAM_SSL)
 
+    if (ngx_stream_proxy_merge_ssl(cf, conf, prev) != NGX_OK) {
+        return NGX_CONF_ERROR;
+    }
+
     ngx_conf_merge_value(conf->ssl_enable, prev->ssl_enable, 0);
 
     ngx_conf_merge_value(conf->ssl_session_reuse,
                               prev->ssl_session_reuse, 1);
 
     ngx_conf_merge_bitmask_value(conf->ssl_protocols, prev->ssl_protocols,
-                              (NGX_CONF_BITMASK_SET|NGX_SSL_TLSv1
-                               |NGX_SSL_TLSv1_1|NGX_SSL_TLSv1_2));
+                              (NGX_CONF_BITMASK_SET
+                               |NGX_SSL_TLSv1|NGX_SSL_TLSv1_1
+                               |NGX_SSL_TLSv1_2|NGX_SSL_TLSv1_3));
 
     ngx_conf_merge_str_value(conf->ssl_ciphers, prev->ssl_ciphers, "DEFAULT");
 
@@ -2197,16 +2207,62 @@ ngx_stream_proxy_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child)
 #if (NGX_STREAM_SSL)
 
 static ngx_int_t
-ngx_stream_proxy_set_ssl(ngx_conf_t *cf, ngx_stream_proxy_srv_conf_t *pscf)
+ngx_stream_proxy_merge_ssl(ngx_conf_t *cf, ngx_stream_proxy_srv_conf_t *conf,
+    ngx_stream_proxy_srv_conf_t *prev)
 {
-    ngx_pool_cleanup_t  *cln;
+    ngx_uint_t  preserve;
+
+    if (conf->ssl_protocols == 0
+        && conf->ssl_ciphers.data == NULL
+        && conf->ssl_certificate == NGX_CONF_UNSET_PTR
+        && conf->ssl_certificate_key == NGX_CONF_UNSET_PTR
+        && conf->ssl_passwords == NGX_CONF_UNSET_PTR
+        && conf->ssl_verify == NGX_CONF_UNSET
+        && conf->ssl_verify_depth == NGX_CONF_UNSET_UINT
+        && conf->ssl_trusted_certificate.data == NULL
+        && conf->ssl_crl.data == NULL
+        && conf->ssl_session_reuse == NGX_CONF_UNSET
+        && conf->ssl_conf_commands == NGX_CONF_UNSET_PTR)
+    {
+        if (prev->ssl) {
+            conf->ssl = prev->ssl;
+            return NGX_OK;
+        }
+
+        preserve = 1;
+
+    } else {
+        preserve = 0;
+    }
 
-    pscf->ssl = ngx_pcalloc(cf->pool, sizeof(ngx_ssl_t));
-    if (pscf->ssl == NULL) {
+    conf->ssl = ngx_pcalloc(cf->pool, sizeof(ngx_ssl_t));
+    if (conf->ssl == NULL) {
         return NGX_ERROR;
     }
 
-    pscf->ssl->log = cf->log;
+    conf->ssl->log = cf->log;
+
+    /*
+     * special handling to preserve conf->ssl
+     * in the "stream" section to inherit it to all servers
+     */
+
+    if (preserve) {
+        prev->ssl = conf->ssl;
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_stream_proxy_set_ssl(ngx_conf_t *cf, ngx_stream_proxy_srv_conf_t *pscf)
+{
+    ngx_pool_cleanup_t  *cln;
+
+    if (pscf->ssl->ctx) {
+        return NGX_OK;
+    }
 
     if (ngx_ssl_create(pscf->ssl, pscf->ssl_protocols, NULL) != NGX_OK) {
         return NGX_ERROR;
@@ -2225,8 +2281,9 @@ ngx_stream_proxy_set_ssl(ngx_conf_t *cf, ngx_stream_proxy_srv_conf_t *pscf)
         return NGX_ERROR;
     }
 
-    if (pscf->ssl_certificate) {
-
+    if (pscf->ssl_certificate
+        && pscf->ssl_certificate->value.len)
+    {
         if (pscf->ssl_certificate_key == NULL) {
             ngx_log_error(NGX_LOG_EMERG, cf->log, 0,
                           "no \"proxy_ssl_certificate_key\" is defined "
diff --git a/src/stream/ngx_stream_ssl_module.c b/src/stream/ngx_stream_ssl_module.c
index c530832..6dee106 100644
--- a/src/stream/ngx_stream_ssl_module.c
+++ b/src/stream/ngx_stream_ssl_module.c
@@ -40,12 +40,12 @@ static ngx_int_t ngx_stream_ssl_variable(ngx_stream_session_t *s,
     ngx_stream_variable_value_t *v, uintptr_t data);
 
 static ngx_int_t ngx_stream_ssl_add_variables(ngx_conf_t *cf);
-static void *ngx_stream_ssl_create_conf(ngx_conf_t *cf);
-static char *ngx_stream_ssl_merge_conf(ngx_conf_t *cf, void *parent,
+static void *ngx_stream_ssl_create_srv_conf(ngx_conf_t *cf);
+static char *ngx_stream_ssl_merge_srv_conf(ngx_conf_t *cf, void *parent,
     void *child);
 
 static ngx_int_t ngx_stream_ssl_compile_certificates(ngx_conf_t *cf,
-    ngx_stream_ssl_conf_t *conf);
+    ngx_stream_ssl_srv_conf_t *conf);
 
 static char *ngx_stream_ssl_password_file(ngx_conf_t *cf, ngx_command_t *cmd,
     void *conf);
@@ -90,21 +90,21 @@ static ngx_command_t  ngx_stream_ssl_commands[] = {
       NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1,
       ngx_conf_set_msec_slot,
       NGX_STREAM_SRV_CONF_OFFSET,
-      offsetof(ngx_stream_ssl_conf_t, handshake_timeout),
+      offsetof(ngx_stream_ssl_srv_conf_t, handshake_timeout),
       NULL },
 
     { ngx_string("ssl_certificate"),
       NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1,
       ngx_conf_set_str_array_slot,
       NGX_STREAM_SRV_CONF_OFFSET,
-      offsetof(ngx_stream_ssl_conf_t, certificates),
+      offsetof(ngx_stream_ssl_srv_conf_t, certificates),
       NULL },
 
     { ngx_string("ssl_certificate_key"),
       NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1,
       ngx_conf_set_str_array_slot,
       NGX_STREAM_SRV_CONF_OFFSET,
-      offsetof(ngx_stream_ssl_conf_t, certificate_keys),
+      offsetof(ngx_stream_ssl_srv_conf_t, certificate_keys),
       NULL },
 
     { ngx_string("ssl_password_file"),
@@ -118,63 +118,63 @@ static ngx_command_t  ngx_stream_ssl_commands[] = {
       NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1,
       ngx_conf_set_str_slot,
       NGX_STREAM_SRV_CONF_OFFSET,
-      offsetof(ngx_stream_ssl_conf_t, dhparam),
+      offsetof(ngx_stream_ssl_srv_conf_t, dhparam),
       NULL },
 
     { ngx_string("ssl_ecdh_curve"),
       NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1,
       ngx_conf_set_str_slot,
       NGX_STREAM_SRV_CONF_OFFSET,
-      offsetof(ngx_stream_ssl_conf_t, ecdh_curve),
+      offsetof(ngx_stream_ssl_srv_conf_t, ecdh_curve),
       NULL },
 
     { ngx_string("ssl_protocols"),
       NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_1MORE,
       ngx_conf_set_bitmask_slot,
       NGX_STREAM_SRV_CONF_OFFSET,
-      offsetof(ngx_stream_ssl_conf_t, protocols),
+      offsetof(ngx_stream_ssl_srv_conf_t, protocols),
       &ngx_stream_ssl_protocols },
 
     { ngx_string("ssl_ciphers"),
       NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1,
       ngx_conf_set_str_slot,
       NGX_STREAM_SRV_CONF_OFFSET,
-      offsetof(ngx_stream_ssl_conf_t, ciphers),
+      offsetof(ngx_stream_ssl_srv_conf_t, ciphers),
       NULL },
 
     { ngx_string("ssl_verify_client"),
       NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1,
       ngx_conf_set_enum_slot,
       NGX_STREAM_SRV_CONF_OFFSET,
-      offsetof(ngx_stream_ssl_conf_t, verify),
+      offsetof(ngx_stream_ssl_srv_conf_t, verify),
       &ngx_stream_ssl_verify },
 
     { ngx_string("ssl_verify_depth"),
       NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1,
       ngx_conf_set_num_slot,
       NGX_STREAM_SRV_CONF_OFFSET,
-      offsetof(ngx_stream_ssl_conf_t, verify_depth),
+      offsetof(ngx_stream_ssl_srv_conf_t, verify_depth),
       NULL },
 
     { ngx_string("ssl_client_certificate"),
       NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1,
       ngx_conf_set_str_slot,
       NGX_STREAM_SRV_CONF_OFFSET,
-      offsetof(ngx_stream_ssl_conf_t, client_certificate),
+      offsetof(ngx_stream_ssl_srv_conf_t, client_certificate),
       NULL },
 
     { ngx_string("ssl_trusted_certificate"),
       NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1,
       ngx_conf_set_str_slot,
       NGX_STREAM_SRV_CONF_OFFSET,
-      offsetof(ngx_stream_ssl_conf_t, trusted_certificate),
+      offsetof(ngx_stream_ssl_srv_conf_t, trusted_certificate),
       NULL },
 
     { ngx_string("ssl_prefer_server_ciphers"),
       NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_FLAG,
       ngx_conf_set_flag_slot,
       NGX_STREAM_SRV_CONF_OFFSET,
-      offsetof(ngx_stream_ssl_conf_t, prefer_server_ciphers),
+      offsetof(ngx_stream_ssl_srv_conf_t, prefer_server_ciphers),
       NULL },
 
     { ngx_string("ssl_session_cache"),
@@ -188,37 +188,44 @@ static ngx_command_t  ngx_stream_ssl_commands[] = {
       NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_FLAG,
       ngx_conf_set_flag_slot,
       NGX_STREAM_SRV_CONF_OFFSET,
-      offsetof(ngx_stream_ssl_conf_t, session_tickets),
+      offsetof(ngx_stream_ssl_srv_conf_t, session_tickets),
       NULL },
 
     { ngx_string("ssl_session_ticket_key"),
       NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1,
       ngx_conf_set_str_array_slot,
       NGX_STREAM_SRV_CONF_OFFSET,
-      offsetof(ngx_stream_ssl_conf_t, session_ticket_keys),
+      offsetof(ngx_stream_ssl_srv_conf_t, session_ticket_keys),
       NULL },
 
     { ngx_string("ssl_session_timeout"),
       NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1,
       ngx_conf_set_sec_slot,
       NGX_STREAM_SRV_CONF_OFFSET,
-      offsetof(ngx_stream_ssl_conf_t, session_timeout),
+      offsetof(ngx_stream_ssl_srv_conf_t, session_timeout),
       NULL },
 
     { ngx_string("ssl_crl"),
       NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1,
       ngx_conf_set_str_slot,
       NGX_STREAM_SRV_CONF_OFFSET,
-      offsetof(ngx_stream_ssl_conf_t, crl),
+      offsetof(ngx_stream_ssl_srv_conf_t, crl),
       NULL },
 
     { ngx_string("ssl_conf_command"),
       NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE2,
       ngx_conf_set_keyval_slot,
       NGX_STREAM_SRV_CONF_OFFSET,
-      offsetof(ngx_stream_ssl_conf_t, conf_commands),
+      offsetof(ngx_stream_ssl_srv_conf_t, conf_commands),
       &ngx_stream_ssl_conf_command_post },
 
+    { ngx_string("ssl_reject_handshake"),
+      NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_FLAG,
+      ngx_conf_set_flag_slot,
+      NGX_STREAM_SRV_CONF_OFFSET,
+      offsetof(ngx_stream_ssl_srv_conf_t, reject_handshake),
+      NULL },
+
     { ngx_string("ssl_alpn"),
       NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_1MORE,
       ngx_stream_ssl_alpn,
@@ -237,8 +244,8 @@ static ngx_stream_module_t  ngx_stream_ssl_module_ctx = {
     NULL,                                  /* create main configuration */
     NULL,                                  /* init main configuration */
 
-    ngx_stream_ssl_create_conf,            /* create server configuration */
-    ngx_stream_ssl_merge_conf              /* merge server configuration */
+    ngx_stream_ssl_create_srv_conf,        /* create server configuration */
+    ngx_stream_ssl_merge_srv_conf          /* merge server configuration */
 };
 
 
@@ -332,11 +339,11 @@ static ngx_str_t ngx_stream_ssl_sess_id_ctx = ngx_string("STREAM");
 static ngx_int_t
 ngx_stream_ssl_handler(ngx_stream_session_t *s)
 {
-    long                    rc;
-    X509                   *cert;
-    ngx_int_t               rv;
-    ngx_connection_t       *c;
-    ngx_stream_ssl_conf_t  *sslcf;
+    long                        rc;
+    X509                       *cert;
+    ngx_int_t                   rv;
+    ngx_connection_t           *c;
+    ngx_stream_ssl_srv_conf_t  *sscf;
 
     if (!s->ssl) {
         return NGX_OK;
@@ -344,23 +351,23 @@ ngx_stream_ssl_handler(ngx_stream_session_t *s)
 
     c = s->connection;
 
-    sslcf = ngx_stream_get_module_srv_conf(s, ngx_stream_ssl_module);
+    sscf = ngx_stream_get_module_srv_conf(s, ngx_stream_ssl_module);
 
     if (c->ssl == NULL) {
         c->log->action = "SSL handshaking";
 
-        rv = ngx_stream_ssl_init_connection(&sslcf->ssl, c);
+        rv = ngx_stream_ssl_init_connection(&sscf->ssl, c);
 
         if (rv != NGX_OK) {
             return rv;
         }
     }
 
-    if (sslcf->verify) {
+    if (sscf->verify) {
         rc = SSL_get_verify_result(c->ssl->connection);
 
         if (rc != X509_V_OK
-            && (sslcf->verify != 3 || !ngx_ssl_verify_error_optional(rc)))
+            && (sscf->verify != 3 || !ngx_ssl_verify_error_optional(rc)))
         {
             ngx_log_error(NGX_LOG_INFO, c->log, 0,
                           "client SSL certificate verify error: (%l:%s)",
@@ -371,7 +378,7 @@ ngx_stream_ssl_handler(ngx_stream_session_t *s)
             return NGX_ERROR;
         }
 
-        if (sslcf->verify == 1) {
+        if (sscf->verify == 1) {
             cert = SSL_get_peer_certificate(c->ssl->connection);
 
             if (cert == NULL) {
@@ -396,7 +403,7 @@ ngx_stream_ssl_init_connection(ngx_ssl_t *ssl, ngx_connection_t *c)
 {
     ngx_int_t                    rc;
     ngx_stream_session_t        *s;
-    ngx_stream_ssl_conf_t       *sslcf;
+    ngx_stream_ssl_srv_conf_t   *sscf;
     ngx_stream_core_srv_conf_t  *cscf;
 
     s = c->data;
@@ -418,9 +425,9 @@ ngx_stream_ssl_init_connection(ngx_ssl_t *ssl, ngx_connection_t *c)
     }
 
     if (rc == NGX_AGAIN) {
-        sslcf = ngx_stream_get_module_srv_conf(s, ngx_stream_ssl_module);
+        sscf = ngx_stream_get_module_srv_conf(s, ngx_stream_ssl_module);
 
-        ngx_add_timer(c->read, sslcf->handshake_timeout);
+        ngx_add_timer(c->read, sscf->handshake_timeout);
 
         c->ssl->handler = ngx_stream_ssl_handshake_handler;
 
@@ -458,7 +465,135 @@ ngx_stream_ssl_handshake_handler(ngx_connection_t *c)
 static int
 ngx_stream_ssl_servername(ngx_ssl_conn_t *ssl_conn, int *ad, void *arg)
 {
+    ngx_int_t                    rc;
+    ngx_str_t                    host;
+    const char                  *servername;
+    ngx_connection_t            *c;
+    ngx_stream_session_t        *s;
+    ngx_stream_ssl_srv_conf_t   *sscf;
+    ngx_stream_core_srv_conf_t  *cscf;
+
+    c = ngx_ssl_get_connection(ssl_conn);
+
+    if (c->ssl->handshaked) {
+        *ad = SSL_AD_NO_RENEGOTIATION;
+        return SSL_TLSEXT_ERR_ALERT_FATAL;
+    }
+
+    s = c->data;
+
+    servername = SSL_get_servername(ssl_conn, TLSEXT_NAMETYPE_host_name);
+
+    if (servername == NULL) {
+        ngx_log_debug0(NGX_LOG_DEBUG_STREAM, c->log, 0,
+                       "SSL server name: null");
+        goto done;
+    }
+
+    ngx_log_debug1(NGX_LOG_DEBUG_STREAM, c->log, 0,
+                   "SSL server name: \"%s\"", servername);
+
+    host.len = ngx_strlen(servername);
+
+    if (host.len == 0) {
+        goto done;
+    }
+
+    host.data = (u_char *) servername;
+
+    rc = ngx_stream_validate_host(&host, c->pool, 1);
+
+    if (rc == NGX_ERROR) {
+        goto error;
+    }
+
+    if (rc == NGX_DECLINED) {
+        goto done;
+    }
+
+    rc = ngx_stream_find_virtual_server(s, &host, &cscf);
+
+    if (rc == NGX_ERROR) {
+        goto error;
+    }
+
+    if (rc == NGX_DECLINED) {
+        goto done;
+    }
+
+    sscf = ngx_stream_get_module_srv_conf(cscf->ctx, ngx_stream_ssl_module);
+
+#if (defined TLS1_3_VERSION                                                   \
+     && !defined LIBRESSL_VERSION_NUMBER && !defined OPENSSL_IS_BORINGSSL)
+
+    /*
+     * SSL_SESSION_get0_hostname() is only available in OpenSSL 1.1.1+,
+     * but servername being negotiated in every TLSv1.3 handshake
+     * is only returned in OpenSSL 1.1.1+ as well
+     */
+
+    if (sscf->verify) {
+        const char  *hostname;
+
+        hostname = SSL_SESSION_get0_hostname(SSL_get0_session(ssl_conn));
+
+        if (hostname != NULL && ngx_strcmp(hostname, servername) != 0) {
+            c->ssl->handshake_rejected = 1;
+            *ad = SSL_AD_ACCESS_DENIED;
+            return SSL_TLSEXT_ERR_ALERT_FATAL;
+        }
+    }
+
+#endif
+
+    s->srv_conf = cscf->ctx->srv_conf;
+
+    ngx_set_connection_log(c, cscf->error_log);
+
+    if (sscf->ssl.ctx) {
+        if (SSL_set_SSL_CTX(ssl_conn, sscf->ssl.ctx) == NULL) {
+            goto error;
+        }
+
+        /*
+         * SSL_set_SSL_CTX() only changes certs as of 1.0.0d
+         * adjust other things we care about
+         */
+
+        SSL_set_verify(ssl_conn, SSL_CTX_get_verify_mode(sscf->ssl.ctx),
+                       SSL_CTX_get_verify_callback(sscf->ssl.ctx));
+
+        SSL_set_verify_depth(ssl_conn, SSL_CTX_get_verify_depth(sscf->ssl.ctx));
+
+#if OPENSSL_VERSION_NUMBER >= 0x009080dfL
+        /* only in 0.9.8m+ */
+        SSL_clear_options(ssl_conn, SSL_get_options(ssl_conn) &
+                                    ~SSL_CTX_get_options(sscf->ssl.ctx));
+#endif
+
+        SSL_set_options(ssl_conn, SSL_CTX_get_options(sscf->ssl.ctx));
+
+#ifdef SSL_OP_NO_RENEGOTIATION
+        SSL_set_options(ssl_conn, SSL_OP_NO_RENEGOTIATION);
+#endif
+    }
+
+done:
+
+    sscf = ngx_stream_get_module_srv_conf(s, ngx_stream_ssl_module);
+
+    if (sscf->reject_handshake) {
+        c->ssl->handshake_rejected = 1;
+        *ad = SSL_AD_UNRECOGNIZED_NAME;
+        return SSL_TLSEXT_ERR_ALERT_FATAL;
+    }
+
     return SSL_TLSEXT_ERR_OK;
+
+error:
+
+    *ad = SSL_AD_INTERNAL_ERROR;
+    return SSL_TLSEXT_ERR_ALERT_FATAL;
 }
 
 #endif
@@ -513,7 +648,7 @@ ngx_stream_ssl_certificate(ngx_ssl_conn_t *ssl_conn, void *arg)
     ngx_uint_t                   i, nelts;
     ngx_connection_t            *c;
     ngx_stream_session_t        *s;
-    ngx_stream_ssl_conf_t       *sslcf;
+    ngx_stream_ssl_srv_conf_t   *sscf;
     ngx_stream_complex_value_t  *certs, *keys;
 
     c = ngx_ssl_get_connection(ssl_conn);
@@ -524,11 +659,11 @@ ngx_stream_ssl_certificate(ngx_ssl_conn_t *ssl_conn, void *arg)
 
     s = c->data;
 
-    sslcf = arg;
+    sscf = arg;
 
-    nelts = sslcf->certificate_values->nelts;
-    certs = sslcf->certificate_values->elts;
-    keys = sslcf->certificate_key_values->elts;
+    nelts = sscf->certificate_values->nelts;
+    certs = sscf->certificate_values->elts;
+    keys = sscf->certificate_key_values->elts;
 
     for (i = 0; i < nelts; i++) {
 
@@ -547,7 +682,7 @@ ngx_stream_ssl_certificate(ngx_ssl_conn_t *ssl_conn, void *arg)
                        "ssl key: \"%s\"", key.data);
 
         if (ngx_ssl_connection_certificate(c, c->pool, &cert, &key,
-                                           sslcf->passwords)
+                                           sscf->passwords)
             != NGX_OK)
         {
             return 0;
@@ -643,53 +778,53 @@ ngx_stream_ssl_add_variables(ngx_conf_t *cf)
 
 
 static void *
-ngx_stream_ssl_create_conf(ngx_conf_t *cf)
+ngx_stream_ssl_create_srv_conf(ngx_conf_t *cf)
 {
-    ngx_stream_ssl_conf_t  *scf;
+    ngx_stream_ssl_srv_conf_t  *sscf;
 
-    scf = ngx_pcalloc(cf->pool, sizeof(ngx_stream_ssl_conf_t));
-    if (scf == NULL) {
+    sscf = ngx_pcalloc(cf->pool, sizeof(ngx_stream_ssl_srv_conf_t));
+    if (sscf == NULL) {
         return NULL;
     }
 
     /*
      * set by ngx_pcalloc():
      *
-     *     scf->listen = 0;
-     *     scf->protocols = 0;
-     *     scf->certificate_values = NULL;
-     *     scf->dhparam = { 0, NULL };
-     *     scf->ecdh_curve = { 0, NULL };
-     *     scf->client_certificate = { 0, NULL };
-     *     scf->trusted_certificate = { 0, NULL };
-     *     scf->crl = { 0, NULL };
-     *     scf->alpn = { 0, NULL };
-     *     scf->ciphers = { 0, NULL };
-     *     scf->shm_zone = NULL;
+     *     sscf->protocols = 0;
+     *     sscf->certificate_values = NULL;
+     *     sscf->dhparam = { 0, NULL };
+     *     sscf->ecdh_curve = { 0, NULL };
+     *     sscf->client_certificate = { 0, NULL };
+     *     sscf->trusted_certificate = { 0, NULL };
+     *     sscf->crl = { 0, NULL };
+     *     sscf->alpn = { 0, NULL };
+     *     sscf->ciphers = { 0, NULL };
+     *     sscf->shm_zone = NULL;
      */
 
-    scf->handshake_timeout = NGX_CONF_UNSET_MSEC;
-    scf->certificates = NGX_CONF_UNSET_PTR;
-    scf->certificate_keys = NGX_CONF_UNSET_PTR;
-    scf->passwords = NGX_CONF_UNSET_PTR;
-    scf->conf_commands = NGX_CONF_UNSET_PTR;
-    scf->prefer_server_ciphers = NGX_CONF_UNSET;
-    scf->verify = NGX_CONF_UNSET_UINT;
-    scf->verify_depth = NGX_CONF_UNSET_UINT;
-    scf->builtin_session_cache = NGX_CONF_UNSET;
-    scf->session_timeout = NGX_CONF_UNSET;
-    scf->session_tickets = NGX_CONF_UNSET;
-    scf->session_ticket_keys = NGX_CONF_UNSET_PTR;
-
-    return scf;
+    sscf->handshake_timeout = NGX_CONF_UNSET_MSEC;
+    sscf->certificates = NGX_CONF_UNSET_PTR;
+    sscf->certificate_keys = NGX_CONF_UNSET_PTR;
+    sscf->passwords = NGX_CONF_UNSET_PTR;
+    sscf->conf_commands = NGX_CONF_UNSET_PTR;
+    sscf->prefer_server_ciphers = NGX_CONF_UNSET;
+    sscf->reject_handshake = NGX_CONF_UNSET;
+    sscf->verify = NGX_CONF_UNSET_UINT;
+    sscf->verify_depth = NGX_CONF_UNSET_UINT;
+    sscf->builtin_session_cache = NGX_CONF_UNSET;
+    sscf->session_timeout = NGX_CONF_UNSET;
+    sscf->session_tickets = NGX_CONF_UNSET;
+    sscf->session_ticket_keys = NGX_CONF_UNSET_PTR;
+
+    return sscf;
 }
 
 
 static char *
-ngx_stream_ssl_merge_conf(ngx_conf_t *cf, void *parent, void *child)
+ngx_stream_ssl_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child)
 {
-    ngx_stream_ssl_conf_t *prev = parent;
-    ngx_stream_ssl_conf_t *conf = child;
+    ngx_stream_ssl_srv_conf_t *prev = parent;
+    ngx_stream_ssl_srv_conf_t *conf = child;
 
     ngx_pool_cleanup_t  *cln;
 
@@ -702,9 +837,12 @@ ngx_stream_ssl_merge_conf(ngx_conf_t *cf, void *parent, void *child)
     ngx_conf_merge_value(conf->prefer_server_ciphers,
                          prev->prefer_server_ciphers, 0);
 
+    ngx_conf_merge_value(conf->reject_handshake, prev->reject_handshake, 0);
+
     ngx_conf_merge_bitmask_value(conf->protocols, prev->protocols,
-                         (NGX_CONF_BITMASK_SET|NGX_SSL_TLSv1
-                          |NGX_SSL_TLSv1_1|NGX_SSL_TLSv1_2));
+                         (NGX_CONF_BITMASK_SET
+                          |NGX_SSL_TLSv1|NGX_SSL_TLSv1_1
+                          |NGX_SSL_TLSv1_2|NGX_SSL_TLSv1_3));
 
     ngx_conf_merge_uint_value(conf->verify, prev->verify, 0);
     ngx_conf_merge_uint_value(conf->verify_depth, prev->verify_depth, 1);
@@ -734,35 +872,21 @@ ngx_stream_ssl_merge_conf(ngx_conf_t *cf, void *parent, void *child)
 
     conf->ssl.log = cf->log;
 
-    if (!conf->listen) {
-        return NGX_CONF_OK;
-    }
+    if (conf->certificates) {
 
-    if (conf->certificates == NULL) {
-        ngx_log_error(NGX_LOG_EMERG, cf->log, 0,
-                      "no \"ssl_certificate\" is defined for "
-                      "the \"listen ... ssl\" directive in %s:%ui",
-                      conf->file, conf->line);
-        return NGX_CONF_ERROR;
-    }
-
-    if (conf->certificate_keys == NULL) {
-        ngx_log_error(NGX_LOG_EMERG, cf->log, 0,
-                      "no \"ssl_certificate_key\" is defined for "
-                      "the \"listen ... ssl\" directive in %s:%ui",
-                      conf->file, conf->line);
-        return NGX_CONF_ERROR;
-    }
+        if (conf->certificate_keys == NULL
+            || conf->certificate_keys->nelts < conf->certificates->nelts)
+        {
+            ngx_log_error(NGX_LOG_EMERG, cf->log, 0,
+                          "no \"ssl_certificate_key\" is defined "
+                          "for certificate \"%V\"",
+                          ((ngx_str_t *) conf->certificates->elts)
+                          + conf->certificates->nelts - 1);
+            return NGX_CONF_ERROR;
+        }
 
-    if (conf->certificate_keys->nelts < conf->certificates->nelts) {
-        ngx_log_error(NGX_LOG_EMERG, cf->log, 0,
-                      "no \"ssl_certificate_key\" is defined "
-                      "for certificate \"%V\" and "
-                      "the \"listen ... ssl\" directive in %s:%ui",
-                      ((ngx_str_t *) conf->certificates->elts)
-                      + conf->certificates->nelts - 1,
-                      conf->file, conf->line);
-        return NGX_CONF_ERROR;
+    } else if (!conf->reject_handshake) {
+        return NGX_CONF_OK;
     }
 
     if (ngx_ssl_create(&conf->ssl, conf->protocols, NULL) != NGX_OK) {
@@ -817,7 +941,7 @@ ngx_stream_ssl_merge_conf(ngx_conf_t *cf, void *parent, void *child)
         return NGX_CONF_ERROR;
 #endif
 
-    } else {
+    } else if (conf->certificates) {
 
         /* configure certificates */
 
@@ -909,13 +1033,17 @@ ngx_stream_ssl_merge_conf(ngx_conf_t *cf, void *parent, void *child)
 
 static ngx_int_t
 ngx_stream_ssl_compile_certificates(ngx_conf_t *cf,
-    ngx_stream_ssl_conf_t *conf)
+    ngx_stream_ssl_srv_conf_t *conf)
 {
     ngx_str_t                           *cert, *key;
     ngx_uint_t                           i, nelts;
     ngx_stream_complex_value_t          *cv;
     ngx_stream_compile_complex_value_t   ccv;
 
+    if (conf->certificates == NULL) {
+        return NGX_OK;
+    }
+
     cert = conf->certificates->elts;
     key = conf->certificate_keys->elts;
     nelts = conf->certificates->nelts;
@@ -994,19 +1122,19 @@ found:
 static char *
 ngx_stream_ssl_password_file(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
 {
-    ngx_stream_ssl_conf_t  *scf = conf;
+    ngx_stream_ssl_srv_conf_t  *sscf = conf;
 
     ngx_str_t  *value;
 
-    if (scf->passwords != NGX_CONF_UNSET_PTR) {
+    if (sscf->passwords != NGX_CONF_UNSET_PTR) {
         return "is duplicate";
     }
 
     value = cf->args->elts;
 
-    scf->passwords = ngx_ssl_read_password_file(cf, &value[1]);
+    sscf->passwords = ngx_ssl_read_password_file(cf, &value[1]);
 
-    if (scf->passwords == NULL) {
+    if (sscf->passwords == NULL) {
         return NGX_CONF_ERROR;
     }
 
@@ -1017,7 +1145,7 @@ ngx_stream_ssl_password_file(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
 static char *
 ngx_stream_ssl_session_cache(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
 {
-    ngx_stream_ssl_conf_t  *scf = conf;
+    ngx_stream_ssl_srv_conf_t  *sscf = conf;
 
     size_t       len;
     ngx_str_t   *value, name, size;
@@ -1029,17 +1157,17 @@ ngx_stream_ssl_session_cache(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
     for (i = 1; i < cf->args->nelts; i++) {
 
         if (ngx_strcmp(value[i].data, "off") == 0) {
-            scf->builtin_session_cache = NGX_SSL_NO_SCACHE;
+            sscf->builtin_session_cache = NGX_SSL_NO_SCACHE;
             continue;
         }
 
         if (ngx_strcmp(value[i].data, "none") == 0) {
-            scf->builtin_session_cache = NGX_SSL_NONE_SCACHE;
+            sscf->builtin_session_cache = NGX_SSL_NONE_SCACHE;
             continue;
         }
 
         if (ngx_strcmp(value[i].data, "builtin") == 0) {
-            scf->builtin_session_cache = NGX_SSL_DFLT_BUILTIN_SCACHE;
+            sscf->builtin_session_cache = NGX_SSL_DFLT_BUILTIN_SCACHE;
             continue;
         }
 
@@ -1054,7 +1182,7 @@ ngx_stream_ssl_session_cache(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
                 goto invalid;
             }
 
-            scf->builtin_session_cache = n;
+            sscf->builtin_session_cache = n;
 
             continue;
         }
@@ -1073,7 +1201,7 @@ ngx_stream_ssl_session_cache(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
                 len++;
             }
 
-            if (len == 0) {
+            if (len == 0 || j == value[i].len) {
                 goto invalid;
             }
 
@@ -1097,13 +1225,13 @@ ngx_stream_ssl_session_cache(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
                 return NGX_CONF_ERROR;
             }
 
-            scf->shm_zone = ngx_shared_memory_add(cf, &name, n,
+            sscf->shm_zone = ngx_shared_memory_add(cf, &name, n,
                                                    &ngx_stream_ssl_module);
-            if (scf->shm_zone == NULL) {
+            if (sscf->shm_zone == NULL) {
                 return NGX_CONF_ERROR;
             }
 
-            scf->shm_zone->init = ngx_ssl_session_cache_init;
+            sscf->shm_zone->init = ngx_ssl_session_cache_init;
 
             continue;
         }
@@ -1111,8 +1239,8 @@ ngx_stream_ssl_session_cache(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
         goto invalid;
     }
 
-    if (scf->shm_zone && scf->builtin_session_cache == NGX_CONF_UNSET) {
-        scf->builtin_session_cache = NGX_SSL_NO_BUILTIN_SCACHE;
+    if (sscf->shm_zone && sscf->builtin_session_cache == NGX_CONF_UNSET) {
+        sscf->builtin_session_cache = NGX_SSL_NO_BUILTIN_SCACHE;
     }
 
     return NGX_CONF_OK;
@@ -1131,14 +1259,14 @@ ngx_stream_ssl_alpn(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
 {
 #ifdef TLSEXT_TYPE_application_layer_protocol_negotiation
 
-    ngx_stream_ssl_conf_t  *scf = conf;
+    ngx_stream_ssl_srv_conf_t  *sscf = conf;
 
     u_char      *p;
     size_t       len;
     ngx_str_t   *value;
     ngx_uint_t   i;
 
-    if (scf->alpn.len) {
+    if (sscf->alpn.len) {
         return "is duplicate";
     }
 
@@ -1155,19 +1283,19 @@ ngx_stream_ssl_alpn(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
         len += value[i].len + 1;
     }
 
-    scf->alpn.data = ngx_pnalloc(cf->pool, len);
-    if (scf->alpn.data == NULL) {
+    sscf->alpn.data = ngx_pnalloc(cf->pool, len);
+    if (sscf->alpn.data == NULL) {
         return NGX_CONF_ERROR;
     }
 
-    p = scf->alpn.data;
+    p = sscf->alpn.data;
 
     for (i = 1; i < cf->args->nelts; i++) {
         *p++ = value[i].len;
         p = ngx_cpymem(p, value[i].data, value[i].len);
     }
 
-    scf->alpn.len = len;
+    sscf->alpn.len = len;
 
     return NGX_CONF_OK;
 
@@ -1194,8 +1322,13 @@ ngx_stream_ssl_conf_command_check(ngx_conf_t *cf, void *post, void *data)
 static ngx_int_t
 ngx_stream_ssl_init(ngx_conf_t *cf)
 {
-    ngx_stream_handler_pt        *h;
-    ngx_stream_core_main_conf_t  *cmcf;
+    ngx_uint_t                     a, p, s;
+    ngx_stream_handler_pt         *h;
+    ngx_stream_conf_addr_t        *addr;
+    ngx_stream_conf_port_t        *port;
+    ngx_stream_ssl_srv_conf_t     *sscf;
+    ngx_stream_core_srv_conf_t   **cscfp, *cscf;
+    ngx_stream_core_main_conf_t   *cmcf;
 
     cmcf = ngx_stream_conf_get_module_main_conf(cf, ngx_stream_core_module);
 
@@ -1206,5 +1339,58 @@ ngx_stream_ssl_init(ngx_conf_t *cf)
 
     *h = ngx_stream_ssl_handler;
 
+    if (cmcf->ports == NULL) {
+        return NGX_OK;
+    }
+
+    port = cmcf->ports->elts;
+    for (p = 0; p < cmcf->ports->nelts; p++) {
+
+        addr = port[p].addrs.elts;
+        for (a = 0; a < port[p].addrs.nelts; a++) {
+
+            if (!addr[a].opt.ssl) {
+                continue;
+            }
+
+            cscf = addr[a].default_server;
+            sscf = cscf->ctx->srv_conf[ngx_stream_ssl_module.ctx_index];
+
+            if (sscf->certificates) {
+                continue;
+            }
+
+            if (!sscf->reject_handshake) {
+                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);
+                return NGX_ERROR;
+            }
+
+            /*
+             * if no certificates are defined in the default server,
+             * check all non-default server blocks
+             */
+
+            cscfp = addr[a].servers.elts;
+            for (s = 0; s < addr[a].servers.nelts; s++) {
+
+                cscf = cscfp[s];
+                sscf = cscf->ctx->srv_conf[ngx_stream_ssl_module.ctx_index];
+
+                if (sscf->certificates || sscf->reject_handshake) {
+                    continue;
+                }
+
+                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);
+                return NGX_ERROR;
+            }
+        }
+    }
+
     return NGX_OK;
 }
diff --git a/src/stream/ngx_stream_ssl_module.h b/src/stream/ngx_stream_ssl_module.h
index e7c825e..6f6d9ae 100644
--- a/src/stream/ngx_stream_ssl_module.h
+++ b/src/stream/ngx_stream_ssl_module.h
@@ -18,10 +18,10 @@ typedef struct {
     ngx_msec_t       handshake_timeout;
 
     ngx_flag_t       prefer_server_ciphers;
+    ngx_flag_t       reject_handshake;
 
     ngx_ssl_t        ssl;
 
-    ngx_uint_t       listen;
     ngx_uint_t       protocols;
 
     ngx_uint_t       verify;
@@ -53,10 +53,7 @@ typedef struct {
 
     ngx_flag_t       session_tickets;
     ngx_array_t     *session_ticket_keys;
-
-    u_char          *file;
-    ngx_uint_t       line;
-} ngx_stream_ssl_conf_t;
+} ngx_stream_ssl_srv_conf_t;
 
 
 extern ngx_module_t  ngx_stream_ssl_module;
diff --git a/src/stream/ngx_stream_ssl_preread_module.c b/src/stream/ngx_stream_ssl_preread_module.c
index a236fc5..bc96ade 100644
--- a/src/stream/ngx_stream_ssl_preread_module.c
+++ b/src/stream/ngx_stream_ssl_preread_module.c
@@ -33,6 +33,8 @@ typedef struct {
 static ngx_int_t ngx_stream_ssl_preread_handler(ngx_stream_session_t *s);
 static ngx_int_t ngx_stream_ssl_preread_parse_record(
     ngx_stream_ssl_preread_ctx_t *ctx, u_char *pos, u_char *last);
+static ngx_int_t ngx_stream_ssl_preread_servername(ngx_stream_session_t *s,
+    ngx_str_t *servername);
 static ngx_int_t ngx_stream_ssl_preread_protocol_variable(
     ngx_stream_session_t *s, ngx_stream_variable_value_t *v, uintptr_t data);
 static ngx_int_t ngx_stream_ssl_preread_server_name_variable(
@@ -187,6 +189,10 @@ ngx_stream_ssl_preread_handler(ngx_stream_session_t *s)
             return NGX_DECLINED;
         }
 
+        if (rc == NGX_OK) {
+            return ngx_stream_ssl_preread_servername(s, &ctx->host);
+        }
+
         if (rc != NGX_AGAIN) {
             return rc;
         }
@@ -404,9 +410,6 @@ ngx_stream_ssl_preread_parse_record(ngx_stream_ssl_preread_ctx_t *ctx,
         case sw_sni_host:
             ctx->host.len = (p[1] << 8) + p[2];
 
-            ngx_log_debug1(NGX_LOG_DEBUG_STREAM, ctx->log, 0,
-                           "ssl preread: SNI hostname \"%V\"", &ctx->host);
-
             state = sw_ext;
             dst = NULL;
             size = ext;
@@ -496,6 +499,54 @@ ngx_stream_ssl_preread_parse_record(ngx_stream_ssl_preread_ctx_t *ctx,
 }
 
 
+static ngx_int_t
+ngx_stream_ssl_preread_servername(ngx_stream_session_t *s,
+    ngx_str_t *servername)
+{
+    ngx_int_t                    rc;
+    ngx_str_t                    host;
+    ngx_connection_t            *c;
+    ngx_stream_core_srv_conf_t  *cscf;
+
+    c = s->connection;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_STREAM, c->log, 0,
+                   "SSL preread server name: \"%V\"", servername);
+
+    if (servername->len == 0) {
+        return NGX_OK;
+    }
+
+    host = *servername;
+
+    rc = ngx_stream_validate_host(&host, c->pool, 1);
+
+    if (rc == NGX_ERROR) {
+        return NGX_ERROR;
+    }
+
+    if (rc == NGX_DECLINED) {
+        return NGX_OK;
+    }
+
+    rc = ngx_stream_find_virtual_server(s, &host, &cscf);
+
+    if (rc == NGX_ERROR) {
+        return NGX_ERROR;
+    }
+
+    if (rc == NGX_DECLINED) {
+        return NGX_OK;
+    }
+
+    s->srv_conf = cscf->ctx->srv_conf;
+
+    ngx_set_connection_log(c, cscf->error_log);
+
+    return NGX_OK;
+}
+
+
 static ngx_int_t
 ngx_stream_ssl_preread_protocol_variable(ngx_stream_session_t *s,
     ngx_variable_value_t *v, uintptr_t data)
diff --git a/src/stream/ngx_stream_variables.c b/src/stream/ngx_stream_variables.c
index 8b59668..4658104 100644
--- a/src/stream/ngx_stream_variables.c
+++ b/src/stream/ngx_stream_variables.c
@@ -23,10 +23,14 @@ static ngx_int_t ngx_stream_variable_proxy_protocol_addr(
     ngx_stream_session_t *s, ngx_stream_variable_value_t *v, uintptr_t data);
 static ngx_int_t ngx_stream_variable_proxy_protocol_port(
     ngx_stream_session_t *s, ngx_stream_variable_value_t *v, uintptr_t data);
+static ngx_int_t ngx_stream_variable_proxy_protocol_tlv(
+    ngx_stream_session_t *s, ngx_stream_variable_value_t *v, uintptr_t data);
 static ngx_int_t ngx_stream_variable_server_addr(ngx_stream_session_t *s,
     ngx_stream_variable_value_t *v, uintptr_t data);
 static ngx_int_t ngx_stream_variable_server_port(ngx_stream_session_t *s,
     ngx_stream_variable_value_t *v, uintptr_t data);
+static ngx_int_t ngx_stream_variable_server_name(ngx_stream_session_t *s,
+    ngx_stream_variable_value_t *v, uintptr_t data);
 static ngx_int_t ngx_stream_variable_bytes(ngx_stream_session_t *s,
     ngx_stream_variable_value_t *v, uintptr_t data);
 static ngx_int_t ngx_stream_variable_session_time(ngx_stream_session_t *s,
@@ -79,12 +83,19 @@ static ngx_stream_variable_t  ngx_stream_core_variables[] = {
       ngx_stream_variable_proxy_protocol_port,
       offsetof(ngx_proxy_protocol_t, dst_port), 0, 0 },
 
+    { ngx_string("proxy_protocol_tlv_"), NULL,
+      ngx_stream_variable_proxy_protocol_tlv,
+      0, NGX_STREAM_VAR_PREFIX, 0 },
+
     { ngx_string("server_addr"), NULL,
       ngx_stream_variable_server_addr, 0, 0, 0 },
 
     { ngx_string("server_port"), NULL,
       ngx_stream_variable_server_port, 0, 0, 0 },
 
+    { ngx_string("server_name"), NULL, ngx_stream_variable_server_name,
+      0, 0, 0 },
+
     { ngx_string("bytes_sent"), NULL, ngx_stream_variable_bytes,
       0, 0, 0 },
 
@@ -621,6 +632,39 @@ ngx_stream_variable_proxy_protocol_port(ngx_stream_session_t *s,
 }
 
 
+static ngx_int_t
+ngx_stream_variable_proxy_protocol_tlv(ngx_stream_session_t *s,
+    ngx_stream_variable_value_t *v, uintptr_t data)
+{
+    ngx_str_t *name = (ngx_str_t *) data;
+
+    ngx_int_t  rc;
+    ngx_str_t  tlv, value;
+
+    tlv.len = name->len - (sizeof("proxy_protocol_tlv_") - 1);
+    tlv.data = name->data + sizeof("proxy_protocol_tlv_") - 1;
+
+    rc = ngx_proxy_protocol_get_tlv(s->connection, &tlv, &value);
+
+    if (rc == NGX_ERROR) {
+        return NGX_ERROR;
+    }
+
+    if (rc == NGX_DECLINED) {
+        v->not_found = 1;
+        return NGX_OK;
+    }
+
+    v->len = value.len;
+    v->valid = 1;
+    v->no_cacheable = 0;
+    v->not_found = 0;
+    v->data = value.data;
+
+    return NGX_OK;
+}
+
+
 static ngx_int_t
 ngx_stream_variable_server_addr(ngx_stream_session_t *s,
     ngx_stream_variable_value_t *v, uintptr_t data)
@@ -682,6 +726,24 @@ ngx_stream_variable_server_port(ngx_stream_session_t *s,
 }
 
 
+static ngx_int_t
+ngx_stream_variable_server_name(ngx_stream_session_t *s,
+    ngx_stream_variable_value_t *v, uintptr_t data)
+{
+    ngx_stream_core_srv_conf_t  *cscf;
+
+    cscf = ngx_stream_get_module_srv_conf(s, ngx_stream_core_module);
+
+    v->len = cscf->server_name.len;
+    v->valid = 1;
+    v->no_cacheable = 0;
+    v->not_found = 0;
+    v->data = cscf->server_name.data;
+
+    return NGX_OK;
+}
+
+
 static ngx_int_t
 ngx_stream_variable_bytes(ngx_stream_session_t *s,
     ngx_stream_variable_value_t *v, uintptr_t data)
diff --git a/src/stream/ngx_stream_write_filter_module.c b/src/stream/ngx_stream_write_filter_module.c
index 156a61c..07dc7b5 100644
--- a/src/stream/ngx_stream_write_filter_module.c
+++ b/src/stream/ngx_stream_write_filter_module.c
@@ -235,7 +235,7 @@ ngx_stream_write_filter(ngx_stream_session_t *s, ngx_chain_t *in,
     if (size == 0
         && !(c->buffered & NGX_LOWLEVEL_BUFFERED)
         && !(last && c->need_last_buf)
-        && !(c->type == SOCK_DGRAM && flush))
+        && !(flush && c->need_flush_buf))
     {
         if (last || flush || sync) {
             for (cl = *out; cl; /* void */) {
-- 
GitLab