]> git.kaiwu.me - nginx.git/commitdiff
Style and handlers.
authorVladimir Homutov <vl@nginx.com>
Wed, 18 Mar 2020 10:02:19 +0000 (13:02 +0300)
committerVladimir Homutov <vl@nginx.com>
Wed, 18 Mar 2020 10:02:19 +0000 (13:02 +0300)
Cleanup in ngx_event_quic.c:
    + reorderded functions, structures
    + added missing prototypes
    + added separate handlers for each frame type
    + numerous indentation/comments/TODO fixes
    + removed non-implemented qc->state and corresponding enum;
        this requires deep thinking, stub was unused.
    + streams inside quic connection are now in own structure

src/event/ngx_event_quic.c

index 000b8be8becc6d21aeb8f1be38362e24fc4e6c80..e6ee6d2a6a48ac51de4930de0a208ce71448f880 100644 (file)
@@ -9,67 +9,38 @@
 #include <ngx_event.h>
 
 
-/* TODO: real states, these are stubs */
-typedef enum  {
-    NGX_QUIC_ST_INITIAL,
-    NGX_QUIC_ST_HANDSHAKE,
-    NGX_QUIC_ST_APP_DATA
-} ngx_quic_state_t;
-
-
-struct ngx_quic_connection_s {
-
-    ngx_quic_state_t   state;
-    ngx_ssl_t         *ssl;
-
-    ngx_quic_frame_t  *frames;
-
-    ngx_str_t          scid;
-    ngx_str_t          dcid;
-    ngx_str_t          token;
-
-    /* current packet numbers for each namespace */
-    ngx_uint_t         initial_pn;
-    ngx_uint_t         handshake_pn;
-    ngx_uint_t         appdata_pn;
-
-    ngx_quic_secrets_t secrets;
-
-    /* streams */
-    ngx_rbtree_t               stree;
-    ngx_rbtree_node_t          stree_sentinel;
-    ngx_msec_t                 stream_timeout;
-    ngx_connection_handler_pt  stream_handler;
-};
+typedef struct {
+    ngx_rbtree_node_t                  node;
+    ngx_buf_t                         *b;
+    ngx_connection_t                  *c;
+    ngx_quic_stream_t                  s;
+} ngx_quic_stream_node_t;
 
 
 typedef struct {
-    ngx_rbtree_node_t      node;
-    ngx_buf_t             *b;
-    ngx_connection_t      *c;
-    ngx_quic_stream_t      s;
-} ngx_quic_stream_node_t;
+    ngx_rbtree_t                      tree;
+    ngx_rbtree_node_t                 sentinel;
+    ngx_msec_t                        timeout;
+    ngx_connection_handler_pt         handler;
+} ngx_quic_streams_t;
 
 
-static ngx_int_t ngx_quic_input(ngx_connection_t *c, ngx_buf_t *b);
-static ngx_int_t ngx_quic_output(ngx_connection_t *c);
+struct ngx_quic_connection_s {
+    ngx_str_t                         scid;
+    ngx_str_t                         dcid;
+    ngx_str_t                         token;
 
-static ngx_int_t ngx_quic_new_connection(ngx_connection_t *c, ngx_ssl_t *ssl,
-    ngx_quic_header_t *pkt);
-static void ngx_quic_close_connection(ngx_connection_t *c);
+    /* current packet numbers  for each namespace */
+    ngx_uint_t                        initial_pn;
+    ngx_uint_t                        handshake_pn;
+    ngx_uint_t                        appdata_pn;
 
-static ngx_quic_stream_node_t *ngx_quic_stream_lookup(ngx_rbtree_t *rbtree,
-    ngx_uint_t key);
-static void ngx_quic_rbtree_insert_stream(ngx_rbtree_node_t *temp,
-    ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel);
+    ngx_quic_secrets_t                secrets;
+    ngx_ssl_t                        *ssl;
+    ngx_quic_frame_t                 *frames;
 
-static void ngx_quic_handshake_handler(ngx_event_t *rev);
-static ngx_int_t ngx_quic_handshake_input(ngx_connection_t *c,
-    ngx_quic_header_t *pkt);
-static ngx_int_t ngx_quic_initial_input(ngx_connection_t *c,
-    ngx_quic_header_t *pkt);
-static ngx_int_t ngx_quic_app_input(ngx_connection_t *c,
-    ngx_quic_header_t *pkt);
+    ngx_quic_streams_t                streams;
+};
 
 
 #if BORINGSSL_API_VERSION >= 10
@@ -84,6 +55,7 @@ static int ngx_quic_set_encryption_secrets(ngx_ssl_conn_t *ssl_conn,
     enum ssl_encryption_level_t level, const uint8_t *read_secret,
     const uint8_t *write_secret, size_t secret_len);
 #endif
+
 static int ngx_quic_add_handshake_data(ngx_ssl_conn_t *ssl_conn,
     enum ssl_encryption_level_t level, const uint8_t *data, size_t len);
 static int ngx_quic_flush_flight(ngx_ssl_conn_t *ssl_conn);
@@ -91,6 +63,44 @@ static int ngx_quic_send_alert(ngx_ssl_conn_t *ssl_conn,
     enum ssl_encryption_level_t level, uint8_t alert);
 
 
+static ngx_int_t ngx_quic_new_connection(ngx_connection_t *c, ngx_ssl_t *ssl,
+    ngx_quic_header_t *pkt);
+static ngx_int_t ngx_quic_init_connection(ngx_connection_t *c);
+static void ngx_quic_handshake_handler(ngx_event_t *rev);
+static void ngx_quic_close_connection(ngx_connection_t *c);
+
+static ngx_int_t ngx_quic_input(ngx_connection_t *c, ngx_buf_t *b);
+static ngx_int_t ngx_quic_initial_input(ngx_connection_t *c,
+    ngx_quic_header_t *pkt);
+static ngx_int_t ngx_quic_handshake_input(ngx_connection_t *c,
+    ngx_quic_header_t *pkt);
+static ngx_int_t ngx_quic_app_input(ngx_connection_t *c,
+    ngx_quic_header_t *pkt);
+static ngx_int_t ngx_quic_payload_handler(ngx_connection_t *c,
+    ngx_quic_header_t *pkt);
+
+static ngx_int_t ngx_quic_handle_ack_frame(ngx_connection_t *c,
+    ngx_quic_header_t *pkt, ngx_quic_ack_frame_t *f);
+static ngx_int_t ngx_quic_handle_crypto_frame(ngx_connection_t *c,
+    ngx_quic_header_t *pkt, ngx_quic_crypto_frame_t *frame);
+static ngx_int_t ngx_quic_handle_stream_frame(ngx_connection_t *c,
+    ngx_quic_header_t *pkt, ngx_quic_stream_frame_t *frame);
+
+static void ngx_quic_queue_frame(ngx_quic_connection_t *qc,
+    ngx_quic_frame_t *frame);
+
+static ngx_int_t ngx_quic_output(ngx_connection_t *c);
+ngx_int_t ngx_quic_frames_send(ngx_connection_t *c, ngx_quic_frame_t *start,
+    ngx_quic_frame_t *end, size_t total);
+static ngx_int_t ngx_quic_send_packet(ngx_connection_t *c,
+    ngx_quic_connection_t *qc, enum ssl_encryption_level_t level,
+    ngx_str_t *payload);
+
+
+static void ngx_quic_rbtree_insert_stream(ngx_rbtree_node_t *temp,
+    ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel);
+static ngx_quic_stream_node_t *ngx_quic_find_stream(ngx_rbtree_t *rbtree,
+    ngx_uint_t key);
 static ssize_t ngx_quic_stream_recv(ngx_connection_t *c, u_char *buf,
     size_t size);
 static ssize_t ngx_quic_stream_send(ngx_connection_t *c, u_char *buf,
@@ -98,6 +108,7 @@ static ssize_t ngx_quic_stream_send(ngx_connection_t *c, u_char *buf,
 static ngx_chain_t *ngx_quic_stream_send_chain(ngx_connection_t *c,
     ngx_chain_t *in, off_t limit);
 
+
 static SSL_QUIC_METHOD quic_method = {
 #if BORINGSSL_API_VERSION >= 10
     ngx_quic_set_read_secret,
@@ -118,6 +129,142 @@ ngx_quic_init_ssl_methods(SSL_CTX* ctx)
 }
 
 
+#if BORINGSSL_API_VERSION >= 10
+
+static int
+ngx_quic_set_read_secret(ngx_ssl_conn_t *ssl_conn,
+    enum ssl_encryption_level_t level, const SSL_CIPHER *cipher,
+    const uint8_t *rsecret, size_t secret_len)
+{
+    ngx_connection_t  *c;
+
+    c = ngx_ssl_get_connection((ngx_ssl_conn_t *) ssl_conn);
+
+    ngx_quic_hexdump(c->log, "level:%d read secret",
+                     rsecret, secret_len, level);
+
+    return ngx_quic_set_encryption_secret(c->pool, ssl_conn, level,
+                                          rsecret, secret_len,
+                                          &c->quic->secrets.client);
+}
+
+
+static int
+ngx_quic_set_write_secret(ngx_ssl_conn_t *ssl_conn,
+    enum ssl_encryption_level_t level, const SSL_CIPHER *cipher,
+    const uint8_t *wsecret, size_t secret_len)
+{
+    ngx_connection_t  *c;
+
+    c = ngx_ssl_get_connection((ngx_ssl_conn_t *) ssl_conn);
+
+    ngx_quic_hexdump(c->log, "level:%d write secret",
+                     wsecret, secret_len, level);
+
+    return ngx_quic_set_encryption_secret(c->pool, ssl_conn, level,
+                                          wsecret, secret_len,
+                                          &c->quic->secrets.server);
+}
+
+#else
+
+static int
+ngx_quic_set_encryption_secrets(ngx_ssl_conn_t *ssl_conn,
+    enum ssl_encryption_level_t level, const uint8_t *rsecret,
+    const uint8_t *wsecret, size_t secret_len)
+{
+    ngx_int_t          rc;
+    ngx_connection_t  *c;
+
+    c = ngx_ssl_get_connection((ngx_ssl_conn_t *) ssl_conn);
+
+    ngx_quic_hexdump(c->log, "level:%d read", rsecret, secret_len, level);
+    ngx_quic_hexdump(c->log, "level:%d write", wsecret, secret_len, level);
+
+    rc = ngx_quic_set_encryption_secret(c->pool, ssl_conn, level,
+                                        rsecret, secret_len,
+                                        &c->quic->secrets.client);
+    if (rc != 1) {
+        return rc;
+    }
+
+    return ngx_quic_set_encryption_secret(c->pool, ssl_conn, level,
+                                          wsecret, secret_len,
+                                          &c->quic->secrets.server);
+}
+
+#endif
+
+
+static int
+ngx_quic_add_handshake_data(ngx_ssl_conn_t *ssl_conn,
+    enum ssl_encryption_level_t level, const uint8_t *data, size_t len)
+{
+    u_char                 *p;
+    ngx_quic_frame_t       *frame;
+    ngx_connection_t       *c;
+    ngx_quic_connection_t  *qc;
+
+    c = ngx_ssl_get_connection((ngx_ssl_conn_t *) ssl_conn);
+    qc = c->quic;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "ngx_quic_add_handshake_data");
+
+    frame = ngx_pcalloc(c->pool, sizeof(ngx_quic_frame_t));
+    if (frame == NULL) {
+        return 0;
+    }
+
+    p = ngx_pnalloc(c->pool, len);
+    if (p == NULL) {
+        return 0;
+    }
+
+    ngx_memcpy(p, data, len);
+
+    frame->level = level;
+    frame->type = NGX_QUIC_FT_CRYPTO;
+    frame->u.crypto.len = len;
+    frame->u.crypto.data = p;
+
+    ngx_sprintf(frame->info, "crypto, generated by SSL len=%ui level=%d", len, level);
+
+    ngx_quic_queue_frame(qc, frame);
+
+    return 1;
+}
+
+
+static int
+ngx_quic_flush_flight(ngx_ssl_conn_t *ssl_conn)
+{
+    ngx_connection_t  *c;
+
+    c = ngx_ssl_get_connection((ngx_ssl_conn_t *) ssl_conn);
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, "ngx_quic_flush_flight()");
+
+    return 1;
+}
+
+
+static int
+ngx_quic_send_alert(ngx_ssl_conn_t *ssl_conn, enum ssl_encryption_level_t level,
+    uint8_t alert)
+{
+    ngx_connection_t  *c;
+
+    c = ngx_ssl_get_connection((ngx_ssl_conn_t *) ssl_conn);
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "ngx_quic_send_alert(), lvl=%d, alert=%d",
+                   (int) level, (int) alert);
+
+    return 1;
+}
+
+
 void
 ngx_quic_run(ngx_connection_t *c, ngx_ssl_t *ssl, ngx_msec_t timeout,
     ngx_connection_handler_pt handler)
