changeset 8383:7ea34e13937f quic

Address validation using Retry packets. The behaviour is toggled with the new directive "quic_retry on|off". QUIC token construction is made suitable for issuing with NEW_TOKEN.
author Sergey Kandaurov <pluknet@nginx.com>
date Thu, 14 May 2020 15:47:18 +0300
parents b7704303a7e5
children 52d0c4832570
files src/event/ngx_event_quic.c src/event/ngx_event_quic.h src/event/ngx_event_quic_protection.c src/event/ngx_event_quic_transport.c src/event/ngx_event_quic_transport.h src/http/v3/ngx_http_v3_module.c
diffstat 6 files changed, 497 insertions(+), 9 deletions(-) [+]
line wrap: on
line diff
--- a/src/event/ngx_event_quic.c
+++ b/src/event/ngx_event_quic.c
@@ -123,6 +123,7 @@ struct ngx_quic_connection_s {
     unsigned                          closing:1;
     unsigned                          draining:1;
     unsigned                          key_phase:1;
+    unsigned                          in_retry:1;
 };
 
 
@@ -154,6 +155,10 @@ static ngx_int_t ngx_quic_new_connection
     ngx_quic_tp_t *tp, ngx_quic_header_t *pkt,
     ngx_connection_handler_pt handler);
 static ngx_int_t ngx_quic_new_dcid(ngx_connection_t *c, ngx_str_t *odcid);
+static ngx_int_t ngx_quic_retry(ngx_connection_t *c);
+static ngx_int_t ngx_quic_new_token(ngx_connection_t *c, ngx_str_t *token);
+static ngx_int_t ngx_quic_validate_token(ngx_connection_t *c,
+    ngx_quic_header_t *pkt);
 static ngx_int_t ngx_quic_init_connection(ngx_connection_t *c);
 static void ngx_quic_input_handler(ngx_event_t *rev);
 
