Patch subject is complete summary.

 src/event/quic/ngx_event_quic.c                |  617 ++++++++++++++++++++++++-
 src/event/quic/ngx_event_quic.h                |   11 +
 src/event/quic/ngx_event_quic_ack.c            |   13 +
 src/event/quic/ngx_event_quic_ack.h            |    2 +
 src/event/quic/ngx_event_quic_connection.h     |   10 +
 src/event/quic/ngx_event_quic_connid.c         |   64 ++-
 src/event/quic/ngx_event_quic_connid.h         |    7 +-
 src/event/quic/ngx_event_quic_migration.c      |   10 +-
 src/event/quic/ngx_event_quic_openssl_compat.c |    4 +
 src/event/quic/ngx_event_quic_output.c         |  109 +++-
 src/event/quic/ngx_event_quic_protection.c     |  101 ++-
 src/event/quic/ngx_event_quic_protection.h     |    3 +
 src/event/quic/ngx_event_quic_socket.c         |   71 ++-
 src/event/quic/ngx_event_quic_socket.h         |    4 +-
 src/event/quic/ngx_event_quic_ssl.c            |  172 +++++-
 src/event/quic/ngx_event_quic_ssl.h            |    3 +
 src/event/quic/ngx_event_quic_streams.c        |  550 +++++++++++++++------
 src/event/quic/ngx_event_quic_streams.h        |    3 +
 src/event/quic/ngx_event_quic_tokens.c         |   48 +
 src/event/quic/ngx_event_quic_tokens.h         |    9 +
 src/event/quic/ngx_event_quic_transport.c      |  214 ++++++--
 src/event/quic/ngx_event_quic_transport.h      |    7 +-
 22 files changed, 1677 insertions(+), 355 deletions(-)


# HG changeset patch
# User Vladimir Khomutov <v...@wbsrv.ru>
# Date 1703082264 -10800
#      Wed Dec 20 17:24:24 2023 +0300
# Node ID f39271dd260b831fac70c776904d9f5ded053968
# Parent  f54423e057f909b1d644cc0af316d67b91cd408f
QUIC: client support.

diff --git a/src/event/quic/ngx_event_quic.c b/src/event/quic/ngx_event_quic.c
--- a/src/event/quic/ngx_event_quic.c
+++ b/src/event/quic/ngx_event_quic.c
@@ -18,6 +18,11 @@ static ngx_int_t ngx_quic_handle_statele
 static void ngx_quic_input_handler(ngx_event_t *rev);
 static void ngx_quic_close_handler(ngx_event_t *ev);
 
+static void ngx_quic_dummy_handler(ngx_event_t *ev);
+static void ngx_quic_client_input_handler(ngx_event_t *rev);
+static ngx_int_t ngx_quic_client_start(ngx_connection_t *c,
+    ngx_quic_header_t *pkt);
+
 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,
@@ -188,8 +193,16 @@ ngx_quic_apply_transport_params(ngx_conn
     qc->streams.server.bidi.max = peer_tp->initial_max_streams_bidi;
     qc->streams.server.uni.max = peer_tp->initial_max_streams_uni;
 
+    if (qc->client) {
+        ngx_memcpy(qc->path->cid->sr_token,
+                   peer_tp->sr_token, NGX_QUIC_SR_TOKEN_LEN);
+    }
+
     ngx_memcpy(&qc->peer_tp, peer_tp, sizeof(ngx_quic_tp_t));
 
+    /* apply transport parameters to early created streams */
+    ngx_quic_streams_init_state(c);
+
     return NGX_OK;
 }
 
@@ -222,10 +235,339 @@ ngx_quic_run(ngx_connection_t *c, ngx_qu
 }
 
 