@@ -144,8 +291,8 @@ ngx_quic_run(ngx_connection_t *c, ngx_ssl_t *ssl, ngx_msec_t timeout,
     }
 
     // we don't need stream handler for initial packet processing
-    c->quic->stream_handler = handler;
-    c->quic->stream_timeout = timeout;
+    c->quic->streams.handler = handler;
+    c->quic->streams.timeout = timeout;
 
     ngx_add_timer(c->read, timeout);
 
@@ -155,52 +302,183 @@ ngx_quic_run(ngx_connection_t *c, ngx_ssl_t *ssl, ngx_msec_t timeout,
 }
 
 
-static void
-ngx_quic_handshake_handler(ngx_event_t *rev)
+static ngx_int_t
+ngx_quic_new_connection(ngx_connection_t *c, ngx_ssl_t *ssl,
+    ngx_quic_header_t *pkt)
 {
-    ssize_t                 n;
-    ngx_connection_t       *c;
-    u_char                  buf[512];
-    ngx_buf_t               b;
+    ngx_quic_connection_t  *qc;
 
-    b.start = buf;
-    b.end = buf + 512;
-    b.pos = b.last = b.start;
+    if (ngx_buf_size(pkt->raw) < 1200) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0, "too small UDP datagram");
+        return NGX_ERROR;
+    }
 
-    c = rev->data;
+    if (ngx_quic_parse_long_header(pkt) != NGX_OK) {
+        return NGX_ERROR;
+    }
 
-    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, rev->log, 0, "quic handshake handler");
+    if ((pkt->flags & 0xf0) != NGX_QUIC_PKT_INITIAL) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "invalid initial packet: 0x%xi", pkt->flags);
+        return NGX_ERROR;
+    }
 
-    if (rev->timedout) {
-        ngx_log_error(NGX_LOG_INFO, c->log, NGX_ETIMEDOUT, "client timed out");
-        ngx_quic_close_connection(c);
-        return;
+    if (ngx_quic_parse_initial_header(pkt) != NGX_OK) {
+        return NGX_ERROR;
     }
 
-    if (c->close) {
-        ngx_quic_close_connection(c);
-        return;
+    qc = ngx_pcalloc(c->pool, sizeof(ngx_quic_connection_t));
+    if (qc == NULL) {
+        return NGX_ERROR;
     }
 
-    n = c->recv(c, b.start, b.end - b.start);
+    ngx_rbtree_init(&qc->streams.tree, &qc->streams.sentinel,
+                    ngx_quic_rbtree_insert_stream);
 
-    if (n == NGX_AGAIN) {
-        return;
-    }
+    c->quic = qc;
+    qc->ssl = ssl;
 
-    if (n == NGX_ERROR) {
-        c->read->eof = 1;
-        ngx_quic_close_connection(c);
-        return;
+    qc->dcid.len = pkt->dcid.len;
+    qc->dcid.data = ngx_pnalloc(c->pool, pkt->dcid.len);
+    if (qc->dcid.data == NULL) {
+        return NGX_ERROR;
     }
+    ngx_memcpy(qc->dcid.data, pkt->dcid.data, qc->dcid.len);
 
-    b.last += n;
+    qc->scid.len = pkt->scid.len;
+    qc->scid.data = ngx_pnalloc(c->pool, qc->scid.len);
+    if (qc->scid.data == NULL) {
+        return NGX_ERROR;
+    }
+    ngx_memcpy(qc->scid.data, pkt->scid.data, qc->scid.len);
 
-    if (ngx_quic_input(c, &b) != NGX_OK) {
-        ngx_quic_close_connection(c);
-        return;
+    qc->token.len = pkt->token.len;
+    qc->token.data = ngx_pnalloc(c->pool, qc->token.len);
+    if (qc->token.data == NULL) {
+        return NGX_ERROR;
     }
-}
+    ngx_memcpy(qc->token.data, pkt->token.data, qc->token.len);
+
+
+    if (ngx_quic_set_initial_secret(c->pool, &qc->secrets, &qc->dcid)
+        != NGX_OK)
+    {
+        return NGX_ERROR;
+    }
+
+    pkt->secret = &qc->secrets.client.in;
+    pkt->level = ssl_encryption_initial;
+
+    if (ngx_quic_decrypt(c->pool, NULL, pkt) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    if (ngx_quic_init_connection(c) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    return ngx_quic_payload_handler(c, pkt);
+}
+
+
+static ngx_int_t
+ngx_quic_init_connection(ngx_connection_t *c)
+{
+    int                     n, sslerr;
+    ngx_ssl_conn_t         *ssl_conn;
+    ngx_quic_connection_t  *qc;
+
+    static const uint8_t params[] =
+        "\x00\x29"                         /* parameters length: 41 bytes         */
+        "\x00\x0e\x00\x01\x05"             /* active connection id limit: 5       */
+        "\x00\x04\x00\x04\x80\x98\x96\x80" /* initial max data = 10000000         */
+        "\x00\x09\x00\x01\x03"             /* initial max streams uni: 3          */
+        "\x00\x08\x00\x01\x10"             /* initial max streams bidi: 16        */
+        "\x00\x05\x00\x02\x40\xff"         /* initial max stream bidi local: 255  */
+        "\x00\x06\x00\x02\x40\xff"         /* initial max stream bidi remote: 255 */
+        "\x00\x07\x00\x02\x40\xff";        /* initial max stream data uni: 255    */
+
+    qc = c->quic;
+
+    if (ngx_ssl_create_connection(qc->ssl, c, NGX_SSL_BUFFER) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    ssl_conn = c->ssl->connection;
+
+    if (SSL_set_quic_transport_params(ssl_conn, params, sizeof(params) - 1) == 0) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "SSL_set_quic_transport_params() failed");
+        return NGX_ERROR;
+    }
+
+    n = SSL_do_handshake(ssl_conn);
+
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, "SSL_do_handshake: %d", n);
+
+    if (n == -1) {
+        sslerr = SSL_get_error(ssl_conn, n);
+
+        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, "SSL_get_error: %d",
+                       sslerr);
+    }
+
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "SSL_quic_read_level: %d, SSL_quic_write_level: %d",
+                   (int) SSL_quic_read_level(ssl_conn),
+                   (int) SSL_quic_write_level(ssl_conn));
+
+    return NGX_OK;
+}
+
+
+static void
+ngx_quic_handshake_handler(ngx_event_t *rev)
+{
+    ssize_t            n;
+    ngx_buf_t          b;
+    ngx_connection_t  *c;
+
+    u_char             buf[512];
+
+    b.start = buf;
+    b.end = buf + 512;
+    b.pos = b.last = b.start;
+
+    c = rev->data;
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, rev->log, 0, "quic handshake handler");
+
+    if (rev->timedout) {
+        ngx_log_error(NGX_LOG_INFO, c->log, NGX_ETIMEDOUT, "client timed out");
+        ngx_quic_close_connection(c);
+        return;
+    }
+
+    if (c->close) {
+        ngx_quic_close_connection(c);
+        return;
+    }
+
+    n = c->recv(c, b.start, b.end - b.start);
+
+    if (n == NGX_AGAIN) {
+        return;
+    }
+
+    if (n == NGX_ERROR) {
+        c->read->eof = 1;
+        ngx_quic_close_connection(c);
+        return;
+    }
+
+    b.last += n;
+
+    if (ngx_quic_input(c, &b) != NGX_OK) {
+        ngx_quic_close_connection(c);
+        return;
+    }
+}
 
 
 static void