@@ -165,6 +170,8 @@ static ngx_int_t ngx_quic_close_streams(
 
 static ngx_int_t ngx_quic_input(ngx_connection_t *c, ngx_buf_t *b);
 static ngx_inline u_char *ngx_quic_skip_zero_padding(ngx_buf_t *b);
+static ngx_int_t ngx_quic_retry_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_handshake_input(ngx_connection_t *c,
@@ -524,7 +531,8 @@ ngx_quic_run(ngx_connection_t *c, ngx_ss
         return;
     }
 
-    ngx_add_timer(c->read, c->quic->tp.max_idle_timeout);
+    ngx_add_timer(c->read, c->quic->in_retry ? NGX_QUIC_RETRY_TIMEOUT
+                                             : c->quic->tp.max_idle_timeout);
 
     c->read->handler = ngx_quic_input_handler;
 
@@ -625,13 +633,6 @@ ngx_quic_new_connection(ngx_connection_t
     }
     ngx_memcpy(qc->scid.data, pkt->scid.data, qc->scid.len);
 
-    qc->token.len = pkt->token.len;
-    qc->token.data = ngx_pnalloc(c->pool, qc->token.len);
-    if (qc->token.data == NULL) {
-        return NGX_ERROR;
-    }
-    ngx_memcpy(qc->token.data, pkt->token.data, qc->token.len);
-
     keys = &c->quic->keys[ssl_encryption_initial];
 
     if (ngx_quic_set_initial_secret(c->pool, &keys->client, &keys->server,
@@ -641,6 +642,10 @@ ngx_quic_new_connection(ngx_connection_t
         return NGX_ERROR;
     }
 
+    if (tp->retry) {
+        return ngx_quic_retry(c);
+    }
+
     pkt->secret = &keys->client;
     pkt->level = ssl_encryption_initial;
     pkt->plaintext = buf;
@@ -707,6 +712,270 @@ ngx_quic_new_dcid(ngx_connection_t *c, n
 
 
 static ngx_int_t
+ngx_quic_retry(ngx_connection_t *c)
+{
+    ssize_t            len;
+    ngx_str_t          res, token;
+    ngx_quic_header_t  pkt;
+    u_char             buf[NGX_QUIC_RETRY_BUFFER_SIZE];
+
+    if (ngx_quic_new_token(c, &token) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    ngx_memzero(&pkt, sizeof(ngx_quic_header_t));
+    pkt.flags = NGX_QUIC_PKT_FIXED_BIT | NGX_QUIC_PKT_LONG | NGX_QUIC_PKT_RETRY;
+    pkt.log = c->log;
+    pkt.odcid = c->quic->odcid;
+    pkt.dcid = c->quic->scid;
+    pkt.scid = c->quic->dcid;
+    pkt.token = token;
+
+    res.data = buf;
+
+    if (ngx_quic_encrypt(&pkt, NULL, &res) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+#ifdef NGX_QUIC_DEBUG_PACKETS
+    ngx_quic_hexdump(c->log, "quic packet to send", res.data, res.len);
+#endif
+
+    len = c->send(c, res.data, res.len);
+    if (len == NGX_ERROR || (size_t) len != res.len) {
+        return NGX_ERROR;
+    }
+
+    c->quic->token = token;
+    c->quic->tp.original_connection_id = c->quic->odcid;
+    c->quic->in_retry = 1;
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_new_token(ngx_connection_t *c, ngx_str_t *token)
+{
+    int                   len, iv_len;
+    u_char               *data, *p, *key, *iv;
+    ngx_msec_t            now;
+    EVP_CIPHER_CTX       *ctx;
+    const EVP_CIPHER     *cipher;
+    struct sockaddr_in   *sin;
+#if (NGX_HAVE_INET6)
+    struct sockaddr_in6  *sin6;
+#endif
+    u_char                in[NGX_QUIC_MAX_TOKEN_SIZE];
+
+    switch (c->sockaddr->sa_family) {
+
+#if (NGX_HAVE_INET6)
+    case AF_INET6:
+        sin6 = (struct sockaddr_in6 *) c->sockaddr;
+
+        len = sizeof(struct in6_addr);
+        data = sin6->sin6_addr.s6_addr;
+
+        break;
+#endif
+
+#if (NGX_HAVE_UNIX_DOMAIN)
+    case AF_UNIX:
+
+        len = ngx_min(c->addr_text.len, NGX_QUIC_MAX_TOKEN_SIZE - sizeof(now));
+        data = c->addr_text.data;
+
+        break;
+#endif
+
+    default: /* AF_INET */
+        sin = (struct sockaddr_in *) c->sockaddr;
+
+        len = sizeof(in_addr_t);
+        data = (u_char *) &sin->sin_addr;
+
+        break;
+    }
+
+    p = ngx_cpymem(in, data, len);
+
+    now = ngx_current_msec;
+    len += sizeof(now);
+    ngx_memcpy(p, &now, sizeof(now));
+
+    cipher = EVP_aes_256_cbc();
+    iv_len = EVP_CIPHER_iv_length(cipher);
+
+    token->len = iv_len + len + EVP_CIPHER_block_size(cipher);
+    token->data = ngx_pnalloc(c->pool, token->len);
+    if (token->data == NULL) {
+        return NGX_ERROR;
+    }
+
+    ctx = EVP_CIPHER_CTX_new();
+    if (ctx == NULL) {
+        return NGX_ERROR;
+    }
+
+    key = c->quic->tp.token_key;
+    iv = token->data;
+
+    if (RAND_bytes(iv, iv_len) <= 0
+        || !EVP_EncryptInit_ex(ctx, cipher, NULL, key, iv))
+    {
+        EVP_CIPHER_CTX_free(ctx);
+        return NGX_ERROR;
+    }
+
+    token->len = iv_len;
+
+    if (EVP_EncryptUpdate(ctx, token->data + token->len, &len, in, len) != 1) {
+        EVP_CIPHER_CTX_free(ctx);
+        return NGX_ERROR;
+    }
+
+    token->len += len;
+
+    if (EVP_EncryptFinal_ex(ctx, token->data + token->len, &len) <= 0) {
+        EVP_CIPHER_CTX_free(ctx);
+        return NGX_ERROR;
+    }
+
+    token->len += len;
+
+    EVP_CIPHER_CTX_free(ctx);
+
+#ifdef NGX_QUIC_DEBUG_PACKETS
+    ngx_quic_hexdump(c->log, "quic new token", token->data, token->len);
+#endif
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_quic_validate_token(ngx_connection_t *c, ngx_quic_header_t *pkt)
+{
+    int                     len, tlen, iv_len;
+    u_char                 *key, *iv, *p, *data;
+    ngx_msec_t              msec;
+    EVP_CIPHER_CTX         *ctx;
+    const EVP_CIPHER       *cipher;
+    struct sockaddr_in     *sin;
+#if (NGX_HAVE_INET6)
+    struct sockaddr_in6    *sin6;
+#endif
+    ngx_quic_connection_t  *qc;
+    u_char                  tdec[NGX_QUIC_MAX_TOKEN_SIZE];
+
+    if (pkt->token.len == 0) {
+        return NGX_ERROR;
+    }
+
+    qc = c->quic;
+
+    /* Retry token */
+
+    if (qc->token.len) {
+        if (pkt->token.len != qc->token.len) {
+            return NGX_ERROR;
+        }
+
+        if (ngx_memcmp(pkt->token.data, qc->token.data, pkt->token.len) != 0) {
+            return NGX_ERROR;
+        }
+
+        return NGX_OK;
+    }
+
+    /* NEW_TOKEN in a previous connection */
+
+    cipher = EVP_aes_256_cbc();
+    key = c->quic->tp.token_key;
+    iv = pkt->token.data;
+    iv_len = EVP_CIPHER_iv_length(cipher);
+
+    /* sanity checks */
+
+    if (pkt->token.len < (size_t) iv_len + EVP_CIPHER_block_size(cipher)) {
+        return NGX_ERROR;
+    }
+
+    if (pkt->token.len > (size_t) iv_len + NGX_QUIC_MAX_TOKEN_SIZE) {
+        return NGX_ERROR;
+    }
+
+    ctx = EVP_CIPHER_CTX_new();
+    if (ctx == NULL) {
+        return NGX_ERROR;
+    }
+
+    if (!EVP_DecryptInit_ex(ctx, cipher, NULL, key, iv)) {
+        EVP_CIPHER_CTX_free(ctx);
+        return NGX_ERROR;
+    }
+
+    p = pkt->token.data + iv_len;
+    len = pkt->token.len - iv_len;
+
+    if (EVP_DecryptUpdate(ctx, tdec, &len, p, len) != 1) {
+        EVP_CIPHER_CTX_free(ctx);
+        return NGX_ERROR;
+    }
+
+    if (EVP_DecryptFinal_ex(ctx, tdec + len, &tlen) <= 0) {
+        EVP_CIPHER_CTX_free(ctx);
+        return NGX_ERROR;
+    }
+
+    EVP_CIPHER_CTX_free(ctx);
+
+    switch (c->sockaddr->sa_family) {
+
+#if (NGX_HAVE_INET6)
+    case AF_INET6:
+        sin6 = (struct sockaddr_in6 *) c->sockaddr;
+
+        len = sizeof(struct in6_addr);
+        data = sin6->sin6_addr.s6_addr;
+
+        break;
+#endif
+
+#if (NGX_HAVE_UNIX_DOMAIN)
+    case AF_UNIX:
+
+        len = ngx_min(c->addr_text.len, NGX_QUIC_MAX_TOKEN_SIZE - sizeof(msec));
+        data = c->addr_text.data;
+
+        break;
+#endif
+
+    default: /* AF_INET */
+        sin = (struct sockaddr_in *) c->sockaddr;
+
+        len = sizeof(in_addr_t);
+        data = (u_char *) &sin->sin_addr;
+
+        break;
+    }
+
+    if (ngx_memcmp(tdec, data, len) != 0) {
+        return NGX_ERROR;
+    }
+
+    ngx_memcpy(&msec, tdec + len, sizeof(msec));
+
+    if (ngx_current_msec - msec > NGX_QUIC_RETRY_LIFETIME) {
+        return NGX_DECLINED;
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
 ngx_quic_init_connection(ngx_connection_t *c)
 {
     u_char                 *p;
@@ -776,6 +1045,7 @@ ngx_quic_input_handler(ngx_event_t *rev)
     b.start = buf;
     b.end = buf + sizeof(buf);
     b.pos = b.last = b.start;
+    b.memory = 1;
 
     c = rev->data;
     qc = c->quic;
@@ -1047,6 +1317,10 @@ ngx_quic_input(ngx_connection_t *c, ngx_
         pkt.log = c->log;
         pkt.flags = p[0];
 
+        if (c->quic->in_retry) {
+            return ngx_quic_retry_input(c, &pkt);
+        }
+
         /* TODO: check current state */
         if (ngx_quic_long_pkt(pkt.flags)) {
 
@@ -1111,6 +1385,93 @@ ngx_quic_skip_zero_padding(ngx_buf_t *b)
 
 
 static ngx_int_t
+ngx_quic_retry_input(ngx_connection_t *c, ngx_quic_header_t *pkt)
+{
+    ngx_quic_secrets_t     *keys;
+    ngx_quic_send_ctx_t    *ctx;
+    ngx_quic_connection_t  *qc;
+    static u_char           buf[NGX_QUIC_DEFAULT_MAX_PACKET_SIZE];
+
+    c->log->action = "retrying quic connection";
+
+    qc = c->quic;
+
+    if (ngx_buf_size(pkt->raw) < NGX_QUIC_MIN_INITIAL_SIZE) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "quic UDP datagram is too small for initial packet");
+        return NGX_OK;
+    }
+
+    if (ngx_quic_parse_long_header(pkt) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    if (ngx_quic_pkt_zrtt(pkt->flags)) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "quic discard inflight 0-RTT packet");
+        return NGX_OK;
+    }
+
+    if (!ngx_quic_pkt_in(pkt->flags)) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "quic invalid initial packet: 0x%xi", pkt->flags);
+        return NGX_ERROR;
+    }
+
+    if (ngx_quic_parse_initial_header(pkt) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    if (ngx_quic_new_dcid(c, &pkt->dcid) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    qc = c->quic;
+
+    keys = &c->quic->keys[ssl_encryption_initial];
+
+    if (ngx_quic_set_initial_secret(c->pool, &keys->client, &keys->server,
+                                    &qc->odcid)
+        != NGX_OK)
+    {
+        return NGX_ERROR;
+    }
+
+    c->quic->in_retry = 0;
+
+    if (ngx_quic_validate_token(c, pkt) != NGX_OK) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0, "quic invalid token");
+        return NGX_ERROR;
+    }
+
+    pkt->secret = &keys->client;
+    pkt->level = ssl_encryption_initial;
+    pkt->plaintext = buf;
+
+    ctx = ngx_quic_get_send_ctx(qc, pkt->level);
+
+    if (ngx_quic_decrypt(pkt, NULL, &ctx->largest_pn) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    if (ngx_quic_init_connection(c) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    if (ngx_quic_payload_handler(c, pkt) != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    /* pos is at header end, adjust by actual packet length */
+    pkt->raw->pos += pkt->len;
+
+    (void) ngx_quic_skip_zero_padding(pkt->raw);
+
+    return ngx_quic_input(c, pkt->raw);
+}
+
+
+static ngx_int_t
 ngx_quic_initial_input(ngx_connection_t *c, ngx_quic_header_t *pkt)
 {
     ngx_ssl_conn_t       *ssl_conn;
--- a/src/event/ngx_event_quic.h
+++ b/src/event/ngx_event_quic.h
@@ -23,6 +23,13 @@
 #define NGX_QUIC_DEFAULT_ACK_DELAY_EXPONENT  3
 #define NGX_QUIC_DEFAULT_MAX_ACK_DELAY       25
 
+#define NGX_QUIC_RETRY_TIMEOUT               3000
+#define NGX_QUIC_RETRY_LIFETIME              30000
+#define NGX_QUIC_RETRY_BUFFER_SIZE           128
+    /* 1 flags + 4 version + 3 x (1 + 20) s/o/dcid + itag + token(44) */
+#define NGX_QUIC_MAX_TOKEN_SIZE              32
+    /* sizeof(struct in6_addr) + sizeof(ngx_msec_t) up to AES-256 block size */
+
 #define NGX_QUIC_HARDCODED_PTO               1000 /* 1s, TODO: collect */
 #define NGX_QUIC_CC_MIN_INTERVAL             1000 /* 1s */
 
@@ -49,9 +56,12 @@ typedef struct {
     ngx_uint_t                 ack_delay_exponent;
     ngx_uint_t                 disable_active_migration;
     ngx_uint_t                 active_connection_id_limit;
+    ngx_str_t                  original_connection_id;
+
+    ngx_flag_t                 retry;
+    u_char                     token_key[32]; /* AES 256 */
 
     /* TODO */
-    ngx_uint_t                 original_connection_id;
     u_char                     stateless_reset_token[16];
     void                      *preferred_address;
 } ngx_quic_tp_t;
--- a/src/event/ngx_event_quic_protection.c
+++ b/src/event/ngx_event_quic_protection.c
@@ -57,6 +57,8 @@ static ngx_int_t ngx_quic_create_long_pa
     ngx_ssl_conn_t *ssl_conn, ngx_str_t *res);
 static ngx_int_t ngx_quic_create_short_packet(ngx_quic_header_t *pkt,
     ngx_ssl_conn_t *ssl_conn, ngx_str_t *res);
+static ngx_int_t ngx_quic_create_retry_packet(ngx_quic_header_t *pkt,
+    ngx_str_t *res);
 
 
 static ngx_int_t
@@ -891,6 +893,53 @@ ngx_quic_create_short_packet(ngx_quic_he
 }
 
 
+static ngx_int_t
+ngx_quic_create_retry_packet(ngx_quic_header_t *pkt, ngx_str_t *res)
+{
+    u_char              *start;
+    ngx_str_t            ad, itag;
+    ngx_quic_secret_t    secret;
+    ngx_quic_ciphers_t   ciphers;
+
+    /* 5.8.  Retry Packet Integrity */
+    static u_char     key[16] =
+        "\x4d\x32\xec\xdb\x2a\x21\x33\xc8"
+        "\x41\xe4\x04\x3d\xf2\x7d\x44\x30";
+    static u_char     nonce[12] =
+        "\x4d\x16\x11\xd0\x55\x13"
+        "\xa5\x52\xc5\x87\xd5\x75";
+    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;
+
+#ifdef NGX_QUIC_DEBUG_CRYPTO
+    ngx_quic_hexdump(pkt->log, "quic retry itag", ad.data, ad.len);
+#endif
+
+    if (ngx_quic_ciphers(NULL, &ciphers, pkt->level) == NGX_ERROR) {
+        return NGX_ERROR;
+    }
+
+    secret.key.len = sizeof(key);
+    secret.key.data = key;
+    secret.iv.len = sizeof(nonce);
+
+    if (ngx_quic_tls_seal(ciphers.c, &secret, &itag, nonce, &in, &ad, pkt->log)
+        != NGX_OK)
+    {
+        return NGX_ERROR;
+    }
+
+    res->len = itag.data + itag.len - start;
+    res->data = start;
+
+    return NGX_OK;
+}
+
+
 static uint64_t
 ngx_quic_parse_pn(u_char **pos, ngx_int_t len, u_char *mask,
     uint64_t *largest_pn)
@@ -952,6 +1001,10 @@ ngx_quic_encrypt(ngx_quic_header_t *pkt,
         return ngx_quic_create_short_packet(pkt, ssl_conn, res);
     }
 
+    if (ngx_quic_pkt_retry(pkt->flags)) {
+        return ngx_quic_create_retry_packet(pkt, res);
+    }
+
     return ngx_quic_create_long_packet(pkt, ssl_conn, res);
 }
 
--- a/src/event/ngx_event_quic_transport.c
+++ b/src/event/ngx_event_quic_transport.c
@@ -385,6 +385,35 @@ ngx_quic_create_short_header(ngx_quic_he
 }
 
 
+size_t
+ngx_quic_create_retry_itag(ngx_quic_header_t *pkt, u_char *out,
+    u_char **start)
+{
+    u_char  *p;
+
+    p = out;
+
+    *p++ = pkt->odcid.len;
+    p = ngx_cpymem(p, pkt->odcid.data, pkt->odcid.len);
+
+    *start = p;
+
+    *p++ = 0xff;
+
+    p = ngx_quic_write_uint32(p, NGX_QUIC_VERSION);
+
+    *p++ = pkt->dcid.len;
+    p = ngx_cpymem(p, pkt->dcid.data, pkt->dcid.len);
+
+    *p++ = pkt->scid.len;
+    p = ngx_cpymem(p, pkt->scid.data, pkt->scid.len);
+
+    p = ngx_cpymem(p, pkt->token.data, pkt->token.len);
+
+    return p - out;
+}
+
+
 ngx_int_t
 ngx_quic_parse_short_header(ngx_quic_header_t *pkt, ngx_str_t *dcid)
 {
@@ -1553,6 +1582,12 @@ ngx_quic_create_transport_params(u_char 
     len += ngx_quic_tp_len(NGX_QUIC_TP_MAX_IDLE_TIMEOUT,
                            tp->max_idle_timeout);
 
+    if (tp->retry) {
+        len += ngx_quic_varint_len(NGX_QUIC_TP_ORIGINAL_CONNECTION_ID);
+        len += ngx_quic_varint_len(tp->original_connection_id.len);
+        len += tp->original_connection_id.len;
+    }
+
     if (pos == NULL) {
         return len;
     }
@@ -1581,6 +1616,13 @@ ngx_quic_create_transport_params(u_char 
     ngx_quic_tp_vint(NGX_QUIC_TP_MAX_IDLE_TIMEOUT,
                      tp->max_idle_timeout);
 
+    if (tp->retry) {
+        ngx_quic_build_int(&p, NGX_QUIC_TP_ORIGINAL_CONNECTION_ID);
+        ngx_quic_build_int(&p, tp->original_connection_id.len);
+        p = ngx_cpymem(p, tp->original_connection_id.data,
+                       tp->original_connection_id.len);
+    }
+
     return p - pos;
 }
 
--- a/src/event/ngx_event_quic_transport.h
+++ b/src/event/ngx_event_quic_transport.h
@@ -280,6 +280,7 @@ typedef struct {
     size_t                                      len;
 
     /* cleartext fields */
+    ngx_str_t                                   odcid; /* retry packet tag */
     ngx_str_t                                   dcid;
     ngx_str_t                                   scid;
     uint64_t                                    pn;
@@ -303,6 +304,9 @@ ngx_int_t ngx_quic_parse_short_header(ng
 size_t ngx_quic_create_short_header(ngx_quic_header_t *pkt, u_char *out,
     size_t pkt_len, u_char **pnp);
 
+size_t ngx_quic_create_retry_itag(ngx_quic_header_t *pkt, u_char *out,
+    u_char **start);
+
 ngx_int_t ngx_quic_parse_initial_header(ngx_quic_header_t *pkt);
 ngx_int_t ngx_quic_parse_handshake_header(ngx_quic_header_t *pkt);
 
--- a/src/http/v3/ngx_http_v3_module.c
+++ b/src/http/v3/ngx_http_v3_module.c
@@ -111,6 +111,13 @@ static ngx_command_t  ngx_http_v3_comman
       offsetof(ngx_http_v3_srv_conf_t, quic.active_connection_id_limit),
       &ngx_http_v3_active_connection_id_limit_bounds },
 
+    { ngx_string("quic_retry"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_FLAG,
+      ngx_conf_set_flag_slot,
+      NGX_HTTP_SRV_CONF_OFFSET,
+      offsetof(ngx_http_v3_srv_conf_t, quic.retry),
+      NULL },
+
       ngx_null_command
 };
 
@@ -257,6 +264,8 @@ ngx_http_v3_create_srv_conf(ngx_conf_t *
     v3cf->quic.disable_active_migration = NGX_CONF_UNSET_UINT;
     v3cf->quic.active_connection_id_limit = NGX_CONF_UNSET_UINT;
 
+    v3cf->quic.retry = NGX_CONF_UNSET;
+
     return v3cf;
 }
 
@@ -310,6 +319,15 @@ ngx_http_v3_merge_srv_conf(ngx_conf_t *c
     ngx_conf_merge_uint_value(conf->quic.active_connection_id_limit,
                               prev->quic.active_connection_id_limit, 2);
 
+    ngx_conf_merge_value(conf->quic.retry, prev->quic.retry, 0);
+
+    if (conf->quic.retry) {
+        if (RAND_bytes(conf->quic.token_key, sizeof(conf->quic.token_key)) <= 0) {
+            return NGX_CONF_ERROR;
+        }
+    }
+
+
     return NGX_CONF_OK;
 }