changeset 8562:b31c02454539 quic

QUIC: added stateless reset support. The new "quic_stateless_reset_token_key" directive is added. It sets the endpoint key used to generate stateless reset tokens and enables feature. If the endpoint receives short-header packet that can't be matched to existing connection, a stateless reset packet is generated with a proper token. If a valid stateless reset token is found in the incoming packet, the connection is closed. Example configuration: http { quic_stateless_reset_token_key "foo"; ... }
author Vladimir Homutov <vl@nginx.com>
date Wed, 30 Sep 2020 20:54:46 +0300
parents b4ef79ef1c23
children bed310672f39
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_protection.h src/event/ngx_event_quic_transport.c src/event/ngx_event_quic_transport.h src/http/modules/ngx_http_quic_module.c
diffstat 7 files changed, 237 insertions(+), 13 deletions(-) [+]
line wrap: on
line diff
--- a/src/event/ngx_event_quic.c
+++ b/src/event/ngx_event_quic.c
@@ -36,6 +36,16 @@
 
 #define NGX_QUIC_STREAM_GONE     (void *) -1
 
+/*
+ * Endpoints MUST discard packets that are too small to be valid QUIC
+ * packets.  With the set of AEAD functions defined in [QUIC-TLS],
+ * packets that are smaller than 21 bytes are never valid.
+ */
+#define NGX_QUIC_MIN_PKT_LEN     21
+
+#define NGX_QUIC_MIN_SR_PACKET   43 /* 5 random + 16 srt + 22 padding */
+#define NGX_QUIC_MAX_SR_PACKET   1200
+
 
 typedef struct {
     ngx_rbtree_t                      tree;
@@ -154,7 +164,7 @@ typedef struct {
     uint64_t                          seqnum;
     size_t                            len;
     u_char                            id[NGX_QUIC_CID_LEN_MAX];
-    u_char                            sr_token[NGX_QUIC_SRT_LEN];
+    u_char                            sr_token[NGX_QUIC_SR_TOKEN_LEN];
 } ngx_quic_client_id_t;
 
 
@@ -184,6 +194,10 @@ static int ngx_quic_send_alert(ngx_ssl_c
 
 static ngx_quic_connection_t *ngx_quic_new_connection(ngx_connection_t *c,
     ngx_ssl_t *ssl, ngx_quic_conf_t *conf, ngx_quic_header_t *pkt);
+static ngx_int_t ngx_quic_send_stateless_reset(ngx_connection_t *c,
+    ngx_quic_conf_t *conf, ngx_quic_header_t *pkt);
+static ngx_int_t ngx_quic_process_stateless_reset(ngx_connection_t *c,
+    ngx_quic_header_t *pkt);
 static ngx_int_t ngx_quic_negotiate_version(ngx_connection_t *c,
     ngx_quic_header_t *inpkt);
 static ngx_int_t ngx_quic_new_dcid(ngx_connection_t *c,
@@ -758,6 +772,105 @@ ngx_quic_new_connection(ngx_connection_t
 
 
 static ngx_int_t
+ngx_quic_send_stateless_reset(ngx_connection_t *c, ngx_quic_conf_t *conf,
+    ngx_quic_header_t *pkt)
+{
+    u_char    *token;
+    size_t     len, max;
+    uint16_t   rndbytes;
+    u_char     buf[NGX_QUIC_MAX_SR_PACKET];
+
+    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
+                   "quic handle stateless reset output");
+
+    if (conf->sr_token_key.len == 0) {
+        return NGX_DECLINED;
+    }
+
+    if (pkt->len <= NGX_QUIC_MIN_PKT_LEN) {
+        return NGX_DECLINED;
+    }
+
+    if (pkt->len <= NGX_QUIC_MIN_SR_PACKET) {
+        len = pkt->len - 1;
+
+    } else {
+        max = ngx_min(NGX_QUIC_MAX_SR_PACKET, pkt->len * 3);
+
+        if (RAND_bytes((u_char *) &rndbytes, sizeof(rndbytes)) != 1) {
+            return NGX_ERROR;
+        }
+
+        len = (rndbytes % (max - NGX_QUIC_MIN_SR_PACKET + 1))
+              + NGX_QUIC_MIN_SR_PACKET;
+    }
+
+    if (RAND_bytes(buf, len - NGX_QUIC_SR_TOKEN_LEN) != 1) {
+        return NGX_ERROR;
+    }
+
+    buf[0] &= ~NGX_QUIC_PKT_LONG;
+    buf[0] |= NGX_QUIC_PKT_FIXED_BIT;
+
+    token = &buf[len - NGX_QUIC_SR_TOKEN_LEN];
+
+    if (ngx_quic_new_sr_token(c, &pkt->dcid, &conf->sr_token_key, token)
+        != NGX_OK)
+    {
+        return NGX_ERROR;
+    }
+
+    (void) c->send(c, buf, len);
+
+    return NGX_DECLINED;
+}
+
+
+static ngx_int_t
+ngx_quic_process_stateless_reset(ngx_connection_t *c, ngx_quic_header_t *pkt)
+{
+    u_char                 *tail, ch;
+    ngx_uint_t              i;
+    ngx_queue_t            *q;
+    ngx_quic_client_id_t   *cid;
+    ngx_quic_connection_t  *qc;
+
+    qc = c->quic;
+
+    /* A stateless reset uses an entire UDP datagram */
+    if (pkt->raw->start != pkt->data) {
+        return NGX_DECLINED;
+    }
+
+    tail = pkt->raw->last - NGX_QUIC_SR_TOKEN_LEN;
+
+    for (q = ngx_queue_head(&qc->client_ids);
+         q != ngx_queue_sentinel(&qc->client_ids);
+         q = ngx_queue_next(q))
+    {
+        cid = ngx_queue_data(q, ngx_quic_client_id_t, queue);
+
+        if (cid->seqnum == 0) {
+            /* no stateless reset token in initial connection id */
+            continue;
+        }
+
+        /* constant time comparison */
+
+        for (ch = 0, i = 0; i < NGX_QUIC_SR_TOKEN_LEN; i++) {
+            ch |= tail[i] ^ cid->sr_token[i];
+        }
+
+        if (ch == 0) {
+            return NGX_OK;
+        }
+    }
+
+    return NGX_DECLINED;
+}
+
+
+static ngx_int_t
 ngx_quic_negotiate_version(ngx_connection_t *c, ngx_quic_header_t *inpkt)
 {
     size_t             len;
@@ -1116,6 +1229,20 @@ ngx_quic_init_connection(ngx_connection_
     }
 #endif
 
+    if (qc->conf->sr_token_key.len) {
+        qc->tp.sr_enabled = 1;
+
+        if (ngx_quic_new_sr_token(c, &qc->dcid, &qc->conf->sr_token_key,
+                                  qc->tp.sr_token)
+            != NGX_OK)
+        {
+            return NGX_ERROR;
+        }
+
+        ngx_quic_hexdump(c->log, "quic stateless reset token",
+                         qc->tp.sr_token, NGX_QUIC_SR_TOKEN_LEN);
+    }
+
     len = ngx_quic_create_transport_params(NULL, NULL, &qc->tp, &clen);
     /* always succeeds */
 
@@ -1578,6 +1705,21 @@ ngx_quic_process_packet(ngx_connection_t
         }
 
         if (ngx_quic_check_peer(qc, pkt) != NGX_OK) {
+
+            if (pkt->level == ssl_encryption_application) {
+                if (ngx_quic_process_stateless_reset(c, pkt) == NGX_OK) {
+                    ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                                  "quic stateless reset packet detected");
+
+                    qc->draining = 1;
+                    ngx_quic_close_connection(c, NGX_OK);
+
+                    return NGX_OK;
+                }
+
+                return ngx_quic_send_stateless_reset(c, qc->conf, pkt);
+            }
+
             return NGX_DECLINED;
         }
 
@@ -1655,7 +1797,7 @@ ngx_quic_process_packet(ngx_connection_t
             }
 
         } else if (pkt->level == ssl_encryption_application) {
-            return NGX_DECLINED;
+            return ngx_quic_send_stateless_reset(c, conf, pkt);
 
         } else {
             return NGX_ERROR;
@@ -3337,7 +3479,7 @@ ngx_quic_handle_new_connection_id_frame(
 
         if (cid->len != f->len
             || ngx_strncmp(cid->id, f->cid, f->len) != 0
-            || ngx_strncmp(cid->sr_token, f->srt, NGX_QUIC_SRT_LEN) != 0)
+            || ngx_strncmp(cid->sr_token, f->srt, NGX_QUIC_SR_TOKEN_LEN) != 0)
         {
             /*
              * ..a sequence number is used for different connection IDs,
@@ -3360,7 +3502,7 @@ ngx_quic_handle_new_connection_id_frame(
         cid->len = f->len;
         ngx_memcpy(cid->id, f->cid, f->len);
 
-        ngx_memcpy(cid->sr_token, f->srt, NGX_QUIC_SRT_LEN);
+        ngx_memcpy(cid->sr_token, f->srt, NGX_QUIC_SR_TOKEN_LEN);
 
         ngx_queue_insert_tail(&qc->client_ids, &cid->queue);
         qc->nclient_ids++;
--- a/src/event/ngx_event_quic.h
+++ b/src/event/ngx_event_quic.h
@@ -56,6 +56,8 @@
 
 #define NGX_QUIC_SERVER_CID_LEN              20
 
+#define NGX_QUIC_SR_TOKEN_LEN                16
+
 
 typedef struct {
     /* configurable */
@@ -75,9 +77,10 @@ typedef struct {
     ngx_str_t                  original_dcid;
     ngx_str_t                  initial_scid;
     ngx_str_t                  retry_scid;
+    u_char                     sr_token[NGX_QUIC_SR_TOKEN_LEN];
+    ngx_uint_t                 sr_enabled;
 
     /* TODO */
-    u_char                     stateless_reset_token[16];
     void                      *preferred_address;
 } ngx_quic_tp_t;
 
@@ -87,6 +90,7 @@ typedef struct {
     ngx_flag_t                 retry;
     ngx_flag_t                 require_alpn;
     u_char                     token_key[32]; /* AES 256 */
+    ngx_str_t                  sr_token_key; /* stateless reset token key */
 } ngx_quic_conf_t;
 
 
--- a/src/event/ngx_event_quic_protection.c
+++ b/src/event/ngx_event_quic_protection.c
@@ -923,6 +923,62 @@ ngx_quic_create_retry_packet(ngx_quic_he
 }
 
 
+ngx_int_t
+ngx_quic_new_sr_token(ngx_connection_t *c, ngx_str_t *cid, ngx_str_t *secret,
+    u_char *token)
+{
+    uint8_t       *p;
+    size_t         is_len, key_len, info_len;
+    ngx_str_t      label;
+    const EVP_MD  *digest;
+    uint8_t       info[20];
+    uint8_t       is[SHA256_DIGEST_LENGTH];
+    uint8_t       key[SHA256_DIGEST_LENGTH];
+
+    /* 10.4.2.  Calculating a Stateless Reset Token */
+
+    digest = EVP_sha256();
+    ngx_str_set(&label, "sr_token_key");
+
+    if (ngx_hkdf_extract(is, &is_len, digest, secret->data, secret->len,
+                         cid->data, cid->len)
+       != NGX_OK)
+    {
+        ngx_ssl_error(NGX_LOG_INFO, c->log, 0,
+                      "ngx_hkdf_extract(%V) failed", &label);
+        return NGX_ERROR;
+    }
+
+    key_len = SHA256_DIGEST_LENGTH;
+
+    info_len = 2 + 1 + label.len + 1;
+
+    info[0] = 0;
+    info[1] = key_len;
+    info[2] = label.len;
+
+    p = ngx_cpymem(&info[3], label.data, label.len);
+    *p = '\0';
+
+    if (ngx_hkdf_expand(key, key_len, digest, is, is_len, info, info_len)
+        != NGX_OK)
+    {
+        ngx_ssl_error(NGX_LOG_INFO, c->log, 0,
+                      "ngx_hkdf_expand(%V) failed", &label);
+        return NGX_ERROR;
+    }
+
+    ngx_memcpy(token, key, NGX_QUIC_SR_TOKEN_LEN);
+
+#if (NGX_DEBUG)
+    ngx_quic_hexdump(c->log, "quic stateless reset token", token,
+                     NGX_QUIC_SR_TOKEN_LEN);
+#endif
+
+    return NGX_OK;
+}
+
+
 static uint64_t
 ngx_quic_parse_pn(u_char **pos, ngx_int_t len, u_char *mask,
     uint64_t *largest_pn)
--- a/src/event/ngx_event_quic_protection.h
+++ b/src/event/ngx_event_quic_protection.h
@@ -39,6 +39,8 @@ int ngx_quic_set_encryption_secret(ngx_p
 
 ngx_int_t ngx_quic_key_update(ngx_connection_t *c,
     ngx_quic_secrets_t *current, ngx_quic_secrets_t *next);
+ngx_int_t ngx_quic_new_sr_token(ngx_connection_t *c, ngx_str_t *cid,
+    ngx_str_t *key, u_char *token);
 
 ngx_int_t ngx_quic_encrypt(ngx_quic_header_t *pkt, ngx_ssl_conn_t *ssl_conn,
      ngx_str_t *res);
--- a/src/event/ngx_event_quic_transport.c
+++ b/src/event/ngx_event_quic_transport.c
@@ -774,7 +774,7 @@ ngx_quic_parse_frame(ngx_quic_header_t *
             goto error;
         }
 
-        p = ngx_quic_copy_bytes(p, end, NGX_QUIC_SRT_LEN, f->u.ncid.srt);
+        p = ngx_quic_copy_bytes(p, end, NGX_QUIC_SR_TOKEN_LEN, f->u.ncid.srt);
         if (p == NULL) {
             goto error;
         }
@@ -1553,7 +1553,7 @@ ngx_quic_parse_transport_params(u_char *
         case NGX_QUIC_TP_ORIGINAL_DCID:
         case NGX_QUIC_TP_PREFERRED_ADDRESS:
         case NGX_QUIC_TP_RETRY_SCID:
-        case NGX_QUIC_TP_STATELESS_RESET_TOKEN:
+        case NGX_QUIC_TP_SR_TOKEN:
             ngx_log_error(NGX_LOG_INFO, log, 0,
                           "quic client sent forbidden transport param"
                           " id 0x%xL", id);
@@ -1810,6 +1810,12 @@ ngx_quic_create_transport_params(u_char 
     }
 #endif
 
+    if (tp->sr_enabled) {
+        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;
     }
@@ -1851,6 +1857,12 @@ ngx_quic_create_transport_params(u_char 
     }
 #endif
 
+    if (tp->sr_enabled) {
+        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;
 }
 
--- a/src/event/ngx_event_quic_transport.h
+++ b/src/event/ngx_event_quic_transport.h
@@ -101,7 +101,7 @@
 /* Transport parameters */
 #define NGX_QUIC_TP_ORIGINAL_DCID                        0x00
 #define NGX_QUIC_TP_MAX_IDLE_TIMEOUT                     0x01
-#define NGX_QUIC_TP_STATELESS_RESET_TOKEN                0x02
+#define NGX_QUIC_TP_SR_TOKEN                             0x02
 #define NGX_QUIC_TP_MAX_UDP_PAYLOAD_SIZE                 0x03
 #define NGX_QUIC_TP_INITIAL_MAX_DATA                     0x04
 #define NGX_QUIC_TP_INITIAL_MAX_STREAM_DATA_BIDI_LOCAL   0x05
@@ -120,9 +120,6 @@
 #define NGX_QUIC_CID_LEN_MIN                                8
 #define NGX_QUIC_CID_LEN_MAX                               20
 
-#define NGX_QUIC_SRT_LEN                                   16
-
-
 typedef struct {
     uint64_t                                    largest;
     uint64_t                                    delay;
@@ -141,7 +138,7 @@ typedef struct {
     uint64_t                                    retire;
     uint8_t                                     len;
     u_char                                      cid[NGX_QUIC_CID_LEN_MAX];
-    u_char                                      srt[NGX_QUIC_SRT_LEN];
+    u_char                                      srt[NGX_QUIC_SR_TOKEN_LEN];
 } ngx_quic_new_conn_id_frame_t;
 
 
--- a/src/http/modules/ngx_http_quic_module.c
+++ b/src/http/modules/ngx_http_quic_module.c
@@ -125,6 +125,13 @@ static ngx_command_t  ngx_http_quic_comm
       offsetof(ngx_quic_conf_t, retry),
       NULL },
 
+    { ngx_string("quic_stateless_reset_token_key"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_FLAG,
+      ngx_conf_set_str_slot,
+      NGX_HTTP_SRV_CONF_OFFSET,
+      offsetof(ngx_quic_conf_t, sr_token_key),
+      NULL },
+
       ngx_null_command
 };
 
@@ -223,8 +230,10 @@ ngx_http_quic_create_srv_conf(ngx_conf_t
      *     conf->tp.original_dcid = { 0, NULL };
      *     conf->tp.initial_scid = { 0, NULL };
      *     conf->tp.retry_scid = { 0, NULL };
-     *     conf->tp.stateless_reset_token = { 0 }
+     *     conf->tp.sr_token = { 0 }
+     *     conf->tp.sr_enabled = 0
      *     conf->tp.preferred_address = NULL
+     *     conf->sr_token_key = { 0, NULL }
      */
 
     conf->tp.max_idle_timeout = NGX_CONF_UNSET_MSEC;
@@ -304,6 +313,8 @@ ngx_http_quic_merge_srv_conf(ngx_conf_t 
         }
     }
 
+    ngx_conf_merge_str_value(conf->sr_token_key, prev->sr_token_key, "");
+
     return NGX_CONF_OK;
 }