@@ -231,14 +509,6 @@ ngx_quic_close_connection(ngx_connection_t *c)
 }
 
 
-ngx_connection_t *
-ngx_quic_create_uni_stream(ngx_connection_t *c)
-{
-    /* XXX */
-    return NULL;
-}
-
-
 static ngx_int_t
 ngx_quic_input(ngx_connection_t *c, ngx_buf_t *b)
 {
@@ -299,317 +569,305 @@ ngx_quic_input(ngx_connection_t *c, ngx_buf_t *b)
     return NGX_OK;
 }
 
+
 static ngx_int_t
-ngx_quic_send_packet(ngx_connection_t *c, ngx_quic_connection_t *qc,
-    enum ssl_encryption_level_t level, ngx_str_t *payload)
+ngx_quic_initial_input(ngx_connection_t *c, ngx_quic_header_t *pkt)
 {
-    ngx_str_t          res;
-    ngx_quic_header_t  pkt;
-
-    pkt.log = c->log;
-
-    static ngx_str_t  initial_token = ngx_null_string;
-
-    ngx_memzero(&pkt, sizeof(ngx_quic_header_t));
-    ngx_quic_hexdump0(c->log, "payload", payload->data, payload->len);
-
-    pkt.level = level;
-    pkt.dcid = qc->dcid;
-    pkt.scid = qc->scid;
-
-    if (level == ssl_encryption_initial) {
-        pkt.number = &qc->initial_pn;
-        pkt.flags = NGX_QUIC_PKT_INITIAL;
-        pkt.secret = &qc->secrets.server.in;
-        pkt.token = initial_token;
+    ngx_ssl_conn_t         *ssl_conn;
+    ngx_quic_connection_t  *qc;
 
-    } else if (level == ssl_encryption_handshake) {
-        pkt.number = &qc->handshake_pn;
-        pkt.flags = NGX_QUIC_PKT_HANDSHAKE;
-        pkt.secret = &qc->secrets.server.hs;
+    qc = c->quic;
+    ssl_conn = c->ssl->connection;
 
-    } else {
-        pkt.number = &qc->appdata_pn;
-        pkt.secret = &qc->secrets.server.ad;
+    if (ngx_quic_parse_long_header(pkt) != NGX_OK) {
+        return NGX_ERROR;
     }
 
-    if (ngx_quic_encrypt(c->pool, c->ssl->connection, &pkt, payload, &res)
-        != NGX_OK)
-    {
+    if (ngx_quic_parse_initial_header(pkt) != NGX_OK) {
         return NGX_ERROR;
     }
 
-    ngx_quic_hexdump0(c->log, "packet to send", res.data, res.len);
-
-    c->send(c, res.data, res.len); // TODO: err handling
+    pkt->secret = &qc->secrets.client.in;
+    pkt->level = ssl_encryption_initial;
 
-    (*pkt.number)++;
+    if (ngx_quic_decrypt(c->pool, ssl_conn, pkt) != NGX_OK) {
+        return NGX_ERROR;
+    }
 
-    return NGX_OK;
+    return ngx_quic_payload_handler(c, pkt);
 }
 
 
-/* pack a group of frames [start; end) into memory p and send as single packet */
-ngx_int_t
-ngx_quic_frames_send(ngx_connection_t *c, ngx_quic_frame_t *start,
-    ngx_quic_frame_t *end, size_t total)
+static ngx_int_t
+ngx_quic_handshake_input(ngx_connection_t *c, ngx_quic_header_t *pkt)
 {
-    ssize_t            len;
-    u_char            *p;
-    ngx_str_t          out;
-    ngx_quic_frame_t  *f;
+    ngx_ssl_conn_t         *ssl_conn;
+    ngx_quic_connection_t  *qc;
 
-    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                   "sending frames %p...%p", start, end);
+    qc = c->quic;
+    ssl_conn = c->ssl->connection;
 
-    p = ngx_pnalloc(c->pool, total);
-    if (p == NULL) {
+    /* extract cleartext data into pkt */
+    if (ngx_quic_parse_long_header(pkt) != NGX_OK) {
         return NGX_ERROR;
     }
 
-    out.data = p;
+    if (pkt->dcid.len != qc->dcid.len) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0, "unexpected quic dcidl");
+        return NGX_ERROR;
+    }
 
-    for (f = start; f != end; f = f->next) {
+    if (ngx_memcmp(pkt->dcid.data, qc->dcid.data, qc->dcid.len) != 0) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0, "unexpected quic dcid");
+        return NGX_ERROR;
+    }
 
-        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, "frame: %s", f->info);
+    if (pkt->scid.len != qc->scid.len) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0, "unexpected quic scidl");
+        return NGX_ERROR;
+    }
 
-        len = ngx_quic_create_frame(p, p + total, f);
-        if (len == -1) {
-            return NGX_ERROR;
-        }
+    if (ngx_memcmp(pkt->scid.data, qc->scid.data, qc->scid.len) != 0) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0, "unexpected quic scid");
+        return NGX_ERROR;
+    }
 
-        p += len;
+    if ((pkt->flags & 0xf0) != NGX_QUIC_PKT_HANDSHAKE) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "invalid packet type: 0x%xi", pkt->flags);
+        return NGX_ERROR;
     }
 
-    out.len = p - out.data;
+    if (ngx_quic_parse_handshake_header(pkt) != NGX_OK) {
+        return NGX_ERROR;
+    }
 
-    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                   "packet ready: %ui bytes at level %d",
-                   out.len, start->level);
+    pkt->secret = &qc->secrets.client.hs;
+    pkt->level = ssl_encryption_handshake;
 
-    // IOVEC/sendmsg_chain ?
-    if (ngx_quic_send_packet(c, c->quic, start->level, &out) != NGX_OK) {
+    if (ngx_quic_decrypt(c->pool, c->ssl->connection, pkt) != NGX_OK) {
         return NGX_ERROR;
     }
 
-    return NGX_OK;
+    return ngx_quic_payload_handler(c, pkt);
 }
 
 
 static ngx_int_t
-ngx_quic_output(ngx_connection_t *c)
+ngx_quic_app_input(ngx_connection_t *c, ngx_quic_header_t *pkt)
 {
-    size_t                  len;
-    ngx_uint_t              lvl;
-    ngx_quic_frame_t       *f, *start;
     ngx_quic_connection_t  *qc;
 
     qc = c->quic;
 
-    if (qc->frames == NULL) {
-        return NGX_OK;
+    if (qc->secrets.client.ad.key.len == 0) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "no read keys yet, packet ignored");
+        return NGX_DECLINED;
     }
 
-    lvl = qc->frames->level;
-    start = qc->frames;
-    f = start;
+    if (ngx_quic_parse_short_header(pkt, &qc->dcid) != NGX_OK) {
+        return NGX_ERROR;
+    }
 