+static void
+ngx_quic_dummy_handler(ngx_event_t *ev)
+{
+}
+
+
+ngx_int_t
+ngx_quic_create_client(ngx_quic_conf_t *conf, ngx_connection_t *c)
+{
+    int                     value;
+    ngx_log_t              *log;
+    ngx_quic_connection_t  *qc;
+
+#if (NGX_HAVE_IP_MTU_DISCOVER)
+
+    if (c->sockaddr->sa_family == AF_INET) {
+        value = IP_PMTUDISC_DO;
+
+        if (setsockopt(c->fd, IPPROTO_IP, IP_MTU_DISCOVER,
+                       (const void *) &value, sizeof(int))
+            == -1)
+        {
+            ngx_log_error(NGX_LOG_ALERT, c->log, ngx_socket_errno,
+                          "setsockopt(IP_MTU_DISCOVER) "
+                          "for quic conn failed, ignored");
+        }
+    }
+
+#elif (NGX_HAVE_IP_DONTFRAG)
+
+    if (c->sockaddr->sa_family == AF_INET) {
+        value = 1;
+
+        if (setsockopt(c->fd, IPPROTO_IP, IP_DONTFRAG,
+                       (const void *) &value, sizeof(int))
+            == -1)
+        {
+            ngx_log_error(NGX_LOG_ALERT, c->log, ngx_socket_errno,
+                          "setsockopt(IP_DONTFRAG) "
+                          "for quic conn failed, ignored");
+        }
+    }
+
+#endif
+
+#if (NGX_HAVE_INET6)
+
+#if (NGX_HAVE_IPV6_MTU_DISCOVER)
+
+    if (c->sockaddr->sa_family == AF_INET6) {
+        value = IPV6_PMTUDISC_DO;
+
+        if (setsockopt(c->fd, IPPROTO_IPV6, IPV6_MTU_DISCOVER,
+                       (const void *) &value, sizeof(int))
+            == -1)
+        {
+            ngx_log_error(NGX_LOG_ALERT, c->log, ngx_socket_errno,
+                          "setsockopt(IPV6_MTU_DISCOVER) "
+                          "for quic conn failed, ignored");
+        }
+    }
+
+#elif (NGX_HAVE_IP_DONTFRAG)
+
+    if (c->sockaddr->sa_family == AF_INET6) {
+
+        value = 1;
+
+        if (setsockopt(c->fd, IPPROTO_IPV6, IPV6_DONTFRAG,
+                       (const void *) &value, sizeof(int))
+            == -1)
+        {
+            ngx_log_error(NGX_LOG_ALERT, c->log, ngx_socket_errno,
+                          "setsockopt(IPV6_DONTFRAG) "
+                          "for quic conn failed, ignored");
+        }
+    }
+#endif
+
+#endif
+
+    c->read->handler = ngx_quic_client_input_handler;
+    c->write->handler = ngx_quic_dummy_handler;
+
+    if (conf->active_connection_id_limit == 0) {
+        /*
+         * TODO: remove when done with testing
+         *
+         * this case exists purely for testing/coverage purposes;
+         * (RFC requires minimum value of 2, and default is 2, so no real
+         *  configurations want to set this zero)
+         */
+        return NGX_ERROR;
+    }
+
+    /*
+     * each stream calls c->listening()->handler for initialization;
+     * handler is set by the caller
+     */
+    c->listening = ngx_pcalloc(c->pool, sizeof(ngx_listening_t));
+    if (c->listening == NULL) {
+        return NGX_ERROR;
+    }
+
+    /*
+     * 'c' is a new connection to upstream and c->log is inherited from
+     * the r->connection->log (allocated from r->pool)
+     *
+     * main quic connection (this) may exist longer than client connection
+     * due to keepalive and/or non-immediate closing
+     *
+     * unlike tcp keepalive, main quic connection is alive during the
+     * time between requests, and may produce events with logging.
+     *
+     * so, use log from ngx_cycle instead of client log, which may be
+     * destroyed.
+     */
+    log = ngx_palloc(c->pool, sizeof(ngx_log_t));
+    if (log == NULL) {
+        return NGX_ERROR;
+    }
+
+    *log = *ngx_cycle->log;
+    c->log = log;
+
+    log->connection = c->number;
+
+    c->read->log = c->log;
+    c->write->log = c->log;
+    c->pool->log = c->log;
+
+    qc = ngx_quic_new_connection(c, conf, NULL);
+    if (qc == NULL) {
+        return NGX_ERROR;
+    }
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic client initialized on c:%p", c);
+
+    return NGX_OK;
+}
+
+
+void
+ngx_quic_client_set_ssl_data(ngx_connection_t *c, void *data)
+{
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    if (qc == NULL) {
+        return;
+    }
+
+    qc->init_ssl_data = data;
+}
+
+
+void *
+ngx_quic_client_get_ssl_data(ngx_connection_t *c)
+{
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    if (qc == NULL) {
+        return NULL;
+    }
+
+    return qc->init_ssl_data;
+}
+
+
+ngx_int_t
+ngx_quic_connect(ngx_connection_t *c, ngx_quic_init_ssl_pt init_ssl, void *data)
+{
+    ngx_str_t               id;
+    ngx_quic_client_id_t   *cid;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    qc->init_ssl = init_ssl;
+    qc->init_ssl_data = data;
+
+    qc->peer_tp.max_udp_payload_size = NGX_QUIC_MAX_UDP_PAYLOAD_SIZE;
+
+    /* use initial dcid we generated on start */
+    id.data = qc->incid;
+    id.len = NGX_QUIC_SERVER_CID_LEN;
+
+    cid = ngx_quic_create_client_id(c, &id, 0, NULL);
+    if (cid == NULL) {
+        return NGX_ERROR;
+    }
+
+    qc->path = ngx_quic_new_path(c, c->sockaddr, c->socklen, cid);
+    if (qc->path == NULL) {
+        return NGX_ERROR;
+    }
+
+    qc->path->tag = NGX_QUIC_PATH_ACTIVE;
+    ngx_quic_path_dbg(c, "set active", qc->path);
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic client initiating connection");
+
+    return ngx_quic_client_start(c, NULL);
+}
+
+
+static ngx_int_t
+ngx_quic_client_start(ngx_connection_t *c, ngx_quic_header_t *pkt)
+{
+    ngx_str_t               dcid;
+    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 (pkt == NULL) {
+        /* not a retry packet */
+        goto start;
+    }
+
+    if (pkt->token.len <= 16) {
+        /*
+         * A client MUST discard a Retry packet with a zero-length
+         * Retry Token field.
+         */
+
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "quic client bad token length");
+        return NGX_ERROR;
+    }
+
+    if (ngx_quic_verify_retry_token_integrity(c, pkt) != NGX_OK) {
+        /*
+         * A client MUST accept and process at most one Retry packet
+         * for each connection attempt.
+         */
+
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "quic client retry token integrity check failed");
+        return NGX_ERROR;
+    }
+
+    /* server responded with new id, update */
+    ngx_memcpy(qc->path->cid->id, pkt->scid.data, pkt->scid.len);
+    qc->path->cid->len = pkt->scid.len;
+    qc->server_id_known = 1;
+
+    qc->client_retry.len = pkt->token.len - 16;
+
+    qc->client_retry.data = ngx_pnalloc(c->pool,
+                                        qc->client_retry.len);
+    if (qc->client_retry.data == NULL) {
+        return NGX_ERROR;
+    }
+
+    ngx_memcpy(qc->client_retry.data, pkt->token.data,
+               qc->client_retry.len);
+
+    /* prepare for one more SID change later */
+    qc->server_id_known = 0;
+
+    /*
+     * RFC 9002  6.3  Handling Retry Packets
+     *
+     * Clients that receive a Retry packet reset congestion control and loss
+     * recovery state, including resetting any pending timers. Other connection
+     * state, in particular cryptographic handshake messages, is retained; see
+     * Section 17.2.5 of [QUIC-TRANSPORT].
+     */
+
+    ctx = ngx_quic_get_send_ctx(qc, ssl_encryption_initial);
+
+    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);
+    }
+
+    ctx->send_ack = 0;
+    qc->pto_count = 0;
+
+    ngx_quic_congestion_reset(qc);
+
+    if (qc->pto.timer_set) {
+        ngx_del_timer(&qc->pto);
+    }
+
+    ngx_quic_set_lost_timer(c);
+
+    /* now we need to restart handshake from the beginning */
+
+    /* reset offset in CRYPTO frames */
+    ctx->crypto_sent = 0;
+
+    /* since SID has changed, new keys need to be generated */
+    dcid.data = pkt->scid.data;
+    dcid.len = pkt->scid.len;
+
+    ngx_quic_keys_cleanup(qc->keys);
+
+    if (ngx_quic_keys_set_initial_secret(qc->keys, &dcid, c->log) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+start:
+
+    if (ngx_quic_client_handshake(c) != NGX_OK) {
+        qc->error_reason = "handshake failed";
+        return NGX_ERROR;
+    }
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic client handshake started");
+
+    return NGX_AGAIN;
+}
+
+
 static ngx_quic_connection_t *
 ngx_quic_new_connection(ngx_connection_t *c, ngx_quic_conf_t *conf,
     ngx_quic_header_t *pkt)
 {
+    ngx_str_t               dcid;
     ngx_uint_t              i;
     ngx_quic_tp_t          *peer_tp;
     ngx_quic_connection_t  *qc;
@@ -235,12 +577,18 @@ ngx_quic_new_connection(ngx_connection_t
         return NULL;
     }
 
+    /* server connection requires a packet from client */
+    qc->client = (pkt == NULL);
+
     qc->keys = ngx_pcalloc(c->pool, sizeof(ngx_quic_keys_t));
     if (qc->keys == NULL) {
         return NULL;
     }
 
-    qc->version = pkt->version;
+    qc->keys->client = qc->client;
+
+    /* client always initiates QUIC v.1 */
+    qc->version = qc->client ? 1 : pkt->version;
 
     ngx_rbtree_init(&qc->streams.tree, &qc->streams.sentinel,
                     ngx_quic_rbtree_insert_stream);
@@ -306,27 +654,40 @@ ngx_quic_new_connection(ngx_connection_t
     qc->streams.client.uni.max = qc->tp.initial_max_streams_uni;
     qc->streams.client.bidi.max = 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) {
+    ngx_quic_congestion_reset(qc);
+
+    if (!qc->client) {
+        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 (qc->client) {
+        if (ngx_quic_create_server_id(c, qc->incid, 1) != NGX_OK) {
             return NULL;
         }
+
+        dcid.data = qc->incid;
+        dcid.len = NGX_QUIC_SERVER_CID_LEN;
+
+    } else {
+        dcid = pkt->dcid;
     }
 
-    if (ngx_quic_keys_set_initial_secret(qc->keys, &pkt->dcid, c->log)
+    if (ngx_quic_keys_set_initial_secret(qc->keys, &dcid, c->log)
         != NGX_OK)
     {
         return NULL;
     }
 
-    qc->validated = pkt->validated;
+    if (!qc->client) {
+        qc->validated = pkt->validated;
+    }
 
     if (ngx_quic_open_sockets(c, qc, pkt) != NGX_OK) {
         ngx_quic_keys_cleanup(qc->keys);
@@ -454,10 +815,151 @@ ngx_quic_input_handler(ngx_event_t *rev)
 }
 
 
+static void
+ngx_quic_client_input_handler(ngx_event_t *rev)
+{
+    ngx_int_t               rc;
+    ngx_str_t               key;
+    ngx_buf_t              *b;
+    ngx_connection_t       *c;
+    ngx_quic_socket_t      *qsock;
+    ngx_quic_connection_t  *qc;
+
+    ssize_t                 n;
+    ngx_buf_t               dbuf;
+    static u_char           cbuf[65535];
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, rev->log, 0,
+                   "quic client input handler");
+
+    c = rev->data;
+    qc = ngx_quic_get_connection(c);
+
+    c->log->action = "handling quic client input";
+
+    if (rev->timedout) {
+        ngx_log_error(NGX_LOG_INFO, c->log, NGX_ETIMEDOUT,
+                      "quic server timed out");
+        ngx_quic_close_connection(c, NGX_DONE);
+        return;
+    }
+
+    if (c->close) {
+        c->close = 0;
+
+        if (!ngx_exiting) {
+            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;
+    }
+
+    if (!rev->ready) {
+        if (qc->closing) {
+            ngx_quic_close_connection(c, NGX_OK);
+
+        } else if (qc->shutdown) {
+            ngx_quic_shutdown_quic(c);
+        }
+
+        return;
+    }
+
+    for ( ;; ) {
+
+        ngx_memzero(&dbuf, sizeof(ngx_buf_t));
+
+        n = c->recv(c, cbuf, sizeof(cbuf));
+
+        ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic ngx_quic_input_handler recv: fd:%d %z",
+                       c->fd, n);
+
+        if (n == NGX_ERROR) {
+            qc->error_reason = "failed read";
+            ngx_quic_close_connection(c, NGX_ERROR);
+            return;
+        }
+
+        if (n == NGX_AGAIN) {
+            break;
+        }
+
+        /*
+         * actually, since client uses connected UDP socket, there should
+         * be no different addresses of incoming packets;
+         *
+         * we only need to dispatch between different quic sockets,
+         * as client may use different DCIDs
+         */
+
+        if (ngx_quic_get_packet_dcid(c->log, cbuf, n, &key) != NGX_OK) {
+            /* broken packet, ignore */
+            continue;
+        }
+
+        qsock = ngx_quic_find_socket_by_id(c, &key);
+        if (qsock == NULL) {
+            /* client uses unknown dcid, ignore */
+            continue;
+        }
+
+        c->udp = &qsock->udp;
+
+        qsock = ngx_quic_get_socket(c);
+
+        ngx_memcpy(&qsock->sockaddr, c->sockaddr, c->socklen);
+        qsock->socklen = c->socklen;
+
+        dbuf.pos = cbuf;
+        dbuf.last = cbuf + n;
+        dbuf.start = dbuf.pos;
+        dbuf.end = cbuf + sizeof(cbuf);
+
+        c->udp->buffer = &dbuf;
+
+        b = c->udp->buffer;
+
+        rc = ngx_quic_handle_datagram(c, b, NULL);
+
+        if (rc == NGX_ERROR) {
+            ngx_quic_close_connection(c, NGX_ERROR);
+            return;
+        }
+
+        if (rc == NGX_DECLINED) {
+            continue;
+        }
+
+        /* rc == NGX_OK */
+    }
+
+    if (ngx_handle_read_event(rev, 0) != NGX_OK) {
+        ngx_quic_close_connection(c, NGX_ERROR);
+        return;
+    }
+
+    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;
+#if (NGX_STAT_STUB)
+    ngx_uint_t              is_client;
+#endif
     ngx_pool_t             *pool;
     ngx_quic_send_ctx_t    *ctx;
     ngx_quic_connection_t  *qc;
@@ -467,12 +969,19 @@ ngx_quic_close_connection(ngx_connection
     if (qc == NULL) {
         ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
                        "quic packet rejected rc:%i, cleanup connection", rc);
+#if (NGX_STAT_STUB)
+        is_client = 0;
+#endif
         goto quic_done;
     }
 
-    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                   "quic close %s rc:%i",
-                   qc->closing ? "resumed": "initiated", rc);
+#if (NGX_STAT_STUB)
+    is_client = qc->client;
+#endif
+
+    ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic close %s rc:%i c:%p",
+                   qc->closing ? "resumed": "initiated", rc, c);
 
     if (!qc->closing) {
 
@@ -547,6 +1056,8 @@ ngx_quic_close_connection(ngx_connection
     }
 
     if (ngx_quic_close_streams(c, qc) == NGX_AGAIN) {
+        ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic close: waiting for streams");
         return;
     }
 
@@ -571,6 +1082,8 @@ ngx_quic_close_connection(ngx_connection
     }
 
     if (qc->close.timer_set) {
+        ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic close: waiting for timers");
         return;
     }
 
@@ -598,7 +1111,9 @@ quic_done:
     }
 
 #if (NGX_STAT_STUB)
-    (void) ngx_atomic_fetch_add(ngx_stat_active, -1);
+    if (!is_client) {
+        (void) ngx_atomic_fetch_add(ngx_stat_active, -1);
+    }
 #endif
 
     c->destroyed = 1;
@@ -623,11 +1138,32 @@ ngx_quic_finalize_connection(ngx_connect
         return;
     }
 
-    qc->error = err;
-    qc->error_reason = reason;
-    qc->error_app = 1;
     qc->error_ftype = 0;
 
+    /* 10.2.3. Immediate Close during the Handshake
+     *
+     * Sending a CONNECTION_CLOSE of type 0x1d in an Initial or Handshake
+     * packet could expose application state or be used to alter application
+     * state. A CONNECTION_CLOSE of type 0x1d MUST be replaced by a
+     * CONNECTION_CLOSE of type 0x1c when sending the frame in Initial or
+     * Handshake packets.
+     *
+     * Endpoints MUST clear the value of the Reason Phrase field and SHOULD use
+     * the APPLICATION_ERROR code when converting to a CONNECTION_CLOSE of type
+     * 0x1c.
+     */
+
+    if (c->ssl == NULL || !c->ssl->handshaked) {
+        qc->error = NGX_QUIC_ERR_APPLICATION_ERROR;
+        qc->error_reason = "";
+        qc->error_app = 0;
+
+    } else {
+        qc->error = err;
+        qc->error_reason = reason;
+        qc->error_app = 1;
+    }
+
     ngx_post_event(&qc->close, &ngx_posted_events);
 }
 
@@ -667,11 +1203,14 @@ ngx_quic_handle_datagram(ngx_connection_
     size_t                  size;
     u_char                 *p, *start;
     ngx_int_t               rc;
-    ngx_uint_t              good;
+    ngx_uint_t              good, is_server_packet;
     ngx_quic_path_t        *path;
     ngx_quic_header_t       pkt;
     ngx_quic_connection_t  *qc;
 
+    qc = ngx_quic_get_connection(c);
+    is_server_packet = qc ? qc->client : 0;
+
     good = 0;
     path = NULL;
 
@@ -690,6 +1229,7 @@ ngx_quic_handle_datagram(ngx_connection_
         pkt.path = path;
         pkt.flags = p[0];
         pkt.raw->pos++;
+        pkt.server = is_server_packet;
 
         rc = ngx_quic_handle_packet(c, conf, &pkt);
 
@@ -835,10 +1375,18 @@ ngx_quic_handle_packet(ngx_connection_t 
                 }
             }
 
-            if (ngx_quic_check_csid(qc, pkt) != NGX_OK) {
-                return NGX_DECLINED;
+            if (qc->client) {
+
+                if (ngx_quic_pkt_retry(pkt->flags)) {
+                    return ngx_quic_client_start(c, pkt);
+                }
+
+            } else {
+
+                if (ngx_quic_check_csid(qc, pkt) != NGX_OK) {
+                    return NGX_DECLINED;
+                }
             }
-
         }
 
         rc = ngx_quic_handle_payload(c, pkt);
@@ -991,6 +1539,14 @@ ngx_quic_handle_payload(ngx_connection_t
 
     c->log->action = "handling decrypted packet";
 
+    if (qc->client && !qc->server_id_known) {
+        /* server generated new ID, use it (only if decrypted) */
+
+        ngx_memcpy(qc->path->cid->id, pkt->scid.data, pkt->scid.len);
+        qc->path->cid->len = pkt->scid.len;
+        qc->server_id_known = 1;
+    }
+
     if (pkt->path == NULL) {
         rc = ngx_quic_set_path(c, pkt);
         if (rc != NGX_OK) {
@@ -1378,6 +1934,19 @@ ngx_quic_handle_frames(ngx_connection_t 
 
             break;
 
+        case NGX_QUIC_FT_HANDSHAKE_DONE:
+            ngx_quic_streams_notify_write(c);
+            break;
+
+        case NGX_QUIC_FT_NEW_TOKEN:
+
+            if (ngx_quic_handle_new_token_frame(c, &frame.u.token)
+                != NGX_OK)
+            {
+                return NGX_ERROR;
+            }
+            break;
+
         default:
             ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
                            "quic missing frame handler");
diff --git a/src/event/quic/ngx_event_quic.h b/src/event/quic/ngx_event_quic.h
--- a/src/event/quic/ngx_event_quic.h
+++ b/src/event/quic/ngx_event_quic.h
@@ -1,5 +1,6 @@
 
 /*
+ * Copyright (C) 2023 Web Server LLC
  * Copyright (C) Nginx, Inc.
  */
 
@@ -31,6 +32,8 @@
 typedef ngx_int_t (*ngx_quic_init_pt)(ngx_connection_t *c);
 typedef void (*ngx_quic_shutdown_pt)(ngx_connection_t *c);
 
+typedef ngx_int_t (*ngx_quic_init_ssl_pt)(ngx_connection_t *c, void *data);
+
 
 typedef enum {
     NGX_QUIC_STREAM_SEND_READY = 0,
@@ -83,6 +86,7 @@ typedef struct {
 
     u_char                         av_token_key[NGX_QUIC_AV_KEY_LEN];
     u_char                         sr_token_key[NGX_QUIC_SR_KEY_LEN];
+    ngx_str_t                      alpn;
 } ngx_quic_conf_t;
 
 
@@ -113,6 +117,13 @@ struct ngx_quic_stream_s {
 
 void ngx_quic_recvmsg(ngx_event_t *ev);
 void ngx_quic_run(ngx_connection_t *c, ngx_quic_conf_t *conf);
+
+ngx_int_t ngx_quic_create_client(ngx_quic_conf_t *conf, ngx_connection_t *c);
+ngx_int_t ngx_quic_connect(ngx_connection_t *c, ngx_quic_init_ssl_pt init_ssl,
+    void *data);
+void ngx_quic_client_set_ssl_data(ngx_connection_t *c, void *data);
+void *ngx_quic_client_get_ssl_data(ngx_connection_t *c);
+
 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);
diff --git a/src/event/quic/ngx_event_quic_ack.c b/src/event/quic/ngx_event_quic_ack.c
--- a/src/event/quic/ngx_event_quic_ack.c
+++ b/src/event/quic/ngx_event_quic_ack.c
@@ -377,6 +377,19 @@ done:
 }
 
 
+void
+ngx_quic_congestion_reset(ngx_quic_connection_t *qc)
+{
+    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;
+}
+
+
 static void
 ngx_quic_drop_ack_ranges(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx,
     uint64_t pn)
diff --git a/src/event/quic/ngx_event_quic_ack.h b/src/event/quic/ngx_event_quic_ack.h
--- a/src/event/quic/ngx_event_quic_ack.h
+++ b/src/event/quic/ngx_event_quic_ack.h
@@ -1,5 +1,6 @@
 
 /*
+ * Copyright (C) 2023 Web Server LLC
  * Copyright (C) Nginx, Inc.
  */
 
@@ -17,6 +18,7 @@ ngx_int_t ngx_quic_handle_ack_frame(ngx_
 
 void ngx_quic_congestion_ack(ngx_connection_t *c,
     ngx_quic_frame_t *frame);
+void ngx_quic_congestion_reset(ngx_quic_connection_t *qc);
 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);
diff --git a/src/event/quic/ngx_event_quic_connection.h b/src/event/quic/ngx_event_quic_connection.h
--- a/src/event/quic/ngx_event_quic_connection.h
+++ b/src/event/quic/ngx_event_quic_connection.h
@@ -284,6 +284,13 @@ struct ngx_quic_connection_s {
     ngx_uint_t                        shutdown_code;
     const char                       *shutdown_reason;
 
+    u_char                            incid[NGX_QUIC_SERVER_CID_LEN];
+
+    ngx_str_t                         client_retry;
+    ngx_str_t                         client_new_token;
+    ngx_quic_init_ssl_pt              init_ssl;
+    void                             *init_ssl_data;
+
     unsigned                          error_app:1;
     unsigned                          send_timer_set:1;
     unsigned                          closing:1;
@@ -292,6 +299,9 @@ struct ngx_quic_connection_s {
     unsigned                          key_phase:1;
     unsigned                          validated:1;
     unsigned                          client_tp_done:1;
+    unsigned                          server_id_known:1;
+    unsigned                          client:1;
+    unsigned                          switch_keys:1;
 };
 
 
diff --git a/src/event/quic/ngx_event_quic_connid.c b/src/event/quic/ngx_event_quic_connid.c
--- a/src/event/quic/ngx_event_quic_connid.c
+++ b/src/event/quic/ngx_event_quic_connid.c
@@ -25,12 +25,16 @@ static ngx_int_t ngx_quic_send_server_id
 
 
 ngx_int_t
-ngx_quic_create_server_id(ngx_connection_t *c, u_char *id)
+ngx_quic_create_server_id(ngx_connection_t *c, u_char *id, ngx_uint_t client)
 {
     if (RAND_bytes(id, NGX_QUIC_SERVER_CID_LEN) != 1) {
         return NGX_ERROR;
     }
 
+    if (client) {
+        return NGX_OK;
+    }
+
 #if (NGX_QUIC_BPF)
     if (ngx_quic_bpf_attach_id(c, id) != NGX_OK) {
         ngx_log_error(NGX_LOG_ERR, c->log, 0,
@@ -204,6 +208,64 @@ done:
 }
 
 
+ngx_int_t
+ngx_quic_handle_new_token_frame(ngx_connection_t *c,
+    ngx_quic_new_token_frame_t *f)
+{
+    u_char                 *p;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    if (f->length == 0) {
+
+        /*
+         * A client MUST treat receipt of a NEW_TOKEN frame with an empty
+         * Token field as a connection error of type FRAME_ENCODING_ERROR.
+         */
+        qc->error = NGX_QUIC_ERR_FRAME_ENCODING_ERROR;
+        qc->error_reason = "zero length NEW_TOKEN frame";
+
+        ngx_log_error(NGX_LOG_ERR, c->log, ngx_socket_errno,
+                      "quic NEW_TOKEN frame of zero length");
+        return NGX_ERROR;
+    }
+
+    if (f->length > NGX_QUIC_MAX_NEW_TOKEN) {
+        ngx_log_error(NGX_LOG_ERR, c->log, ngx_socket_errno,
+                      "quic NEW_TOKEN frame is too big: %ui", f->length);
+        return NGX_ERROR;
+    }
+
+    p = qc->client_new_token.data;
+    if (p == NULL) {
+
+        p = ngx_pnalloc(c->pool, NGX_QUIC_MAX_NEW_TOKEN);
+        if (p == NULL) {
+            return NGX_ERROR;
+        }
+
+        qc->client_new_token.data = p;
+    }
+
+    /*
+     * currently, keep only one token, and rewrite if multiple
+     * tokens received.
+     *
+     * the token is for use in 'future connections', so it is not
+     * really used currently.
+     */
+
+    ngx_memcpy(p, f->data, f->length);
+    qc->client_new_token.len = f->length;
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic new token received: %*xs", f->length, f->data);
+
+    return NGX_OK;
+}
+
+
 static ngx_int_t
 ngx_quic_retire_client_id(ngx_connection_t *c, ngx_quic_client_id_t *cid)
 {
diff --git a/src/event/quic/ngx_event_quic_connid.h b/src/event/quic/ngx_event_quic_connid.h
--- a/src/event/quic/ngx_event_quic_connid.h
+++ b/src/event/quic/ngx_event_quic_connid.h
@@ -1,5 +1,6 @@
 
 /*
+ * Copyright (C) 2023 Web Server LLC
  * Copyright (C) Nginx, Inc.
  */
 
@@ -17,8 +18,12 @@ ngx_int_t ngx_quic_handle_retire_connect
 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_handle_new_token_frame(ngx_connection_t *c,
+    ngx_quic_new_token_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_int_t ngx_quic_create_server_id(ngx_connection_t *c, u_char *id,
+    ngx_uint_t client);
 
 ngx_quic_client_id_t *ngx_quic_create_client_id(ngx_connection_t *c,
     ngx_str_t *id, uint64_t seqnum, u_char *token);
diff --git a/src/event/quic/ngx_event_quic_migration.c b/src/event/quic/ngx_event_quic_migration.c
--- a/src/event/quic/ngx_event_quic_migration.c
+++ b/src/event/quic/ngx_event_quic_migration.c
@@ -181,13 +181,7 @@ valid:
         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_congestion_reset(qc);
 
         ngx_quic_init_rtt(qc);
     }
@@ -313,7 +307,7 @@ ngx_quic_set_path(ngx_connection_t *c, n
 
     len = pkt->raw->last - pkt->raw->start;
 
-    if (c->udp->buffer == NULL) {
+    if (c->udp->buffer == NULL && !qc->client) {
         /* first ever packet in connection, path already exists  */
         path = qc->path;
         goto update;
diff --git a/src/event/quic/ngx_event_quic_openssl_compat.c b/src/event/quic/ngx_event_quic_openssl_compat.c
--- a/src/event/quic/ngx_event_quic_openssl_compat.c
+++ b/src/event/quic/ngx_event_quic_openssl_compat.c
@@ -208,6 +208,10 @@ ngx_quic_compat_keylog_callback(const SS
     com = qc->compat;
     cipher = SSL_get_current_cipher(ssl);
 
+    if (qc->client) {
+        write = !write;
+    }
+
     if (write) {
         com->method->set_write_secret((SSL *) ssl, level, cipher, secret, n);
         com->write_level = level;
diff --git a/src/event/quic/ngx_event_quic_output.c b/src/event/quic/ngx_event_quic_output.c
--- a/src/event/quic/ngx_event_quic_output.c
+++ b/src/event/quic/ngx_event_quic_output.c
@@ -1,5 +1,6 @@
 
 /*
+ * Copyright (C) 2023 Web Server LLC
  * Copyright (C) Nginx, Inc.
  */
 
@@ -393,12 +394,13 @@ 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;
+    size_t                  clen;
+    ssize_t                 n;
+    uint16_t               *valp;
+    struct iovec            iov;
+    struct msghdr           msg;
+    struct cmsghdr         *cmsg;
+    ngx_quic_connection_t  *qc;
 
 #if (NGX_HAVE_ADDRINFO_CMSG)
     char             msg_control[CMSG_SPACE(sizeof(uint16_t))
@@ -416,8 +418,13 @@ ngx_quic_send_segments(ngx_connection_t 
     msg.msg_iov = &iov;
     msg.msg_iovlen = 1;
 
-    msg.msg_name = sockaddr;
-    msg.msg_namelen = socklen;
+    qc = ngx_quic_get_connection(c);
+
+    if (qc == NULL || !qc->client) {
+        /* TODO: *BSD: socket already connected */
+        msg.msg_name = sockaddr;
+        msg.msg_namelen = socklen;
+    }
 
     msg.msg_control = msg_control;
     msg.msg_controllen = sizeof(msg_control);
@@ -467,15 +474,36 @@ ngx_quic_get_padding_level(ngx_connectio
 
     /*
      * RFC 9000, 14.1.  Initial Datagram Size
-     *
+     */
+
+    qc = ngx_quic_get_connection(c);
+    ctx = ngx_quic_get_send_ctx(qc, ssl_encryption_initial);
+
+    if (qc->client) {
+
+        /*
+         * A client MUST expand the payload of all UDP datagrams carrying
+         * Initial packets to at least the smallest allowed maximum datagram
+         * size of 1200 bytes
+         */
+
+        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;
+    }
+
+    /*
      * 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))
@@ -660,6 +688,11 @@ ngx_quic_init_packet(ngx_connection_t *c
     if (ctx->level == ssl_encryption_initial) {
         pkt->flags |= NGX_QUIC_PKT_LONG | NGX_QUIC_PKT_INITIAL;
 
+        if (qc->client_retry.len) {
+            pkt->token = qc->client_retry;
+            pkt->token.len = qc->client_retry.len;
+        }
+
     } else if (ctx->level == ssl_encryption_handshake) {
         pkt->flags |= NGX_QUIC_PKT_LONG | NGX_QUIC_PKT_HANDSHAKE;
 
@@ -688,13 +721,14 @@ 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;
+    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))];
+    struct cmsghdr         *cmsg;
+    char                    msg_control[CMSG_SPACE(sizeof(ngx_addrinfo_t))];
 #endif
+    ngx_quic_connection_t  *qc;
 
     ngx_memzero(&msg, sizeof(struct msghdr));
 
@@ -704,8 +738,13 @@ ngx_quic_send(ngx_connection_t *c, u_cha
     msg.msg_iov = &iov;
     msg.msg_iovlen = 1;
 
-    msg.msg_name = sockaddr;
-    msg.msg_namelen = socklen;
+    qc = ngx_quic_get_connection(c);
+
+    if (qc == NULL || !qc->client) {
+        /* TODO: *BSD: socket already connected */
+        msg.msg_name = sockaddr;
+        msg.msg_namelen = socklen;
+    }
 
 #if (NGX_HAVE_ADDRINFO_CMSG)
     if (c->listening && c->listening->wildcard && c->local_sockaddr) {
@@ -920,6 +959,11 @@ ngx_quic_send_early_cc(ngx_connection_t 
 
     ngx_memzero(&keys, sizeof(ngx_quic_keys_t));
 
+    /*
+     * ngx_quic_send_early_cc() is only called from token check, i.e. server
+     * thus keys.client = 0
+     */
+
     pkt.keys = &keys;
 
     if (ngx_quic_keys_set_initial_secret(pkt.keys, &inpkt->dcid, c->log)
@@ -1216,10 +1260,18 @@ ngx_quic_frame_sendto(ngx_connection_t *
 
     ngx_quic_init_packet(c, ctx, &pkt, path);
 
+    if (qc->client && frame->level == ssl_encryption_initial
+        && min < NGX_QUIC_MIN_INITIAL_SIZE)
+    {
+        /* client must expand all initial packets */
+        min = NGX_QUIC_MIN_INITIAL_SIZE;
+    }
+
     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);
 
@@ -1304,9 +1356,26 @@ ngx_quic_frame_sendto(ngx_connection_t *
 size_t
 ngx_quic_path_limit(ngx_connection_t *c, ngx_quic_path_t *path, size_t size)
 {
-    off_t  max;
+    off_t                   max;
+    ngx_quic_connection_t  *qc;
 
     if (!path->validated) {
+
+        qc = ngx_quic_get_connection(c);
+
+        if (qc->client) {
+
+            /*
+             * RFC 9000  21.1.1.1. Anti-Amplification
+             *
+             *  The anti-amplification limit does not apply to clients when
+             *  establishing a new connection or when initiating connection
+             *  migration.
+             */
+
+            return size;
+        }
+
         max = path->received * 3;
         max = (path->sent >= max) ? 0 : max - path->sent;
 
diff --git a/src/event/quic/ngx_event_quic_protection.c b/src/event/quic/ngx_event_quic_protection.c
--- a/src/event/quic/ngx_event_quic_protection.c
+++ b/src/event/quic/ngx_event_quic_protection.c
@@ -1,5 +1,6 @@
 
 /*
+ * Copyright (C) 2023 Web Server LLC
  * Copyright (C) Nginx, Inc.
  */
 
@@ -40,6 +41,8 @@ static ngx_int_t ngx_quic_crypto_hp_init
 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_quic_secret_t *ngx_quic_select_secret(ngx_quic_keys_t *keys,
+    enum ssl_encryption_level_t level, ngx_uint_t is_write);
 
 static ngx_int_t ngx_quic_create_packet(ngx_quic_header_t *pkt,
     ngx_str_t *res);
@@ -190,13 +193,13 @@ ngx_quic_keys_set_initial_secret(ngx_qui
         return NGX_ERROR;
     }
 
-    if (ngx_quic_crypto_init(ciphers.c, client, &client_key, 0, log)
+    if (ngx_quic_crypto_init(ciphers.c, client, &client_key, keys->client, log)
         == NGX_ERROR)
     {
         return NGX_ERROR;
     }
 
-    if (ngx_quic_crypto_init(ciphers.c, server, &server_key, 1, log)
+    if (ngx_quic_crypto_init(ciphers.c, server, &server_key, !keys->client, log)
         == NGX_ERROR)
     {
         goto failed;
@@ -649,6 +652,17 @@ ngx_quic_crypto_hp_cleanup(ngx_quic_secr
 }
 
 
+static ngx_quic_secret_t *
+ngx_quic_select_secret(ngx_quic_keys_t *keys,
+    enum ssl_encryption_level_t level, ngx_uint_t is_write)
+{
+    return keys->client ? (is_write ? &keys->secrets[level].client
+                                    : &keys->secrets[level].server)
+                        : (is_write ? &keys->secrets[level].server
+                                    : &keys->secrets[level].client);
+}
+
+
 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,
@@ -662,8 +676,7 @@ ngx_quic_keys_set_encryption_secret(ngx_
     ngx_quic_secret_t   *peer_secret;
     ngx_quic_ciphers_t   ciphers;
 
-    peer_secret = is_write ? &keys->secrets[level].server
-                           : &keys->secrets[level].client;
+    peer_secret = ngx_quic_select_secret(keys, level, is_write);
 
     keys->cipher = SSL_CIPHER_get_id(cipher);
 
@@ -720,11 +733,11 @@ 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;
-    }
+    ngx_quic_secret_t  *s;
 
-    return keys->secrets[level].server.ctx != NULL;
+    s = ngx_quic_select_secret(keys, level, is_write);
+
+    return (s->ctx != NULL);
 }
 
 
@@ -827,13 +840,15 @@ ngx_quic_keys_update(ngx_event_t *ev)
         }
     }
 
-    if (ngx_quic_crypto_init(ciphers.c, &next->client, &client_key, 0, c->log)
+    if (ngx_quic_crypto_init(ciphers.c, &next->client, &client_key,
+                             qc->client, c->log)
         == NGX_ERROR)
     {
         goto failed;
     }
 
-    if (ngx_quic_crypto_init(ciphers.c, &next->server, &server_key, 1, c->log)
+    if (ngx_quic_crypto_init(ciphers.c, &next->server, &server_key,
+                             !qc->client, c->log)
         == NGX_ERROR)
     {
         goto failed;
@@ -847,6 +862,16 @@ ngx_quic_keys_update(ngx_event_t *ev)
     ngx_explicit_memzero(client_key.data, client_key.len);
     ngx_explicit_memzero(server_key.data, server_key.len);
 
+    if (qc->switch_keys) {
+        qc->key_phase ^= 1;
+
+        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic switching keys, phase: %ui", qc->key_phase);
+
+        ngx_quic_keys_switch(c, qc->keys);
+        qc->switch_keys = 0;
+    }
+
     return;
 
 failed:
@@ -897,7 +922,8 @@ ngx_quic_create_packet(ngx_quic_header_t
                    "quic ad len:%uz %xV", ad.len, &ad);
 #endif
 
-    secret = &pkt->keys->secrets[pkt->level].server;
+    secret = pkt->keys->client ? &pkt->keys->secrets[pkt->level].client
+                               : &pkt->keys->secrets[pkt->level].server;
 
     ngx_memcpy(nonce, secret->iv.data, secret->iv.len);
     ngx_quic_compute_nonce(nonce, sizeof(nonce), pkt->number);
@@ -926,11 +952,9 @@ ngx_quic_create_packet(ngx_quic_header_t
 }
 
 
-static ngx_int_t
-ngx_quic_create_retry_packet(ngx_quic_header_t *pkt, ngx_str_t *res)
+ngx_int_t
+ngx_quic_retry_seal(ngx_str_t *ad, ngx_str_t *itag, ngx_log_t *log)
 {
-    u_char              *start;
-    ngx_str_t            ad, itag;
     ngx_quic_md_t        key;
     ngx_quic_secret_t    secret;
     ngx_quic_ciphers_t   ciphers;
@@ -942,15 +966,10 @@ ngx_quic_create_retry_packet(ngx_quic_he
         "\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);
+                   "quic retry itag len:%uz %xV", ad->len, ad);
 #endif
 
     if (ngx_quic_ciphers(NGX_QUIC_INITIAL_CIPHER, &ciphers) == NGX_ERROR) {
@@ -961,13 +980,13 @@ ngx_quic_create_retry_packet(ngx_quic_he
     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)
+    if (ngx_quic_crypto_init(ciphers.c, &secret, &key, 1, log)
         == NGX_ERROR)
     {
         return NGX_ERROR;
     }
 
-    if (ngx_quic_crypto_seal(&secret, &itag, nonce, &in, &ad, pkt->log)
+    if (ngx_quic_crypto_seal(&secret, itag, nonce, &in, ad, log)
         != NGX_OK)
     {
         ngx_quic_crypto_cleanup(&secret);
@@ -976,6 +995,26 @@ ngx_quic_create_retry_packet(ngx_quic_he
 
     ngx_quic_crypto_cleanup(&secret);
 
+    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;
+
+    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;
+
+    if (ngx_quic_retry_seal(&ad, &itag, pkt->log) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
     res->len = itag.data + itag.len - start;
     res->data = start;
 
@@ -1109,10 +1148,14 @@ ngx_quic_decrypt(ngx_quic_header_t *pkt,
     ngx_int_t           pnl;
     ngx_str_t           in, ad;
     ngx_uint_t          key_phase;
-    ngx_quic_secret_t  *secret;
+    ngx_quic_secret_t  *secret, *next;
     uint8_t             nonce[NGX_QUIC_IV_LEN], mask[NGX_QUIC_HP_LEN];
 
-    secret = &pkt->keys->secrets[pkt->level].client;
+    secret = pkt->keys->client ? &pkt->keys->secrets[pkt->level].server
+                               : &pkt->keys->secrets[pkt->level].client;
+
+    next = pkt->keys->client ? &pkt->keys->next_key.server
+                             : &pkt->keys->next_key.client;
 
     p = pkt->raw->pos;
     len = pkt->data + pkt->len - p;
@@ -1143,8 +1186,12 @@ ngx_quic_decrypt(ngx_quic_header_t *pkt,
     if (ngx_quic_short_pkt(pkt->flags)) {
         key_phase = (pkt->flags & NGX_QUIC_PKT_KPHASE) != 0;
 
-        if (key_phase != pkt->key_phase) {
-            secret = &pkt->keys->next_key.client;
+        /*
+         * we don't want to switch to next key if we don't have it yet;
+         * the fact of being here may be caused by the header corruption
+         */
+        if (key_phase != pkt->key_phase && next->ctx != NULL) {
+            secret = next;
             pkt->key_update = 1;
         }
     }
diff --git a/src/event/quic/ngx_event_quic_protection.h b/src/event/quic/ngx_event_quic_protection.h
--- a/src/event/quic/ngx_event_quic_protection.h
+++ b/src/event/quic/ngx_event_quic_protection.h
@@ -1,5 +1,6 @@
 
 /*
+ * Copyright (C) 2023 Web Server LLC
  * Copyright (C) Nginx, Inc.
  */
 
@@ -64,6 +65,7 @@ struct ngx_quic_keys_s {
     ngx_quic_secrets_t        secrets[NGX_QUIC_ENCRYPTION_LAST];
     ngx_quic_secrets_t        next_key;
     ngx_uint_t                cipher;
+    ngx_uint_t                client;
 };
 
 
@@ -115,6 +117,7 @@ ngx_int_t ngx_quic_crypto_seal(ngx_quic_
 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);
+ngx_int_t ngx_quic_retry_seal(ngx_str_t *ad, ngx_str_t *itag, 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
--- a/src/event/quic/ngx_event_quic_socket.c
+++ b/src/event/quic/ngx_event_quic_socket.c
@@ -1,5 +1,6 @@
 
 /*
+ * Copyright (C) 2023 Web Server LLC
  * Copyright (C) Nginx, Inc.
  */
 
@@ -35,10 +36,15 @@ ngx_quic_open_sockets(ngx_connection_t *
     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;
+    if (qc->client) {
+        qc->tp.original_dcid.len = 0;
+
+    } else {
+        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) */
@@ -66,6 +72,10 @@ ngx_quic_open_sockets(ngx_connection_t *
 
     /* ngx_quic_get_connection(c) macro is now usable */
 
+    if (qc->client) {
+        return NGX_OK;
+    }
+
     /* we have a client identified by scid */
     cid = ngx_quic_create_client_id(c, &pkt->scid, 0, NULL);
     if (cid == NULL) {
@@ -104,7 +114,10 @@ ngx_quic_open_sockets(ngx_connection_t *
 
 failed:
 
-    ngx_rbtree_delete(&c->listening->rbtree, &qsock->udp.node);
+    if (!qc->client) {
+        ngx_rbtree_delete(&c->listening->rbtree, &qsock->udp.node);
+    }
+
     c->udp = NULL;
 
     return NGX_ERROR;
@@ -135,7 +148,7 @@ ngx_quic_create_socket(ngx_connection_t 
     }
 
     sock->sid.len = NGX_QUIC_SERVER_CID_LEN;
-    if (ngx_quic_create_server_id(c, sock->sid.id) != NGX_OK) {
+    if (ngx_quic_create_server_id(c, sock->sid.id, qc->client) != NGX_OK) {
         return NULL;
     }
 
@@ -155,7 +168,10 @@ ngx_quic_close_socket(ngx_connection_t *
     ngx_queue_remove(&qsock->queue);
     ngx_queue_insert_head(&qc->free_sockets, &qsock->queue);
 
-    ngx_rbtree_delete(&c->listening->rbtree, &qsock->udp.node);
+    if (!qc->client) {
+        ngx_rbtree_delete(&c->listening->rbtree, &qsock->udp.node);
+    }
+
     qc->nsockets--;
 
     ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
@@ -176,11 +192,13 @@ ngx_quic_listen(ngx_connection_t *c, ngx
     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;
+    if (!qc->client) {
+        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_rbtree_insert(&c->listening->rbtree, &qsock->udp.node);
+    }
 
     ngx_queue_insert_tail(&qc->sockets, &qsock->queue);
 
@@ -235,3 +253,34 @@ ngx_quic_find_socket(ngx_connection_t *c
 
     return NULL;
 }
+
+
+ngx_quic_socket_t *
+ngx_quic_find_socket_by_id(ngx_connection_t *c, ngx_str_t *key)
+{
+    ngx_queue_t            *q;
+    ngx_quic_socket_t      *qsock;
+    ngx_quic_connection_t  *qc;
+
+    if (key->len == 0) {
+        return NULL;
+    }
+
+    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 (ngx_memn2cmp(key->data, qsock->sid.id,
+                         key->len, qsock->sid.len)
+            == 0)
+        {
+            return qsock;
+        }
+    }
+
+    return NULL;
+}
diff --git a/src/event/quic/ngx_event_quic_socket.h b/src/event/quic/ngx_event_quic_socket.h
--- a/src/event/quic/ngx_event_quic_socket.h
+++ b/src/event/quic/ngx_event_quic_socket.h
@@ -1,5 +1,6 @@
 
 /*
+ * Copyright (C) 2023 Web Server LLC
  * Copyright (C) Nginx, Inc.
  */
 
@@ -23,6 +24,7 @@ ngx_int_t ngx_quic_listen(ngx_connection
 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);
-
+ngx_quic_socket_t *ngx_quic_find_socket_by_id(ngx_connection_t *c,
+    ngx_str_t *key);
 
 #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
--- a/src/event/quic/ngx_event_quic_ssl.c
+++ b/src/event/quic/ngx_event_quic_ssl.c
@@ -195,18 +195,19 @@ ngx_quic_add_handshake_data(ngx_ssl_conn
          */
 
 #if defined(TLSEXT_TYPE_application_layer_protocol_negotiation)
+        if (!qc->client) {
 
-        SSL_get0_alpn_selected(ssl_conn, &alpn_data, &alpn_len);
+            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";
+            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;
+                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,
@@ -216,7 +217,37 @@ ngx_quic_add_handshake_data(ngx_ssl_conn
                        "quic SSL_get_peer_quic_transport_params():"
                        " params_len:%ui", client_params_len);
 
-        if (client_params_len == 0) {
+        if (client_params_len) {
+
+            p = (u_char *) client_params;
+            end = p + client_params_len;
+
+            /* defaults for parameters not sent by client */
+            ngx_memcpy(&peer_tp, &qc->peer_tp, sizeof(ngx_quic_tp_t));
+
+            if (ngx_quic_parse_transport_params(p, end, &peer_tp, c->log,
+                                                qc->client)
+                != 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, &peer_tp) != NGX_OK) {
+                return 0;
+            }
+
+            qc->client_tp_done = 1;
+
+        } else {
+
+            if (qc->client) {
+                /* when we send 1st packet, no response from server yet */
+                goto skip;
+            }
+
             /* RFC 9001, 8.2.  QUIC Transport Parameters Extension */
             qc->error = NGX_QUIC_ERR_CRYPTO(SSL_AD_MISSING_EXTENSION);
             qc->error_reason = "missing transport parameters";
@@ -225,28 +256,9 @@ ngx_quic_add_handshake_data(ngx_ssl_conn
                           "missing transport parameters");
             return 0;
         }
-
-        p = (u_char *) client_params;
-        end = p + client_params_len;
-
-        /* defaults for parameters not sent by client */
-        ngx_memcpy(&peer_tp, &qc->peer_tp, sizeof(ngx_quic_tp_t));
+    }
 
-        if (ngx_quic_parse_transport_params(p, end, &peer_tp, 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, &peer_tp) != NGX_OK) {
-            return 0;
-        }
-
-        qc->client_tp_done = 1;
-    }
+skip:
 
     ctx = ngx_quic_get_send_ctx(qc, level);
 
@@ -450,21 +462,51 @@ ngx_quic_crypto_input(ngx_connection_t *
     ngx_ssl_handshake_log(c);
 #endif
 
-    c->ssl->handshaked = 1;
-
-    frame = ngx_quic_alloc_frame(c);
-    if (frame == NULL) {
+#if !defined(NGX_QUIC_OPENSSL_COMPAT)
+    /* missing in compat, session reuse is not going to work there */
+    if (SSL_process_quic_post_handshake(c->ssl->connection) != 1) {
         return NGX_ERROR;
     }
+#endif
 
-    frame->level = ssl_encryption_application;
-    frame->type = NGX_QUIC_FT_HANDSHAKE_DONE;
-    ngx_quic_queue_frame(qc, frame);
+    c->ssl->handshaked = 1;
+
+    if (qc->client) {
+        if (level != ssl_encryption_application) {
+            /*
+             * Server sends TLS Handshake Finished twice:
+             * in handshake and application level packets;
+             *
+             * perform h/s complete actions only once, when app received
+             */
+
+            return NGX_OK;
+        }
+
+        /* flush output: need to ack with previous keys */
+        if (ngx_quic_output(c) != NGX_OK) {
+            return NGX_ERROR;
+        }
 
-    if (qc->conf->retry) {
-        if (ngx_quic_send_new_token(c, qc->path) != NGX_OK) {
+        /* initiate key update procedure */
+        qc->switch_keys = 1;
+
+    } else {
+
+        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;
+            }
+        }
     }
 
     /*
@@ -485,7 +527,7 @@ ngx_quic_crypto_input(ngx_connection_t *
 
     ngx_quic_discover_path_mtu(c, qc->path);
 
-    /* start accepting clients on negotiated number of server ids */
+    /* start accepting packets on negotiated number of server ids */
     if (ngx_quic_create_sockets(c) != NGX_OK) {
         return NGX_ERROR;
     }
@@ -505,6 +547,7 @@ ngx_quic_init_connection(ngx_connection_
     size_t                   clen;
     ssize_t                  len;
     ngx_str_t                dcid;
+    ngx_uint_t               flags;
     ngx_ssl_conn_t          *ssl_conn;
     ngx_quic_socket_t       *qsock;
     ngx_quic_connection_t   *qc;
@@ -512,7 +555,14 @@ ngx_quic_init_connection(ngx_connection_
 
     qc = ngx_quic_get_connection(c);
 
-    if (ngx_ssl_create_connection(qc->conf->ssl, c, 0) != NGX_OK) {
+    flags = qc->client ? NGX_SSL_CLIENT : 0;
+
+    if (c->ssl) {
+        /* retry packet case: reinit ssl */
+        SSL_free(c->ssl->connection);
+    }
+
+    if (ngx_ssl_create_connection(qc->conf->ssl, c, flags) != NGX_OK) {
         return NGX_ERROR;
     }
 
@@ -538,6 +588,10 @@ ngx_quic_init_connection(ngx_connection_
         return NGX_ERROR;
     }
 
+    if (qc->init_ssl && qc->init_ssl(c, qc->init_ssl_data) != NGX_OK) {
+        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);
@@ -555,7 +609,8 @@ ngx_quic_init_connection(ngx_connection_
         return NGX_ERROR;
     }
 
-    len = ngx_quic_create_transport_params(NULL, NULL, &qc->tp, &clen);
+    len = ngx_quic_create_transport_params(NULL, NULL, &qc->tp, &clen,
+                                           qc->client);
     /* always succeeds */
 
     p = ngx_pnalloc(c->pool, len);
@@ -563,7 +618,8 @@ ngx_quic_init_connection(ngx_connection_
         return NGX_ERROR;
     }
 
-    len = ngx_quic_create_transport_params(p, p + len, &qc->tp, NULL);
+    len = ngx_quic_create_transport_params(p, p + len, &qc->tp, NULL,
+                                           qc->client);
     if (len < 0) {
         return NGX_ERROR;
     }
@@ -589,3 +645,33 @@ ngx_quic_init_connection(ngx_connection_
 
     return NGX_OK;
 }
+
+
+ngx_int_t
+ngx_quic_client_handshake(ngx_connection_t *c)
+{
+    int              n, sslerr;
+    ngx_ssl_conn_t  *ssl_conn;
+
+    if (ngx_quic_init_connection(c) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    ssl_conn = c->ssl->connection;
+
+    n = SSL_do_handshake(ssl_conn);
+
+    if (n <= 0) {
+        sslerr = SSL_get_error(ssl_conn, n);
+
+        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, "SSL_get_error: %d",
+                       sslerr);
+
+        if (sslerr != SSL_ERROR_WANT_READ) {
+            ngx_ssl_error(NGX_LOG_ERR, c->log, 0, "SSL_do_handshake() failed");
+            return NGX_ERROR;
+        }
+    }
+
+    return NGX_OK;
+}
diff --git a/src/event/quic/ngx_event_quic_ssl.h b/src/event/quic/ngx_event_quic_ssl.h
--- a/src/event/quic/ngx_event_quic_ssl.h
+++ b/src/event/quic/ngx_event_quic_ssl.h
@@ -1,5 +1,6 @@
 
 /*
+ * Copyright (C) 2023 Web Server LLC
  * Copyright (C) Nginx, Inc.
  */
 
@@ -16,4 +17,6 @@ ngx_int_t ngx_quic_init_connection(ngx_c
 ngx_int_t ngx_quic_handle_crypto_frame(ngx_connection_t *c,
     ngx_quic_header_t *pkt, ngx_quic_frame_t *frame);
 
+ngx_int_t ngx_quic_client_handshake(ngx_connection_t *c);
+
 #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
--- a/src/event/quic/ngx_event_quic_streams.c
+++ b/src/event/quic/ngx_event_quic_streams.c
@@ -13,6 +13,17 @@
 
 #define NGX_QUIC_STREAM_GONE     (void *) -1
 
+#define ngx_quic_uni_stream(id)                                               \
+    ((id) & NGX_QUIC_STREAM_UNIDIRECTIONAL)
+
+#define ngx_quic_out_stream(qc, id)                                           \
+    (((qc)->client && (((id) & NGX_QUIC_STREAM_SERVER_INITIATED) == 0))       \
+     || ((!(qc)->client) && ((id) & NGX_QUIC_STREAM_SERVER_INITIATED)))
+
+#define ngx_quic_input_stream(qc, id)                                         \
+    (((qc)->client && ((id) & NGX_QUIC_STREAM_SERVER_INITIATED))              \
+     || ((!(qc)->client) && (((id) & NGX_QUIC_STREAM_SERVER_INITIATED) == 0)))
+
 
 static ngx_int_t ngx_quic_do_reset_stream(ngx_quic_stream_t *qs,
     ngx_uint_t err);
@@ -25,6 +36,8 @@ static void ngx_quic_init_streams_handle
 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_stream_init_state(ngx_quic_connection_t *qc,
+    ngx_quic_stream_t *qs);
 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);
@@ -32,6 +45,10 @@ static ssize_t ngx_quic_stream_send(ngx_
     size_t size);
 static ngx_chain_t *ngx_quic_stream_send_chain(ngx_connection_t *c,
     ngx_chain_t *in, off_t limit);
+static ssize_t ngx_quic_stream_recv_chain(ngx_connection_t *c,
+    ngx_chain_t *in, off_t limit);
+static void ngx_quic_copy_chain_data(ngx_chain_t *dst_chain,
+    ngx_chain_t *src_chain);
 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);
@@ -46,62 +63,51 @@ static void ngx_quic_set_event(ngx_event
 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;
+    uint64_t                 id;
+    ngx_connection_t        *pc, *sc;
+    ngx_quic_stream_t       *qs;
+    ngx_quic_connection_t   *qc;
+    ngx_quic_stream_ctl_t   *sctl;
+    ngx_quic_stream_peer_t  *peer;
 
     pc = c->quic ? c->quic->parent : c;
     qc = ngx_quic_get_connection(pc);
 
     if (qc->closing) {
+        ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic open stream failed: already closing");
+        return NULL;
+    }
+
+    peer = qc->client ? &qc->streams.client : &qc->streams.server;
+    sctl = bidi ? &peer->bidi : &peer->uni;
+
+    if (sctl->count >= sctl->max) {
+        ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic too many %s %s streams:%uL",
+                       qc->client ? "client" : "server", bidi ? "bidi": "uni",
+                       sctl->count);
         return NULL;
     }
 
-    if (bidi) {
-        if (qc->streams.server.bidi.count
-            >= qc->streams.server.bidi.max)
-        {
-            ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                           "quic too many server bidi streams:%uL",
-                           qc->streams.server.bidi.count);
-            return NULL;
-        }
-
-        id = (qc->streams.server.bidi.count << 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.bidi.count,
-                       qc->streams.server.bidi.max, id);
-
-        qc->streams.server.bidi.count++;
-
-    } else {
-        if (qc->streams.server.uni.count
-            >= qc->streams.server.uni.max)
-        {
-            ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                           "quic too many server uni streams:%uL",
-                           qc->streams.server.uni.count);
-            return NULL;
-        }
-
-        id = (qc->streams.server.uni.count << 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.uni.count,
-                       qc->streams.server.uni.max, id);
-
-        qc->streams.server.uni.count++;
+    id = (sctl->count << 2);
+
+    if (!bidi) {
+        id |= NGX_QUIC_STREAM_UNIDIRECTIONAL;
+    }
+
+    if (!qc->client) {
+        id |= NGX_QUIC_STREAM_SERVER_INITIATED;
     }
 
+    ngx_log_debug5(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic creating %s %s stream"
+                   " streams:%uL max:%uL id:0x%xL",
+                   qc->client ? "client" : "server", bidi ? "bidi" : "uni",
+                   sctl->count, sctl->max, id);
+
+    sctl->count++;
+
     qs = ngx_quic_create_stream(pc, id);
     if (qs == NULL) {
         return NULL;
@@ -237,6 +243,11 @@ ngx_quic_close_streams(ngx_connection_t 
 ngx_int_t
 ngx_quic_reset_stream(ngx_connection_t *c, ngx_uint_t err)
 {
+    if (!c->quic->parent->ssl->handshaked) {
+        /* the stream was created early, do not try to send anything */
+        return NGX_OK;
+    }
+
     return ngx_quic_do_reset_stream(c->quic, err);
 }
 
@@ -290,6 +301,15 @@ ngx_quic_do_reset_stream(ngx_quic_stream
 ngx_int_t
 ngx_quic_shutdown_stream(ngx_connection_t *c, int how)
 {
+    ngx_connection_t  *pc;
+
+    pc = c->quic->parent;
+
+    if (!pc->ssl->handshaked) {
+        /* the stream was created early, do not try to send anything */
+        return NGX_OK;
+    }
+
     if (how == NGX_RDWR_SHUTDOWN || how == NGX_WRITE_SHUTDOWN) {
         if (ngx_quic_shutdown_stream_send(c) != NGX_OK) {
             return NGX_ERROR;
@@ -374,10 +394,12 @@ ngx_quic_shutdown_stream_recv(ngx_connec
 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;
+    uint64_t                 min_id;
+    ngx_event_t             *rev;
+    ngx_quic_stream_t       *qs;
+    ngx_quic_connection_t   *qc;
+    ngx_quic_stream_ctl_t   *sctl;
+    ngx_quic_stream_peer_t  *peer;
 
     qc = ngx_quic_get_connection(c);
 
@@ -394,54 +416,45 @@ ngx_quic_get_stream(ngx_connection_t *c,
     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.uni.count) {
-                return NGX_QUIC_STREAM_GONE;
-            }
-
-            qc->error = NGX_QUIC_ERR_STREAM_STATE_ERROR;
-            return NULL;
-        }
-
-        if ((id >> 2) < qc->streams.client.uni.count) {
+    if (ngx_quic_out_stream(qc, id)) {
+        /* stream is initiated by us, but peer is trying to use it */
+
+        peer = qc->client ? &qc->streams.client : &qc->streams.server;
+
+        sctl = ngx_quic_uni_stream(id) ? &peer->uni : &peer->bidi;
+
+        if ((id >> 2) < sctl->count) {
             return NGX_QUIC_STREAM_GONE;
         }
 
-        if ((id >> 2) >= qc->streams.client.uni.max) {
-            qc->error = NGX_QUIC_ERR_STREAM_LIMIT_ERROR;
-            return NULL;
-        }
-
-        min_id = (qc->streams.client.uni.count << 2)
-                 | NGX_QUIC_STREAM_UNIDIRECTIONAL;
-        qc->streams.client.uni.count = (id >> 2) + 1;
-
-    } else {
-
-        if (id & NGX_QUIC_STREAM_SERVER_INITIATED) {
-            if ((id >> 2) < qc->streams.server.bidi.count) {
-                return NGX_QUIC_STREAM_GONE;
-            }
-
-            qc->error = NGX_QUIC_ERR_STREAM_STATE_ERROR;
-            return NULL;
-        }
-
-        if ((id >> 2) < qc->streams.client.bidi.count) {
-            return NGX_QUIC_STREAM_GONE;
-        }
-
-        if ((id >> 2) >= qc->streams.client.bidi.max) {
-            qc->error = NGX_QUIC_ERR_STREAM_LIMIT_ERROR;
-            return NULL;
-        }
-
-        min_id = (qc->streams.client.bidi.count << 2);
-        qc->streams.client.bidi.count = (id >> 2) + 1;
+        qc->error = NGX_QUIC_ERR_STREAM_STATE_ERROR;
+        return NULL;
+    }
+
+    peer = qc->client ? &qc->streams.server: &qc->streams.client;
+    sctl = ngx_quic_uni_stream(id) ? &peer->uni : &peer->bidi;
+
+    if ((id >> 2) < sctl->count) {
+        return NGX_QUIC_STREAM_GONE;
     }
 
+    if ((id >> 2) >= sctl->max) {
+        qc->error = NGX_QUIC_ERR_STREAM_LIMIT_ERROR;
+        return NULL;
+    }
+
+    min_id = sctl->count << 2;
+
+    if (qc->client) {
+        min_id++;
+    }
+
+    if (ngx_quic_uni_stream(id)) {
+        min_id |= NGX_QUIC_STREAM_UNIDIRECTIONAL;
+    }
+
+    sctl->count = (id >> 2) + 1;
+
     /*
      * RFC 9000, 2.1.  Stream Types and Identifiers
      *
@@ -504,9 +517,8 @@ ngx_quic_reject_stream(ngx_connection_t 
 
     qc = ngx_quic_get_connection(c);
 
-    code = (id & NGX_QUIC_STREAM_UNIDIRECTIONAL)
-           ? qc->conf->stream_reject_code_uni
-           : qc->conf->stream_reject_code_bidi;
+    code = ngx_quic_uni_stream(id) ? qc->conf->stream_reject_code_uni
+                                   : qc->conf->stream_reject_code_bidi;
 
     if (code == 0) {
         return NGX_DECLINED;
@@ -555,7 +567,7 @@ ngx_quic_init_stream_handler(ngx_event_t
 
     ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, "quic init stream");
 
-    if ((qs->id & NGX_QUIC_STREAM_UNIDIRECTIONAL) == 0) {
+    if (!ngx_quic_uni_stream(qs->id)) {
         c->write->active = 1;
         c->write->ready = 1;
     }
@@ -755,40 +767,17 @@ ngx_quic_create_stream(ngx_connection_t 
     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->recv_chain = ngx_quic_stream_recv_chain;
+
+    sc->read->log = c->log;
+    sc->write->log = c->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->peer_tp.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->peer_tp.initial_max_stream_data_bidi_remote;
-            qs->recv_max_data = qc->tp.initial_max_stream_data_bidi_local;
-
-        } else {
-            qs->send_max_data = qc->peer_tp.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;
-    }
+    ngx_quic_stream_init_state(qc, qs);
 
     qs->recv_window = qs->recv_max_data;
 
@@ -810,6 +799,101 @@ ngx_quic_create_stream(ngx_connection_t 
 }
 
 
+static void
+ngx_quic_stream_init_state(ngx_quic_connection_t *qc, ngx_quic_stream_t *qs)
+{
+    ngx_uint_t  out;
+
+    out = ngx_quic_out_stream(qc, qs->id);
+
+    if (ngx_quic_uni_stream(qs->id)) {
+        if (out) {
+            qs->send_max_data = qc->peer_tp.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 (out) {
+            qs->send_max_data = qc->peer_tp.initial_max_stream_data_bidi_remote;
+            qs->recv_max_data = qc->tp.initial_max_stream_data_bidi_local;
+
+        } else {
+            qs->send_max_data = qc->peer_tp.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;
+    }
+}
+
+
+void
+ngx_quic_streams_init_state(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) {
+        return;
+    }
+
+    node = ngx_rbtree_min(tree->root, tree->sentinel);
+
+    while (node) {
+        qs = (ngx_quic_stream_t *) node;
+        node = ngx_rbtree_next(tree, node);
+
+        ngx_quic_stream_init_state(qc, qs);
+    }
+}
+
+
+void
+ngx_quic_streams_notify_write(ngx_connection_t *c)
+{
+    ngx_rbtree_t           *tree;
+    ngx_connection_t       *sc;
+    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) {
+        return;
+    }
+
+    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;
+        if (sc == NULL) {
+            continue;
+        }
+
+        ngx_post_event(sc->write, &ngx_posted_events);
+    }
+}
+
+
 void
 ngx_quic_cancelable_stream(ngx_connection_t *c)
 {
@@ -859,6 +943,8 @@ ngx_quic_stream_recv(ngx_connection_t *c
         || qs->recv_state == NGX_QUIC_STREAM_RECV_RESET_READ)
     {
         qs->recv_state = NGX_QUIC_STREAM_RECV_RESET_READ;
+        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic stream id:0x%xL bad recv state", qs->id);
         return NGX_ERROR;
     }
 
@@ -1002,6 +1088,129 @@ ngx_quic_stream_send_chain(ngx_connectio
 }
 
 
+static ssize_t
+ngx_quic_stream_recv_chain(ngx_connection_t *c, ngx_chain_t *in, off_t limit)
+{
+    size_t              len;
+    ngx_buf_t          *b;
+    ngx_chain_t        *cl, *out;
+    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;
+        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic stream id:0x%xL bad recv state", qs->id);
+        return NGX_ERROR;
+    }
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, pc->log, 0,
+                   "quic stream id:0x%xL recv chain", qs->id);
+
+    len = 0;
+    for (cl = in; cl; cl = cl->next) {
+        len += cl->buf->end - cl->buf->last;
+    }
+
+    if (limit && len > (size_t) limit) {
+        len = limit;
+    }
+
+    out = ngx_quic_read_buffer(pc, &qs->recv, len);
+    if (out == NGX_CHAIN_ERROR) {
+        return NGX_ERROR;
+    }
+
+    len = 0;
+
+    for (cl = out; cl; cl = cl->next) {
+        b = cl->buf;
+        len += b->last - b->pos;
+    }
+
+    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 chain() not ready", qs->id);
+        return NGX_AGAIN;
+    }
+
+    ngx_quic_copy_chain_data(in, out);
+
+    ngx_quic_free_chain(pc, out);
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic stream id:0x%xL recv chain len:%z", qs->id, len);
+
+    if (ngx_quic_update_flow(qs, qs->recv_offset + len) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    return len;
+}
+
+
+static void
+ngx_quic_copy_chain_data(ngx_chain_t *dst_chain, ngx_chain_t *src_chain)
+{
+    u_char       *rpos, *wpos;
+    size_t        data_size, buf_size, len;
+    ngx_chain_t  *src, *dst;
+
+    src = src_chain;
+    dst = dst_chain;
+
+    rpos = src->buf->pos;
+    wpos = dst->buf->last;
+
+    while (src && dst) {
+
+        data_size = src->buf->last - rpos;
+        buf_size = dst->buf->end - wpos;
+
+        len = ngx_min(data_size, buf_size);
+
+        ngx_memcpy(wpos, rpos, len);
+
+        rpos += len;
+        wpos += len;
+
+        if (rpos == src->buf->last) {
+            src = src->next;
+            if (src) {
+                rpos = src->buf->pos;
+            }
+        }
+
+        if (wpos == dst->buf->end) {
+            dst = dst->next;
+            if (dst) {
+                wpos = dst->buf->last;
+            }
+        }
+    }
+}
+
+
 static ngx_int_t
 ngx_quic_stream_flush(ngx_quic_stream_t *qs)
 {
@@ -1094,7 +1303,13 @@ ngx_quic_stream_cleanup_handler(void *da
 
     qs = c->quic;
 
-    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, qs->parent->log, 0,
+    /* stream log was allocated from pool, now deleted */
+    c->log = qs->parent->log;
+    c->read->log = c->log;
+    c->write->log = c->log;
+    c->pool->log = c->log;
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
                    "quic stream id:0x%xL cleanup", qs->id);
 
     if (ngx_quic_shutdown_stream(c, NGX_RDWR_SHUTDOWN) != NGX_OK) {
@@ -1121,9 +1336,11 @@ failed:
 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;
+    ngx_connection_t        *pc;
+    ngx_quic_frame_t        *frame;
+    ngx_quic_connection_t   *qc;
+    ngx_quic_stream_ctl_t   *sctl;
+    ngx_quic_stream_peer_t  *peer;
 
     pc = qs->parent;
     qc = ngx_quic_get_connection(pc);
@@ -1166,7 +1383,12 @@ ngx_quic_close_stream(ngx_quic_stream_t 
         return NGX_OK;
     }
 
-    if ((qs->id & NGX_QUIC_STREAM_SERVER_INITIATED) == 0) {
+    if (ngx_quic_input_stream(qc, qs->id)) {
+
+        peer = qc->client ? &qc->streams.server : &qc->streams.client;
+
+        sctl = ngx_quic_uni_stream(qs->id) ? &peer->uni : &peer->bidi;
+
         frame = ngx_quic_alloc_frame(pc);
         if (frame == NULL) {
             return NGX_ERROR;
@@ -1175,14 +1397,8 @@ ngx_quic_close_stream(ngx_quic_stream_t 
         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.uni.max;
-            frame->u.max_streams.bidi = 0;
-
-        } else {
-            frame->u.max_streams.limit = ++qc->streams.client.bidi.max;
-            frame->u.max_streams.bidi = 1;
-        }
+        frame->u.max_streams.limit = ++sctl->max;
+        frame->u.max_streams.bidi = !ngx_quic_uni_stream(qs->id);
 
         ngx_quic_queue_frame(qc, frame);
     }
@@ -1232,8 +1448,8 @@ ngx_quic_handle_stream_frame(ngx_connect
     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))
+    if (ngx_quic_uni_stream(f->stream_id)
+        && ngx_quic_out_stream(qc, f->stream_id))
     {
         qc->error = NGX_QUIC_ERR_STREAM_STATE_ERROR;
         return NGX_ERROR;
@@ -1377,9 +1593,7 @@ ngx_quic_handle_stream_data_blocked_fram
 
     qc = ngx_quic_get_connection(c);
 
-    if ((f->id & NGX_QUIC_STREAM_UNIDIRECTIONAL)
-        && (f->id & NGX_QUIC_STREAM_SERVER_INITIATED))
-    {
+    if (ngx_quic_uni_stream(f->id) && ngx_quic_out_stream(qc, f->id)) {
         qc->error = NGX_QUIC_ERR_STREAM_STATE_ERROR;
         return NGX_ERROR;
     }
@@ -1407,9 +1621,14 @@ ngx_quic_handle_max_stream_data_frame(ng
 
     qc = ngx_quic_get_connection(c);
 
-    if ((f->id & NGX_QUIC_STREAM_UNIDIRECTIONAL)
-        && (f->id & NGX_QUIC_STREAM_SERVER_INITIATED) == 0)
-    {
+    /*
+     * RFC 9000, 19.10. MAX_STREAM_DATA Frames
+     *
+     *  An endpoint that receives a MAX_STREAM_DATA frame for a receive-only
+     *  stream MUST terminate the connection with error STREAM_STATE_ERROR.
+     */
+
+    if (ngx_quic_uni_stream(f->id) && ngx_quic_input_stream(qc, f->id)) {
         qc->error = NGX_QUIC_ERR_STREAM_STATE_ERROR;
         return NGX_ERROR;
     }
@@ -1450,9 +1669,7 @@ ngx_quic_handle_reset_stream_frame(ngx_c
 
     qc = ngx_quic_get_connection(c);
 
-    if ((f->id & NGX_QUIC_STREAM_UNIDIRECTIONAL)
-        && (f->id & NGX_QUIC_STREAM_SERVER_INITIATED))
-    {
+    if (ngx_quic_uni_stream(f->id) && ngx_quic_out_stream(qc, f->id)) {
         qc->error = NGX_QUIC_ERR_STREAM_STATE_ERROR;
         return NGX_ERROR;
     }
@@ -1519,9 +1736,14 @@ ngx_quic_handle_stop_sending_frame(ngx_c
 
     qc = ngx_quic_get_connection(c);
 
-    if ((f->id & NGX_QUIC_STREAM_UNIDIRECTIONAL)
-        && (f->id & NGX_QUIC_STREAM_SERVER_INITIATED) == 0)
-    {
+    /*
+     * RFC 9000,  19.5. STOP_SENDING Frames
+     *
+     *  An endpoint that receives a STOP_SENDING frame for a receive-only
+     *  stream MUST terminate the connection with error STREAM_STATE_ERROR.
+     */
+
+    if (ngx_quic_uni_stream(f->id) && ngx_quic_input_stream(qc, f->id)) {
         qc->error = NGX_QUIC_ERR_STREAM_STATE_ERROR;
         return NGX_ERROR;
     }
@@ -1554,25 +1776,19 @@ 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_stream_ctl_t  *sctl;
     ngx_quic_connection_t  *qc;
 
     qc = ngx_quic_get_connection(c);
 
-    if (f->bidi) {
-        if (qc->streams.server.bidi.max < f->limit) {
-            qc->streams.server.bidi.max = f->limit;
-
-            ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                           "quic max_streams_bidi:%uL", f->limit);
-        }
-
-    } else {
-        if (qc->streams.server.uni.max < f->limit) {
-            qc->streams.server.uni.max = f->limit;
-
-            ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                           "quic max_streams_uni:%uL", f->limit);
-        }
+    sctl = f->bidi ? &qc->streams.server.bidi : &qc->streams.server.uni;
+
+    if (sctl->max < f->limit) {
+        sctl->max = f->limit;
+
+        ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic max_streams_%s:%uL",
+                       f->bidi? "bidi": "uni", f->limit);
     }
 
     return NGX_OK;
diff --git a/src/event/quic/ngx_event_quic_streams.h b/src/event/quic/ngx_event_quic_streams.h
--- a/src/event/quic/ngx_event_quic_streams.h
+++ b/src/event/quic/ngx_event_quic_streams.h
@@ -1,5 +1,6 @@
 
 /*
+ * Copyright (C) 2023 Web Server LLC
  * Copyright (C) Nginx, Inc.
  */
 
@@ -34,6 +35,8 @@ ngx_int_t ngx_quic_handle_max_streams_fr
     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_streams_init_state(ngx_connection_t *c);
+void ngx_quic_streams_notify_write(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,
diff --git a/src/event/quic/ngx_event_quic_tokens.c b/src/event/quic/ngx_event_quic_tokens.c
--- a/src/event/quic/ngx_event_quic_tokens.c
+++ b/src/event/quic/ngx_event_quic_tokens.c
@@ -1,5 +1,6 @@
 
 /*
+ * Copyright (C) 2023 Web Server LLC
  * Copyright (C) Nginx, Inc.
  */
 
@@ -307,3 +308,50 @@ bad_token:
 
     return NGX_DECLINED;
 }
+
+
+ngx_int_t
+ngx_quic_verify_retry_token_integrity(ngx_connection_t *c,
+    ngx_quic_header_t *pkt)
+{
+    u_char                 *p;
+    size_t                  size;
+    ngx_str_t               ad, itag, pkt_tag;
+    ngx_quic_connection_t  *qc;
+
+    qc = ngx_quic_get_connection(c);
+
+    /* integrity tag from retry packet */
+    pkt_tag.data = pkt->data + pkt->len - NGX_QUIC_TAG_LEN;
+
+    /* pseudo packet size */
+    size = pkt->len + 1 /* od len */ + 20 /* odcid */;
+
+    p = ngx_pcalloc(c->pool, size);
+    if (p == NULL) {
+        return NGX_ERROR;
+    }
+
+    ad.data = p;
+
+    *p++ = NGX_QUIC_SERVER_CID_LEN;
+    p = ngx_cpymem(p, qc->incid, NGX_QUIC_SERVER_CID_LEN);
+    p = ngx_cpymem(p, pkt->data, pkt->len - NGX_QUIC_TAG_LEN);
+
+    ad.len = p - ad.data;
+
+    /* integrity tag to calculate using pseudo packet input */
+    itag.data = ad.data + ad.len;
+    itag.len = NGX_QUIC_TAG_LEN;
+
+    if (ngx_quic_retry_seal(&ad, &itag, pkt->log) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    if (ngx_memcmp(pkt_tag.data, itag.data, NGX_QUIC_TAG_LEN) != 0) {
+        return NGX_ERROR;
+    }
+
+    return NGX_OK;
+}
+
diff --git a/src/event/quic/ngx_event_quic_tokens.h b/src/event/quic/ngx_event_quic_tokens.h
--- a/src/event/quic/ngx_event_quic_tokens.h
+++ b/src/event/quic/ngx_event_quic_tokens.h
@@ -1,5 +1,6 @@
 
 /*
+ * Copyright (C) 2023 Web Server LLC
  * Copyright (C) Nginx, Inc.
  */
 
@@ -15,6 +16,12 @@
 #define NGX_QUIC_MAX_TOKEN_SIZE              64
     /* SHA-1(addr)=20 + sizeof(time_t) + retry(1) + odcid.len(1) + odcid */
 
+/*
+ * max size is not specified, this is arbitrary limit
+ * to match values found in the wild
+ */
+#define NGX_QUIC_MAX_NEW_TOKEN  (NGX_QUIC_MAX_TOKEN_SIZE * 2)
+
 #define NGX_QUIC_AES_256_GCM_IV_LEN          12
 #define NGX_QUIC_AES_256_GCM_TAG_LEN         16
 
@@ -30,5 +37,7 @@ ngx_int_t ngx_quic_new_token(ngx_log_t *
     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);
+ngx_int_t ngx_quic_verify_retry_token_integrity(ngx_connection_t *c,
+    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
--- a/src/event/quic/ngx_event_quic_transport.c
+++ b/src/event/quic/ngx_event_quic_transport.c
@@ -1,5 +1,6 @@
 
 /*
+ * Copyright (C) 2023 Web Server LLC
  * Copyright (C) Nginx, Inc.
  */
 
@@ -441,7 +442,7 @@ ngx_quic_parse_long_header_v1(ngx_quic_h
 
     if (ngx_quic_pkt_in(pkt->flags)) {
 
-        if (pkt->len < NGX_QUIC_MIN_INITIAL_SIZE) {
+        if (!pkt->server && 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;
@@ -471,6 +472,29 @@ ngx_quic_parse_long_header_v1(ngx_quic_h
     } else if (ngx_quic_pkt_hs(pkt->flags)) {
         pkt->level = ssl_encryption_handshake;
 
+    } else if (ngx_quic_pkt_retry(pkt->flags)) {
+        pkt->level = ssl_encryption_initial;
+
+        pkt->token.len = end - p;
+
+        if (pkt->token.len < 17) {
+            ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
+                          "quic retry packet too small");
+            return NGX_ERROR;
+        }
+
+        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->raw->pos = p;
+        pkt->len = p - pkt->data;
+
+        return NGX_OK;
+
     } else {
         ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
                       "quic bad packet type");
@@ -619,15 +643,19 @@ static size_t
 ngx_quic_create_long_header(ngx_quic_header_t *pkt, u_char *out,
     u_char **pnp)
 {
-    size_t   rem_len;
+    size_t   rem_len, tlen;
     u_char  *p, *start;
 
+    tlen = (pkt->level != ssl_encryption_initial)
+                        ? 0
+                        : ngx_quic_varint_len(pkt->token.len) + pkt->token.len;
+
     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);
+               + tlen;
     }
 
     p = start = out;
@@ -643,7 +671,11 @@ ngx_quic_create_long_header(ngx_quic_hea
     p = ngx_cpymem(p, pkt->scid.data, pkt->scid.len);
 
     if (pkt->level == ssl_encryption_initial) {
-        ngx_quic_build_int(&p, 0);
+        ngx_quic_build_int(&p, pkt->token.len);
+
+        if (pkt->token.len) {
+            p = ngx_cpymem(p, pkt->token.data, pkt->token.len);
+        }
     }
 
     ngx_quic_build_int(&p, rem_len);
@@ -1128,6 +1160,26 @@ ngx_quic_parse_frame(ngx_quic_header_t *
 
         break;
 
+    case NGX_QUIC_FT_HANDSHAKE_DONE:
+        break;
+
+    case NGX_QUIC_FT_NEW_TOKEN:
+
+        p = ngx_quic_parse_int(p, end, &varint);
+        if (p == NULL) {
+            goto error;
+        }
+
+        f->u.token.length = varint;
+
+        p = ngx_quic_read_bytes(p, end, f->u.token.length,
+                                &f->u.token.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);
@@ -1157,43 +1209,48 @@ ngx_quic_frame_allowed(ngx_quic_header_t
 {
     uint8_t  ptype;
 
+#define PKT_SRV  0x10
+#define PKT_CLN  0x20
+#define PKT_ANY  0x30
+
     /*
      * RFC 9000, 12.4. Frames and Frame Types: Table 3
      *
-     * Frame permissions per packet: 4 bits: IH01
+     * Frame permissions per packet: 6 bits: CSIH01
+     * Two high bits: 'allowed on client' and 'allowed on server'
      */
     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 */
+         /* PADDING */               0xF | PKT_ANY,
+         /* PING */                  0xF | PKT_ANY,
+         /* ACK */                   0xD | PKT_ANY,
+         /* ACK_ECN */               0xD | PKT_ANY,
+         /* RESET_STREAM */          0x3 | PKT_ANY,
+         /* STOP_SENDING */          0x3 | PKT_ANY,
+         /* CRYPTO */                0xD | PKT_ANY,
+         /* NEW_TOKEN */             0x0 | PKT_SRV, /* only sent by server */
+         /* STREAM */                0x3 | PKT_ANY,
+         /* STREAM1 */               0x3 | PKT_ANY,
+         /* STREAM2 */               0x3 | PKT_ANY,
+         /* STREAM3 */               0x3 | PKT_ANY,
+         /* STREAM4 */               0x3 | PKT_ANY,
+         /* STREAM5 */               0x3 | PKT_ANY,
+         /* STREAM6 */               0x3 | PKT_ANY,
+         /* STREAM7 */               0x3 | PKT_ANY,
+         /* MAX_DATA */              0x3 | PKT_ANY,
+         /* MAX_STREAM_DATA */       0x3 | PKT_ANY,
+         /* MAX_STREAMS */           0x3 | PKT_ANY,
+         /* MAX_STREAMS2 */          0x3 | PKT_ANY,
+         /* DATA_BLOCKED */          0x3 | PKT_ANY,
+         /* STREAM_DATA_BLOCKED */   0x3 | PKT_ANY,
+         /* STREAMS_BLOCKED */       0x3 | PKT_ANY,
+         /* STREAMS_BLOCKED2 */      0x3 | PKT_ANY,
+         /* NEW_CONNECTION_ID */     0x3 | PKT_ANY,
+         /* RETIRE_CONNECTION_ID */  0x3 | PKT_ANY,
+         /* PATH_CHALLENGE */        0x3 | PKT_ANY,
+         /* PATH_RESPONSE */         0x1 | PKT_ANY,
+         /* CONNECTION_CLOSE */      0xF | PKT_ANY,
+         /* CONNECTION_CLOSE2 */     0x3 | PKT_ANY,
+         /* HANDSHAKE_DONE */        0x0 | PKT_SRV, /* only sent by server */
     };
 
     if (ngx_quic_long_pkt(pkt->flags)) {
@@ -1212,6 +1269,8 @@ ngx_quic_frame_allowed(ngx_quic_header_t
         ptype = 1; /* application data */
     }
 
+    ptype |= (pkt->server ? PKT_SRV : PKT_CLN);
+
     if (ptype & ngx_quic_frame_masks[frame_type]) {
         return NGX_OK;
     }
@@ -1648,7 +1707,10 @@ ngx_quic_parse_transport_param(u_char *p
         }
         break;
 
+    case NGX_QUIC_TP_ORIGINAL_DCID:
     case NGX_QUIC_TP_INITIAL_SCID:
+    case NGX_QUIC_TP_SR_TOKEN:
+    case NGX_QUIC_TP_RETRY_SCID:
 
         str.len = end - p;
         str.data = p;
@@ -1708,6 +1770,28 @@ ngx_quic_parse_transport_param(u_char *p
         dst->initial_scid = str;
         break;
 
+    case NGX_QUIC_TP_SR_TOKEN:
+
+        if (str.len != NGX_QUIC_SR_TOKEN_LEN) {
+            return NGX_ERROR;
+        }
+        ngx_memcpy(dst->sr_token, str.data, NGX_QUIC_SR_TOKEN_LEN);
+        break;
+
+    case NGX_QUIC_TP_ORIGINAL_DCID:
+        if (str.len > NGX_QUIC_CID_LEN_MAX) {
+            return NGX_ERROR;
+        }
+        dst->original_dcid = str;
+        break;
+
+    case NGX_QUIC_TP_RETRY_SCID:
+        if (str.len > NGX_QUIC_CID_LEN_MAX) {
+            return NGX_ERROR;
+        }
+        dst->retry_scid = str;
+        break;
+
     default:
         return NGX_ERROR;
     }
@@ -1718,7 +1802,7 @@ ngx_quic_parse_transport_param(u_char *p
 
 ngx_int_t
 ngx_quic_parse_transport_params(u_char *p, u_char *end, ngx_quic_tp_t *tp,
-    ngx_log_t *log)
+    ngx_log_t *log, ngx_uint_t client)
 {
     uint64_t   id, len;
     ngx_int_t  rc;
@@ -1736,10 +1820,12 @@ ngx_quic_parse_transport_params(u_char *
         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;
+            if (!client) {
+                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);
@@ -1828,6 +1914,24 @@ ngx_quic_parse_transport_params(u_char *
                    "quic tp initial source_connection_id len:%uz %xV",
                    tp->initial_scid.len, &tp->initial_scid);
 
+    if (client) {
+        ngx_log_debug2(NGX_LOG_DEBUG_EVENT, log, 0,
+                       "quic tp stateless reset token %*xs",
+                       NGX_QUIC_SR_TOKEN_LEN, tp->sr_token);
+
+        if (tp->original_dcid.len) {
+            ngx_log_debug2(NGX_LOG_DEBUG_EVENT, log, 0,
+                           "quic tp original_dcid len:%uz %xV",
+                           tp->original_dcid.len, &tp->original_dcid);
+        }
+
+        if (tp->retry_scid.len) {
+            ngx_log_debug2(NGX_LOG_DEBUG_EVENT, log, 0,
+                           "quic tp retry_scid len:%uz %xV",
+                           tp->retry_scid.len, &tp->retry_scid);
+        }
+    }
+
     return NGX_OK;
 }
 
@@ -2015,7 +2119,7 @@ ngx_quic_init_transport_params(ngx_quic_
 
 ssize_t
 ngx_quic_create_transport_params(u_char *pos, u_char *end, ngx_quic_tp_t *tp,
-    size_t *clen)
+    size_t *clen, ngx_uint_t client)
 {
     u_char  *p;
     size_t   len;
@@ -2086,16 +2190,21 @@ ngx_quic_create_transport_params(u_char 
     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);
+    if (!client) {
+        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 (!client) {
+        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;
@@ -2141,16 +2250,21 @@ ngx_quic_create_transport_params(u_char 
     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);
+    if (!client) {
+        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);
+    if (!client) {
+        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;
 }
diff --git a/src/event/quic/ngx_event_quic_transport.h b/src/event/quic/ngx_event_quic_transport.h
--- a/src/event/quic/ngx_event_quic_transport.h
+++ b/src/event/quic/ngx_event_quic_transport.h
@@ -1,5 +1,6 @@
 
 /*
+ * Copyright (C) 2023 Web Server LLC
  * Copyright (C) Nginx, Inc.
  */
 
@@ -167,6 +168,7 @@ typedef struct {
 
 typedef struct {
     uint64_t                                    length;
+    u_char                                     *data;
 } ngx_quic_new_token_frame_t;
 
 /*
@@ -337,6 +339,7 @@ typedef struct {
     unsigned                                    first:1;
     unsigned                                    rebound:1;
     unsigned                                    path_challenged:1;
+    unsigned                                    server:1; /* is from server */
 } ngx_quic_header_t;
 
 
@@ -388,9 +391,9 @@ size_t ngx_quic_create_ack_range(u_char 
 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);
+    ngx_quic_tp_t *tp, ngx_log_t *log, ngx_uint_t client);
 ssize_t ngx_quic_create_transport_params(u_char *p, u_char *end,
-    ngx_quic_tp_t *tp, size_t *clen);
+    ngx_quic_tp_t *tp, size_t *clen, ngx_uint_t client);
 
 void ngx_quic_dcid_encode_key(u_char *dcid, uint64_t key);
 
_______________________________________________
nginx-devel mailing list
nginx-devel@nginx.org
https://mailman.nginx.org/mailman/listinfo/nginx-devel

Reply via email to