-    do {
-        len = 0;
-
-        do {
-            /* process same-level group of frames */
+    pkt->secret = &qc->secrets.client.ad;
+    pkt->level = ssl_encryption_application;
 
-            len += ngx_quic_frame_len(f);// TODO: handle overflow, max size
+    if (ngx_quic_decrypt(c->pool, c->ssl->connection, pkt) != NGX_OK) {
+        return NGX_ERROR;
+    }
 
-            f = f->next;
-        } while (f && f->level == lvl);
+    return ngx_quic_payload_handler(c, pkt);
+}
 
 
-        if (ngx_quic_frames_send(c, start, f, len) != NGX_OK) {
-            return NGX_ERROR;
-        }
+static ngx_int_t
+ngx_quic_payload_handler(ngx_connection_t *c, ngx_quic_header_t *pkt)
+{
+    u_char                 *end, *p;
+    ssize_t                 len;
+    ngx_uint_t              ack_this, do_close;
+    ngx_quic_frame_t        frame, *ack_frame;
+    ngx_quic_connection_t  *qc;
 
-        if (f == NULL) {
-            break;
-        }
+    qc = c->quic;
 
-        lvl = f->level; // TODO: must not decrease (ever, also between calls)
-        start = f;
+    p = pkt->payload.data;
+    end = p + pkt->payload.len;
 
-    } while (1);
+    ack_this = 0;
+    do_close = 0;
 
-    qc->frames = NULL;
+    while (p < end) {
 
-    return NGX_OK;
-}
+        len = ngx_quic_parse_frame(p, end, &frame);
+        if (len < 0) {
+            ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "failed to parse frame type %xi", frame.type);
+            return NGX_ERROR;
+        }
 
+        p += len;
 
-#if BORINGSSL_API_VERSION >= 10
+        switch (frame.type) {
 
-static int
-ngx_quic_set_read_secret(ngx_ssl_conn_t *ssl_conn,
-    enum ssl_encryption_level_t level, const SSL_CIPHER *cipher,
-    const uint8_t *rsecret, size_t secret_len)
-{
-    ngx_connection_t  *c;
+        case NGX_QUIC_FT_ACK:
 
-    c = ngx_ssl_get_connection((ngx_ssl_conn_t *) ssl_conn);
+            ngx_log_debug4(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                           "ACK: { largest=%ui delay=%ui first=%ui count=%ui}",
+                           frame.u.ack.largest,
+                           frame.u.ack.delay,
+                           frame.u.ack.first_range,
+                           frame.u.ack.range_count);
 
-    ngx_quic_hexdump(c->log, "level:%d read secret",
-                     rsecret, secret_len, level);
+            if (ngx_quic_handle_ack_frame(c, pkt, &frame.u.ack) != NGX_OK) {
+                return NGX_ERROR;
+            }
 
-    return ngx_quic_set_encryption_secret(c->pool, ssl_conn, level,
-                                          rsecret, secret_len,
-                                          &c->quic->secrets.client);
-}
+            break;
 
+        case NGX_QUIC_FT_CRYPTO:
 
-static int
-ngx_quic_set_write_secret(ngx_ssl_conn_t *ssl_conn,
-    enum ssl_encryption_level_t level, const SSL_CIPHER *cipher,
-    const uint8_t *wsecret, size_t secret_len)
-{
-    ngx_connection_t  *c;
+            ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic CRYPTO frame length: %uL off:%uL pp:%p",
+                       frame.u.crypto.len, frame.u.crypto.offset,
+                       frame.u.crypto.data);
 
-    c = ngx_ssl_get_connection((ngx_ssl_conn_t *) ssl_conn);
+            ngx_quic_hexdump0(c->log, "CRYPTO frame contents",
+                          frame.u.crypto.data, frame.u.crypto.len);
 
-    ngx_quic_hexdump(c->log, "level:%d write secret",
-                     wsecret, secret_len, level);
 
-    return ngx_quic_set_encryption_secret(c->pool, ssl_conn, level,
-                                          wsecret, secret_len,
-                                          &c->quic->secrets.server);
-}
+            if (ngx_quic_handle_crypto_frame(c, pkt, &frame.u.crypto)
+                != NGX_OK)
+            {
+                return NGX_ERROR;
+            }
 
-#else
+            ack_this = 1;
+            break;
 
-static int
-ngx_quic_set_encryption_secrets(ngx_ssl_conn_t *ssl_conn,
-    enum ssl_encryption_level_t level, const uint8_t *rsecret,
-    const uint8_t *wsecret, size_t secret_len)
-{
-    ngx_int_t          rc;
-    ngx_connection_t  *c;
+        case NGX_QUIC_FT_PADDING:
+            break;
 
-    c = ngx_ssl_get_connection((ngx_ssl_conn_t *) ssl_conn);
+        case NGX_QUIC_FT_PING:
+            ack_this = 1;
+            break;
 
-    ngx_quic_hexdump(c->log, "level:%d read", rsecret, secret_len, level);
-    ngx_quic_hexdump(c->log, "level:%d write", wsecret, secret_len, level);
+        case NGX_QUIC_FT_NEW_CONNECTION_ID:
+            ack_this = 1;
+            ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                           "NCID: { seq=%ui retire=%ui len=%ui}",
+                           frame.u.ncid.seqnum,
+                           frame.u.ncid.retire,
+                           frame.u.ncid.len);
+            break;
 
-    rc = ngx_quic_set_encryption_secret(c->pool, ssl_conn, level,
-                                        rsecret, secret_len,
-                                        &c->quic->secrets.client);
-    if (rc != 1) {
-        return rc;
-    }
+        case NGX_QUIC_FT_CONNECTION_CLOSE:
+            ngx_log_debug4(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                           "CONN.CLOSE: { %s (0x%xi) type=0x%xi reason='%V'}",
+                           ngx_quic_error_text(frame.u.close.error_code),
+                           frame.u.close.error_code,
+                           frame.u.close.frame_type,
+                           &frame.u.close.reason);
 
-    return ngx_quic_set_encryption_secret(c->pool, ssl_conn, level,
-                                          wsecret, secret_len,
-                                          &c->quic->secrets.server);
-}
+            do_close = 1;
+            break;
 
-#endif
+        case NGX_QUIC_FT_STREAM0:
+        case NGX_QUIC_FT_STREAM1:
+        case NGX_QUIC_FT_STREAM2:
+        case NGX_QUIC_FT_STREAM3:
+        case NGX_QUIC_FT_STREAM4:
+        case NGX_QUIC_FT_STREAM5:
+        case NGX_QUIC_FT_STREAM6:
+        case NGX_QUIC_FT_STREAM7:
 
+            ngx_log_debug7(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                           "STREAM frame { 0x%xi id 0x%xi offset 0x%xi len 0x%xi bits:off=%d len=%d fin=%d }",
+                           frame.type,
+                           frame.u.stream.stream_id,
+                           frame.u.stream.offset,
+                           frame.u.stream.length,
+                           frame.u.stream.off,
+                           frame.u.stream.len,
+                           frame.u.stream.fin);
 
-static void
-ngx_quic_queue_frame(ngx_quic_connection_t *qc, ngx_quic_frame_t *frame)
-{
-    ngx_quic_frame_t *f;
+            ngx_quic_hexdump0(c->log, "STREAM frame contents",
+                              frame.u.stream.data, frame.u.stream.length);
 
-    if (qc->frames == NULL) {
-        qc->frames = frame;
-        return;
-    }
+            if (ngx_quic_handle_stream_frame(c, pkt, &frame.u.stream)
+                != NGX_OK)
+            {
+                return NGX_ERROR;
+            }
 
-    for (f = qc->frames; f->next; f = f->next) {
-        if (f->next->level > frame->level) {
+            ack_this = 1;
             break;
+
+        default:
+            ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                          "unsupported frame type 0x%xd in packet", frame.type);
+            return NGX_ERROR;
         }
     }
 
-    frame->next = f->next;
-    f->next = frame;
-}
-
-
-static int
-ngx_quic_add_handshake_data(ngx_ssl_conn_t *ssl_conn,
-    enum ssl_encryption_level_t level, const uint8_t *data, size_t len)
-{
-    u_char                   *p;
-    ngx_quic_frame_t         *frame;
-    ngx_connection_t         *c;
-    ngx_quic_connection_t    *qc;
-
-    c = ngx_ssl_get_connection((ngx_ssl_conn_t *) ssl_conn);
-    qc = c->quic;
-
-    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                   "ngx_quic_add_handshake_data");
-
-    frame = ngx_pcalloc(c->pool, sizeof(ngx_quic_frame_t));
-    if (frame == NULL) {
-        return 0;
+    if (p != end) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "trailing garbage in payload: %ui bytes", end - p);
+        return NGX_ERROR;
     }
 
-    p = ngx_pnalloc(c->pool, len);
-    if (p == NULL) {
-        return 0;
+    if (do_close) {
+        // TODO: handle stream close
     }
 
-    ngx_memcpy(p, data, len);
-
-    frame->level = level;
-    frame->type = NGX_QUIC_FT_CRYPTO;
-    frame->u.crypto.len = len;
-    frame->u.crypto.data = p;
-
-    ngx_sprintf(frame->info, "crypto, generated by SSL len=%ui level=%d", len, level);
-
-    ngx_quic_queue_frame(qc, frame);
-
-    return 1;
-}
+    if (ack_this == 0) {
+        /* do not ack packets with ACKs and PADDING */
+        return NGX_OK;
+    }
 
+    // packet processed, ACK it now if required
+    // TODO: if (ack_required) ...  - currently just ack each packet
 
-static int
-ngx_quic_flush_flight(ngx_ssl_conn_t *ssl_conn)
-{
-    ngx_connection_t  *c;
+    ack_frame = ngx_pcalloc(c->pool, sizeof(ngx_quic_frame_t));
+    if (ack_frame == NULL) {
+        return NGX_ERROR;
+    }
 
-    c = ngx_ssl_get_connection((ngx_ssl_conn_t *) ssl_conn);
+    ack_frame->level = pkt->level;
+    ack_frame->type = NGX_QUIC_FT_ACK;
+    ack_frame->u.ack.pn = pkt->pn;
 
-    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, "ngx_quic_flush_flight()");
+    ngx_sprintf(ack_frame->info, "ACK for PN=%d from frame handler level=%d", pkt->pn, pkt->level);
+    ngx_quic_queue_frame(qc, ack_frame);
 
-    return 1;
+    return ngx_quic_output(c);
 }
 
 
-static int
-ngx_quic_send_alert(ngx_ssl_conn_t *ssl_conn, enum ssl_encryption_level_t level,
-    uint8_t alert)
+static ngx_int_t
+ngx_quic_handle_ack_frame(ngx_connection_t *c, ngx_quic_header_t *pkt,
+    ngx_quic_ack_frame_t *f)
 {
-    ngx_connection_t  *c;
-
-    c = ngx_ssl_get_connection((ngx_ssl_conn_t *) ssl_conn);
-
-    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                   "ngx_quic_send_alert(), lvl=%d, alert=%d",
-                   (int) level, (int) alert);
-
-    return 1;
+    /* TODO: handle ACK here */
+    return NGX_OK;
 }
 
 
-
 static ngx_int_t
 ngx_quic_handle_crypto_frame(ngx_connection_t *c, ngx_quic_header_t *pkt,
-    ngx_quic_frame_t *frame)
+    ngx_quic_crypto_frame_t *f)
 {
-    int             sslerr;
-    ssize_t         n;
-    ngx_ssl_conn_t *ssl_conn;
+    int              sslerr;
+    ssize_t          n;
+    ngx_ssl_conn_t  *ssl_conn;
+
+    if (f->offset != 0x0) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "crypto frame with non-zero offset");
+        // TODO: add support for crypto frames spanning packets
+        return NGX_ERROR;
+    }
 
     ssl_conn = c->ssl->connection;
 
@@ -618,9 +876,8 @@ ngx_quic_handle_crypto_frame(ngx_connection_t *c, ngx_quic_header_t *pkt,
                    (int) SSL_quic_read_level(ssl_conn),
                    (int) SSL_quic_write_level(ssl_conn));
 
-
     if (!SSL_provide_quic_data(ssl_conn, SSL_quic_read_level(ssl_conn),
-                               frame->u.crypto.data, frame->u.crypto.len))
+                               f->data, f->len))
     {
         ngx_ssl_error(NGX_LOG_INFO, c->log, 0,
                       "SSL_provide_quic_data() failed");
@@ -654,449 +911,281 @@ ngx_quic_handle_crypto_frame(ngx_connection_t *c, ngx_quic_header_t *pkt,
 }
 
 
-
 static ngx_int_t
-ngx_quic_init_connection(ngx_connection_t *c)
+ngx_quic_handle_stream_frame(ngx_connection_t *c,
+    ngx_quic_header_t *pkt, ngx_quic_stream_frame_t *f)
 {
-    int                     n, sslerr;
-    ngx_ssl_conn_t         *ssl_conn;
-    ngx_quic_connection_t  *qc;
-
-    static const uint8_t params[] =
-        "\x00\x29"                         /* parameters length: 41 bytes         */
-        "\x00\x0e\x00\x01\x05"             /* active connection id limit: 5       */
-        "\x00\x04\x00\x04\x80\x98\x96\x80" /* initial max data = 10000000         */
-        "\x00\x09\x00\x01\x03"             /* initial max streams uni: 3          */
-        "\x00\x08\x00\x01\x10"             /* initial max streams bidi: 16        */
-        "\x00\x05\x00\x02\x40\xff"         /* initial max stream bidi local: 255  */
-        "\x00\x06\x00\x02\x40\xff"         /* initial max stream bidi remote: 255 */
-        "\x00\x07\x00\x02\x40\xff";        /* initial max stream data uni: 255    */
+    ngx_buf_t               *b;
+    ngx_log_t               *log;
+    ngx_pool_t              *pool;
+    ngx_event_t             *rev, *wev;
+    ngx_quic_connection_t   *qc;
+    ngx_quic_stream_node_t  *sn;
 
     qc = c->quic;
 
-    if (ngx_ssl_create_connection(qc->ssl, c, NGX_SSL_BUFFER) != NGX_OK) {
-        return NGX_ERROR;
-    }
+    sn = ngx_quic_find_stream(&qc->streams.tree, f->stream_id);
 
-    ssl_conn = c->ssl->connection;
+    if (sn) {
+        ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, "existing stream");
+        b = sn->b;
 
-    if (SSL_set_quic_transport_params(ssl_conn, params, sizeof(params) - 1) == 0) {
-        ngx_log_error(NGX_LOG_INFO, c->log, 0,
-                      "SSL_set_quic_transport_params() failed");
-        return NGX_ERROR;
-    }
-
-    n = SSL_do_handshake(ssl_conn);
+        if ((size_t) (b->end - b->pos) < f->length) {
+            ngx_log_error(NGX_LOG_INFO, c->log, 0, "no space in stream buffer");
+            return NGX_ERROR;
+        }
 
-    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, "SSL_do_handshake: %d", n);
+        ngx_memcpy(b->pos, f->data, f->length);
+        b->pos += f->length;
 
-    if (n == -1) {
-        sslerr = SSL_get_error(ssl_conn, n);
+        // TODO: notify
 
-        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, "SSL_get_error: %d",
-                       sslerr);
+        return NGX_OK;
     }
 
-    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                   "SSL_quic_read_level: %d, SSL_quic_write_level: %d",
-                   (int) SSL_quic_read_level(ssl_conn),
-                   (int) SSL_quic_write_level(ssl_conn));
-
-    return NGX_OK;
-}
-
-
-static ssize_t
-ngx_quic_stream_recv(ngx_connection_t *c, u_char *buf, size_t size)
-{
-    ssize_t                  len;
-    ngx_buf_t               *b;
-    ngx_quic_stream_t       *qs;
-    ngx_quic_connection_t   *qc;
-    ngx_quic_stream_node_t  *sn;
-
-    qs = c->qs;
-    qc = qs->parent->quic;
-
-    // XXX: get direct pointer from stream structure?
-    sn = ngx_quic_stream_lookup(&qc->stree, qs->id);
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, "stream is new");
 
+    sn = ngx_pcalloc(c->pool, sizeof(ngx_quic_stream_node_t));
     if (sn == NULL) {
         return NGX_ERROR;
     }
 
-    // XXX: how to return EOF?
-
-    b = sn->b;
-
-    if (b->last - b->pos == 0) {
-        c->read->ready = 0;
-        ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                       "quic recv() not ready");
-        return NGX_AGAIN; // ?
+    sn->c = ngx_get_connection(-1, c->log); // TODO: free on connection termination
+    if (sn->c == NULL) {
+        return NGX_ERROR;
     }
 
-    len = ngx_min(b->last - b->pos, (ssize_t) size);
-
-    ngx_memcpy(buf, b->pos, len);
+    pool = ngx_create_pool(NGX_DEFAULT_POOL_SIZE, c->log);
+    if (pool == NULL) {
+        /* XXX free connection */
+        return NGX_ERROR;
+    }
 
-    b->pos += len;
+    log = ngx_palloc(pool, sizeof(ngx_log_t));
+    if (log == NULL) {
+        /* XXX free pool and connection */
+        return NGX_ERROR;
+    }
 
-    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                  "quic recv: %z of %uz", len, size);
+    *log = *c->log;
+    pool->log = log;
 
-    return len;
-}
+    sn->c->log = log;
+    sn->c->pool = pool;
 
+    sn->c->listening = c->listening;
+    sn->c->sockaddr = c->sockaddr;
+    sn->c->local_sockaddr = c->local_sockaddr;
 
-static ssize_t
-ngx_quic_stream_send(ngx_connection_t *c, u_char *buf, size_t size)
-{
-    u_char                  *p;
-    ngx_connection_t        *pc;
-    ngx_quic_frame_t        *frame;
-    ngx_quic_stream_t       *qs;
-    ngx_quic_connection_t   *qc;
-    ngx_quic_stream_node_t  *sn;
+    rev = sn->c->read;
+    wev = sn->c->write;
 
-    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, "quic send: %uz", size);
+    rev->ready = 1;
 
-    qs = c->qs;
-    pc = qs->parent;
-    qc = pc->quic;
+    rev->log = c->log;
+    wev->log = c->log;
 
-    // XXX: get direct pointer from stream structure?
-    sn = ngx_quic_stream_lookup(&qc->stree, qs->id);
+    sn->c->number = ngx_atomic_fetch_add(ngx_connection_counter, 1);
 
-    if (sn == NULL) {
+    sn->node.key = f->stream_id;
+    sn->b = ngx_create_temp_buf(pool, 16 * 1024); // XXX enough for everyone
+    if (sn->b == NULL) {
         return NGX_ERROR;
     }
+    b = sn->b;
 
-    frame = ngx_pcalloc(pc->pool, sizeof(ngx_quic_frame_t));
-    if (frame == NULL) {
-        return 0;
-    }
-
-    p = ngx_pnalloc(pc->pool, size);
-    if (p == NULL) {
-        return 0;
-    }
+    ngx_memcpy(b->start, f->data, f->length);
+    b->last = b->start + f->length;
 
-    ngx_memcpy(p, buf, size);
+    ngx_rbtree_insert(&qc->streams.tree, &sn->node);
 
-    frame->level = ssl_encryption_application;
-    frame->type = NGX_QUIC_FT_STREAM6; /* OFF=1 LEN=1 FIN=0 */
-    frame->u.stream.off = 1;
-    frame->u.stream.len = 1;
-    frame->u.stream.fin = 0;
+    sn->s.id = f->stream_id;
+    sn->s.unidirectional = (sn->s.id & 0x02) ? 1 : 0;
+    sn->s.parent = c;
+    sn->c->qs = &sn->s;
 
-    frame->u.stream.type = frame->type;
-    frame->u.stream.stream_id = qs->id;
-    frame->u.stream.offset = c->sent;
-    frame->u.stream.length = size;
-    frame->u.stream.data = p;
+    sn->c->recv = ngx_quic_stream_recv;
+    sn->c->send = ngx_quic_stream_send;
+    sn->c->send_chain = ngx_quic_stream_send_chain;
 
-    c->sent += size;
+    qc->streams.handler(sn->c);
 
-    ngx_sprintf(frame->info, "stream %xi len=%ui level=%d",
-                qs->id, size, frame->level);
-
-    ngx_quic_queue_frame(qc, frame);
-
-    return size;
+    return NGX_OK;
 }
 
 
-static ngx_chain_t *
-ngx_quic_stream_send_chain(ngx_connection_t *c, ngx_chain_t *in,
-    off_t limit)
+static void
+ngx_quic_queue_frame(ngx_quic_connection_t *qc, ngx_quic_frame_t *frame)
 {
-    size_t      len;
-    ssize_t     n;
-    ngx_buf_t  *b;
-
-    for ( /* void */; in; in = in->next) {
-        b = in->buf;
-
-        if (!ngx_buf_in_memory(b)) {
-            continue;
-        }
-
-        if (ngx_buf_size(b) == 0) {
-            continue;
-        }
-
-        len = b->last - b->pos;
-
-        n = ngx_quic_stream_send(c, b->pos, len);
-
-        if (n == NGX_ERROR) {
-            return NGX_CHAIN_ERROR;
-        }
+    ngx_quic_frame_t *f;
 
-        if (n == NGX_AGAIN) {
-            return in;
-        }
+    if (qc->frames == NULL) {
+        qc->frames = frame;
+        return;
+    }
 
-        if (n != (ssize_t) len) {
-            b->pos += n;
-            return in;
+    for (f = qc->frames; f->next; f = f->next) {
+        if (f->next->level > frame->level) {
+            break;
         }
     }
 
-    return NULL;
+    frame->next = f->next;
+    f->next = frame;
 }
 
 
-/* process all payload from the current packet and generate ack if required */
 static ngx_int_t
-ngx_quic_payload_handler(ngx_connection_t *c, ngx_quic_header_t *pkt)
+ngx_quic_output(ngx_connection_t *c)
 {
-    u_char                  *end, *p;
-    ssize_t                  len;
-    ngx_buf_t               *b;
-    ngx_log_t               *log;
-    ngx_uint_t               ack_this, do_close;
-    ngx_pool_t              *pool;
-    ngx_event_t             *rev, *wev;
-    ngx_quic_frame_t         frame, *ack_frame;
-    ngx_quic_connection_t   *qc;
-    ngx_quic_stream_node_t  *sn;
+    size_t                  len;
+    ngx_uint_t              lvl;
+    ngx_quic_frame_t       *f, *start;
+    ngx_quic_connection_t  *qc;
 
     qc = c->quic;
 
-    p = pkt->payload.data;
-    end = p + pkt->payload.len;
-
-    ack_this = 0;
-    do_close = 0;
-
-    while (p < end) {
-
-        len = ngx_quic_parse_frame(p, end, &frame);
-        if (len < 0) {
-            ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                       "unknown frame type %xi", frame.type);
-            // XXX: log here
-            return NGX_ERROR;
-        }
-
-        p += len;
-
-        switch (frame.type) {
-
-        case NGX_QUIC_FT_ACK:
-
-            // TODO: handle ack
-
-            ngx_log_debug4(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                           "ACK: { largest=%ui delay=%ui first=%ui count=%ui}",
-                           frame.u.ack.largest,
-                           frame.u.ack.delay,
-                           frame.u.ack.first_range,
-                           frame.u.ack.range_count);
-
-            break;
-
-        case NGX_QUIC_FT_CRYPTO:
-            ngx_quic_hexdump0(c->log, "CRYPTO frame",
-                          frame.u.crypto.data, frame.u.crypto.len);
-
-            ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                       "quic CRYPTO frame length: %uL off:%uL pp:%p",
-                       frame.u.crypto.len, frame.u.crypto.offset,
-                       frame.u.crypto.data);
-
-            if (frame.u.crypto.offset != 0x0) {
-                ngx_log_error(NGX_LOG_INFO, c->log, 0,
-                              "crypto frame with non-zero offset");
-                // TODO: support packet spanning with offsets
-                return NGX_ERROR;
-            }
+    if (qc->frames == NULL) {
+        return NGX_OK;
+    }
 
-            if (ngx_quic_handle_crypto_frame(c, pkt, &frame) != NGX_OK) {
-                return NGX_ERROR;
-            }
+    lvl = qc->frames->level;
+    start = qc->frames;
+    f = start;
 
-            ack_this = 1;
+    do {
+        len = 0;
 
-            continue;
+        do {
+            /* process same-level group of frames */
 
-        case NGX_QUIC_FT_PADDING:
-            continue;
+            len += ngx_quic_frame_len(f);// TODO: handle overflow, max size
 
-        case NGX_QUIC_FT_PING:
-            ack_this = 1;
-            continue;
+            f = f->next;
+        } while (f && f->level == lvl);
 
-        case NGX_QUIC_FT_NEW_CONNECTION_ID:
-            ack_this = 1;
-            ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                           "NCID: { seq=%ui retire=%ui len=%ui}",
-                           frame.u.ncid.seqnum,
-                           frame.u.ncid.retire,
-                           frame.u.ncid.len);
-            continue;
 
-        case NGX_QUIC_FT_CONNECTION_CLOSE:
-            ngx_log_debug4(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                           "CONN.CLOSE: { %s (0x%xi) type=0x%xi reason='%V'}",
-                           ngx_quic_error_text(frame.u.close.error_code),
-                           frame.u.close.error_code,
-                           frame.u.close.frame_type,
-                           &frame.u.close.reason);
+        if (ngx_quic_frames_send(c, start, f, len) != NGX_OK) {
+            return NGX_ERROR;
+        }
 
-            do_close = 1;
+        if (f == NULL) {
             break;
+        }
 
-        case NGX_QUIC_FT_STREAM0:
-        case NGX_QUIC_FT_STREAM1:
-        case NGX_QUIC_FT_STREAM2:
-        case NGX_QUIC_FT_STREAM3:
-        case NGX_QUIC_FT_STREAM4:
-        case NGX_QUIC_FT_STREAM5:
-        case NGX_QUIC_FT_STREAM6:
-        case NGX_QUIC_FT_STREAM7:
-
-            ack_this = 1;
-
-            ngx_log_debug7(NGX_LOG_DEBUG_EVENT, c->log, 0,
-                           "STREAM frame 0x%xi id 0x%xi offset 0x%xi len 0x%xi bits:off=%d len=%d fin=%d",
-                           frame.type,
-                           frame.u.stream.stream_id,
-                           frame.u.stream.offset,
-                           frame.u.stream.length,
-                           frame.u.stream.off,
-                           frame.u.stream.len,
-                           frame.u.stream.fin);
-
-            sn = ngx_quic_stream_lookup(&qc->stree, frame.u.stream.stream_id);
-            if (sn == NULL) {
-                ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, "stream is new");
-
-                sn = ngx_pcalloc(c->pool, sizeof(ngx_quic_stream_node_t));
-                if (sn == NULL) {
-                    return NGX_ERROR;
-                }
-
-                sn->c = ngx_get_connection(-1, c->log); // TODO: free on connection termination
-                if (sn->c == NULL) {
-                    return NGX_ERROR;
-                }
+        lvl = f->level; // TODO: must not decrease (ever, also between calls)
+        start = f;
 
-                pool = ngx_create_pool(NGX_DEFAULT_POOL_SIZE, c->log);
-                if (pool == NULL) {
-                    /* XXX free connection */
-                    return NGX_ERROR;
-                }
+    } while (1);
 
-                log = ngx_palloc(pool, sizeof(ngx_log_t));
-                if (log == NULL) {
-                    /* XXX free pool and connection */
-                    return NGX_ERROR;
-                }
+    qc->frames = NULL;
 
-                *log = *c->log;
-                pool->log = log;
+    return NGX_OK;
+}
 
-                sn->c->log = log;
-                sn->c->pool = pool;
 
-                sn->c->listening = c->listening;
-                sn->c->sockaddr = c->sockaddr;
-                sn->c->local_sockaddr = c->local_sockaddr;
+/* pack a group of frames [start; end) into memory p and send as single packet */
+ngx_int_t
+ngx_quic_frames_send(ngx_connection_t *c, ngx_quic_frame_t *start,
+    ngx_quic_frame_t *end, size_t total)
+{
+    ssize_t            len;
+    u_char            *p;
+    ngx_str_t          out;
+    ngx_quic_frame_t  *f;
 
-                rev = sn->c->read;
-                wev = sn->c->write;
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "sending frames %p...%p", start, end);
 
-                rev->ready = 1;
+    p = ngx_pnalloc(c->pool, total);
+    if (p == NULL) {
+        return NGX_ERROR;
+    }
 
-                rev->log = c->log;
-                wev->log = c->log;
+    out.data = p;
 
-                sn->c->number = ngx_atomic_fetch_add(ngx_connection_counter, 1);
+    for (f = start; f != end; f = f->next) {
 
-                sn->node.key = frame.u.stream.stream_id;
-                sn->b = ngx_create_temp_buf(pool, 16 * 1024); // XXX enough for everyone
-                if (sn->b == NULL) {
-                    return NGX_ERROR;
-                }
-                b = sn->b;
+        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, "frame: %s", f->info);
 
-                ngx_memcpy(b->start, frame.u.stream.data, frame.u.stream.length);
-                b->last = b->start + frame.u.stream.length;
+        len = ngx_quic_create_frame(p, p + total, f);
+        if (len == -1) {
+            return NGX_ERROR;
+        }
 
-                ngx_rbtree_insert(&qc->stree, &sn->node);
+        p += len;
+    }
 
-                sn->s.id = frame.u.stream.stream_id;
-                sn->s.unidirectional = (sn->s.id & 0x02) ? 1 : 0;
-                sn->s.parent = c;
-                sn->c->qs = &sn->s;
+    out.len = p - out.data;
 
-                sn->c->recv = ngx_quic_stream_recv;
-                sn->c->send = ngx_quic_stream_send;
-                sn->c->send_chain = ngx_quic_stream_send_chain;
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "packet ready: %ui bytes at level %d",
+                   out.len, start->level);
 
-                qc->stream_handler(sn->c);
+    // IOVEC/sendmsg_chain ?
+    if (ngx_quic_send_packet(c, c->quic, start->level, &out) != NGX_OK) {
+        return NGX_ERROR;
+    }
 
-            } else {
-                ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, "existing stream");
-                b = sn->b;
+    return NGX_OK;
+}
 
-                if ((size_t) (b->end - b->pos) < frame.u.stream.length) {
-                    ngx_log_error(NGX_LOG_INFO, c->log, 0,
-                                 "no space in stream buffer");
-                    return NGX_ERROR;
-                }
 
-                ngx_memcpy(b->pos, frame.u.stream.data, frame.u.stream.length);
-                b->pos += frame.u.stream.length;
+static ngx_int_t
+ngx_quic_send_packet(ngx_connection_t *c, ngx_quic_connection_t *qc,
+    enum ssl_encryption_level_t level, ngx_str_t *payload)
+{
+    ngx_str_t         res;
+    ngx_quic_header_t pkt;
 
-                // TODO: ngx_post_event(&c->read, &ngx_posted_events) ???
-            }
+    pkt.log = c->log;
 
-            ngx_quic_hexdump0(c->log, "STREAM.data",
-                              frame.u.stream.data, frame.u.stream.length);
-            break;
+    static ngx_str_t  initial_token = ngx_null_string;
 
-        default:
-            ngx_log_error(NGX_LOG_INFO, c->log, 0,
-                          "unexpected frame type 0x%xd in packet", frame.type);
-            return NGX_ERROR;
-        }
-    }
+    ngx_memzero(&pkt, sizeof(ngx_quic_header_t));
+    ngx_quic_hexdump0(c->log, "payload", payload->data, payload->len);
 
-    if (p != end) {
-        ngx_log_error(NGX_LOG_INFO, c->log, 0,
-                      "trailing garbage in payload: %ui bytes", end - p);
-        return NGX_ERROR;
-    }
+    pkt.level = level;
+    pkt.dcid = qc->dcid;
+    pkt.scid = qc->scid;
 
-    if (do_close) {
-        // TODO: handle stream close
-    }
+    if (level == ssl_encryption_initial) {
+        pkt.number = &qc->initial_pn;
+        pkt.flags = NGX_QUIC_PKT_INITIAL;
+        pkt.secret = &qc->secrets.server.in;
+        pkt.token = initial_token;
 
-    if (ack_this == 0) {
-        /* do not ack packets with ACKs and PADDING */
-        return NGX_OK;
-    }
+    } else if (level == ssl_encryption_handshake) {
+        pkt.number = &qc->handshake_pn;
+        pkt.flags = NGX_QUIC_PKT_HANDSHAKE;
+        pkt.secret = &qc->secrets.server.hs;
 
-    // packet processed, ACK it now if required
-    // TODO: if (ack_required) ...  - currently just ack each packet
+    } else {
+        pkt.number = &qc->appdata_pn;
+        pkt.secret = &qc->secrets.server.ad;
+    }
 
-    ack_frame = ngx_pcalloc(c->pool, sizeof(ngx_quic_frame_t));
-    if (ack_frame == NULL) {
+    if (ngx_quic_encrypt(c->pool, c->ssl->connection, &pkt, payload, &res)
+        != NGX_OK)
+    {
         return NGX_ERROR;
     }
 
-    ack_frame->level = pkt->level;
-    ack_frame->type = NGX_QUIC_FT_ACK;
-    ack_frame->u.ack.pn = pkt->pn;
+    ngx_quic_hexdump0(c->log, "packet to send", res.data, res.len);
 
-    ngx_sprintf(ack_frame->info, "ACK for PN=%d from frame handler level=%d", pkt->pn, pkt->level);
-    ngx_quic_queue_frame(qc, ack_frame);
+    c->send(c, res.data, res.len); // TODO: err handling
 
-    return ngx_quic_output(c);
+    (*pkt.number)++;
+
+    return NGX_OK;
+}
+
+
+ngx_connection_t *
+ngx_quic_create_uni_stream(ngx_connection_t *c)
+{
+    /* XXX */
+    return NULL;
 }
 
 
@@ -1104,8 +1193,8 @@ static void
 ngx_quic_rbtree_insert_stream(ngx_rbtree_node_t *temp,
     ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel)
 {
-    ngx_rbtree_node_t           **p;
-    ngx_quic_stream_node_t       *qn, *qnt;
+    ngx_rbtree_node_t       **p;
+    ngx_quic_stream_node_t   *qn, *qnt;
 
     for ( ;; ) {
 
@@ -1145,7 +1234,7 @@ ngx_quic_rbtree_insert_stream(ngx_rbtree_node_t *temp,
 
 
 static ngx_quic_stream_node_t *
-ngx_quic_stream_lookup(ngx_rbtree_t *rbtree, ngx_uint_t key)
+ngx_quic_find_stream(ngx_rbtree_t *rbtree, ngx_uint_t key)
 {
     ngx_rbtree_node_t  *node, *sentinel;
 
@@ -1165,193 +1254,143 @@ ngx_quic_stream_lookup(ngx_rbtree_t *rbtree, ngx_uint_t key)
 }
 
 
-static ngx_int_t
-ngx_quic_new_connection(ngx_connection_t *c, ngx_ssl_t *ssl,
-    ngx_quic_header_t *pkt)
+static ssize_t
+ngx_quic_stream_recv(ngx_connection_t *c, u_char *buf, size_t size)
 {
-    ngx_quic_connection_t  *qc;
-
-    if (ngx_buf_size(pkt->raw) < 1200) {
-        ngx_log_error(NGX_LOG_INFO, c->log, 0, "too small UDP datagram");
-        return NGX_ERROR;
-    }
-
-    if (ngx_quic_parse_long_header(pkt) != NGX_OK) {
-        return NGX_ERROR;
-    }
+    ssize_t                  len;
+    ngx_buf_t               *b;
+    ngx_quic_stream_t       *qs;
+    ngx_quic_connection_t   *qc;
+    ngx_quic_stream_node_t  *sn;
 
-    if ((pkt->flags & 0xf0) != NGX_QUIC_PKT_INITIAL) {
-        ngx_log_error(NGX_LOG_INFO, c->log, 0,
-                      "invalid initial packet: 0x%xi", pkt->flags);
-        return NGX_ERROR;
-    }
+    qs = c->qs;
+    qc = qs->parent->quic;
 
-    if (ngx_quic_parse_initial_header(pkt) != NGX_OK) {
-        return NGX_ERROR;
-    }
+    // XXX: get direct pointer from stream structure?
+    sn = ngx_quic_find_stream(&qc->streams.tree, qs->id);
 
-    qc = ngx_pcalloc(c->pool, sizeof(ngx_quic_connection_t));
-    if (qc == NULL) {
+    if (sn == NULL) {
         return NGX_ERROR;
     }
 
-    ngx_rbtree_init(&qc->stree, &qc->stree_sentinel,
-                    ngx_quic_rbtree_insert_stream);
-
-    c->quic = qc;
-    qc->ssl = ssl;
-
-    qc->dcid.len = pkt->dcid.len;
-    qc->dcid.data = ngx_pnalloc(c->pool, pkt->dcid.len);
-    if (qc->dcid.data == NULL) {
-        return NGX_ERROR;
-    }
-    ngx_memcpy(qc->dcid.data, pkt->dcid.data, qc->dcid.len);
+    // XXX: how to return EOF?
 
-    qc->scid.len = pkt->scid.len;
-    qc->scid.data = ngx_pnalloc(c->pool, qc->scid.len);
-    if (qc->scid.data == NULL) {
-        return NGX_ERROR;
-    }
-    ngx_memcpy(qc->scid.data, pkt->scid.data, qc->scid.len);
+    b = sn->b;
 
-    qc->token.len = pkt->token.len;
-    qc->token.data = ngx_pnalloc(c->pool, qc->token.len);
-    if (qc->token.data == NULL) {
-        return NGX_ERROR;
+    if (b->last - b->pos == 0) {
+        c->read->ready = 0;
+        ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                       "quic recv() not ready");
+        return NGX_AGAIN; // ?
     }
-    ngx_memcpy(qc->token.data, pkt->token.data, qc->token.len);
 
+    len = ngx_min(b->last - b->pos, (ssize_t) size);
 
-    if (ngx_quic_set_initial_secret(c->pool, &qc->secrets, &qc->dcid)
-        != NGX_OK)
-    {
-        return NGX_ERROR;
-    }
-
-    pkt->secret = &qc->secrets.client.in;
-    pkt->level = ssl_encryption_initial;
+    ngx_memcpy(buf, b->pos, len);
 
-    if (ngx_quic_decrypt(c->pool, NULL, pkt) != NGX_OK) {
-        return NGX_ERROR;
-    }
+    b->pos += len;
 
-    if (ngx_quic_init_connection(c) != NGX_OK) {
-        return NGX_ERROR;
-    }
+    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                  "quic recv: %z of %uz", len, size);
 
-    return ngx_quic_payload_handler(c, pkt);
+    return len;
 }
 
 
-static ngx_int_t
-ngx_quic_initial_input(ngx_connection_t *c, ngx_quic_header_t *pkt)
+static ssize_t
+ngx_quic_stream_send(ngx_connection_t *c, u_char *buf, size_t size)
 {
-    ngx_ssl_conn_t         *ssl_conn;
-    ngx_quic_connection_t  *qc;
-
-    qc = c->quic;
-    ssl_conn = c->ssl->connection;
-
-    if (ngx_quic_parse_long_header(pkt) != NGX_OK) {
-        return NGX_ERROR;
-    }
-
-    if (ngx_quic_parse_initial_header(pkt) != NGX_OK) {
-        return NGX_ERROR;
-    }
-
-    pkt->secret = &qc->secrets.client.in;
-    pkt->level = ssl_encryption_initial;
-
-    if (ngx_quic_decrypt(c->pool, ssl_conn, pkt) != NGX_OK) {
-        return NGX_ERROR;
-    }
-
-    return ngx_quic_payload_handler(c, pkt);
-}
+    u_char                  *p;
+    ngx_connection_t        *pc;
+    ngx_quic_frame_t        *frame;
+    ngx_quic_stream_t       *qs;
+    ngx_quic_connection_t   *qc;
+    ngx_quic_stream_node_t  *sn;
 
+    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, "quic send: %uz", size);
 
-static ngx_int_t
-ngx_quic_handshake_input(ngx_connection_t *c, ngx_quic_header_t *pkt)
-{
-    ngx_ssl_conn_t         *ssl_conn;
-    ngx_quic_connection_t  *qc;
+    qs = c->qs;
+    pc = qs->parent;
+    qc = pc->quic;
 
-    qc = c->quic;
-    ssl_conn = c->ssl->connection;
+    // XXX: get direct pointer from stream structure?
+    sn = ngx_quic_find_stream(&qc->streams.tree, qs->id);
 
-    /* extract cleartext data into pkt */
-    if (ngx_quic_parse_long_header(pkt) != NGX_OK) {
+    if (sn == NULL) {
         return NGX_ERROR;
     }
 
-    if (pkt->dcid.len != qc->dcid.len) {
-        ngx_log_error(NGX_LOG_INFO, c->log, 0, "unexpected quic dcidl");
-        return NGX_ERROR;
+    frame = ngx_pcalloc(pc->pool, sizeof(ngx_quic_frame_t));
+    if (frame == NULL) {
+        return 0;
     }
 
-    if (ngx_memcmp(pkt->dcid.data, qc->dcid.data, qc->dcid.len) != 0) {
-        ngx_log_error(NGX_LOG_INFO, c->log, 0, "unexpected quic dcid");
-        return NGX_ERROR;
+    p = ngx_pnalloc(pc->pool, size);
+    if (p == NULL) {
+        return 0;
     }
 
-    if (pkt->scid.len != qc->scid.len) {
-        ngx_log_error(NGX_LOG_INFO, c->log, 0, "unexpected quic scidl");
-        return NGX_ERROR;
-    }
+    ngx_memcpy(p, buf, size);
 
-    if (ngx_memcmp(pkt->scid.data, qc->scid.data, qc->scid.len) != 0) {
-        ngx_log_error(NGX_LOG_INFO, c->log, 0, "unexpected quic scid");
-        return NGX_ERROR;
-    }
+    frame->level = ssl_encryption_application;
+    frame->type = NGX_QUIC_FT_STREAM6; /* OFF=1 LEN=1 FIN=0 */
+    frame->u.stream.off = 1;
+    frame->u.stream.len = 1;
+    frame->u.stream.fin = 0;
 
-    if ((pkt->flags & 0xf0) != NGX_QUIC_PKT_HANDSHAKE) {
-        ngx_log_error(NGX_LOG_INFO, c->log, 0,
-                      "invalid packet type: 0x%xi", pkt->flags);
-        return NGX_ERROR;
-    }
+    frame->u.stream.type = frame->type;
+    frame->u.stream.stream_id = qs->id;
+    frame->u.stream.offset = c->sent;
+    frame->u.stream.length = size;
+    frame->u.stream.data = p;
 
-    if (ngx_quic_parse_handshake_header(pkt) != NGX_OK) {
-        return NGX_ERROR;
-    }
+    c->sent += size;
 
-    pkt->secret = &qc->secrets.client.hs;
-    pkt->level = ssl_encryption_handshake;
+    ngx_sprintf(frame->info, "stream %xi len=%ui level=%d",
+                qs->id, size, frame->level);
 
-    if (ngx_quic_decrypt(c->pool, c->ssl->connection, pkt) != NGX_OK) {
-        return NGX_ERROR;
-    }
+    ngx_quic_queue_frame(qc, frame);
 
-    return ngx_quic_payload_handler(c, pkt);
+    return size;
 }
 
 
-static ngx_int_t
-ngx_quic_app_input(ngx_connection_t *c, ngx_quic_header_t *pkt)
+static ngx_chain_t *
+ngx_quic_stream_send_chain(ngx_connection_t *c, ngx_chain_t *in,
+    off_t limit)
 {
-    ngx_quic_connection_t  *qc;
+    size_t      len;
+    ssize_t     n;
+    ngx_buf_t  *b;
 
-    qc = c->quic;
+    for ( /* void */; in; in = in->next) {
+        b = in->buf;
 
-    if (qc->secrets.client.ad.key.len == 0) {
-        ngx_log_error(NGX_LOG_INFO, c->log, 0,
-                      "no read keys yet, packet ignored");
-        return NGX_DECLINED;
-    }
+        if (!ngx_buf_in_memory(b)) {
+            continue;
+        }
 
-    if (ngx_quic_parse_short_header(pkt, &qc->dcid) != NGX_OK) {
-        return NGX_ERROR;
-    }
+        if (ngx_buf_size(b) == 0) {
+            continue;
+        }
 
-    pkt->secret = &qc->secrets.client.ad;
-    pkt->level = ssl_encryption_application;
+        len = b->last - b->pos;
 
-    if (ngx_quic_decrypt(c->pool, c->ssl->connection, pkt) != NGX_OK) {
-        return NGX_ERROR;
-    }
+        n = ngx_quic_stream_send(c, b->pos, len);
 
-    return ngx_quic_payload_handler(c, pkt);
-}
+        if (n == NGX_ERROR) {
+            return NGX_CHAIN_ERROR;
+        }
+
+        if (n == NGX_AGAIN) {
+            return in;
+        }
 
+        if (n != (ssize_t) len) {
+            b->pos += n;
+            return in;
+        }
+    }
 
+    return NULL;
+}