# HG changeset patch # User Vladimir Homutov # Date 1601488486 -10800 # Node ID b31c0245453928d6212cc472f257b8b1e2c824df # Parent b4ef79ef1c236d79350aba85115f5071583def32 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"; ... } diff --git a/src/event/ngx_event_quic.c b/src/event/ngx_event_quic.c --- 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++; diff --git a/src/event/ngx_event_quic.h b/src/event/ngx_event_quic.h --- 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; diff --git a/src/event/ngx_event_quic_protection.c b/src/event/ngx_event_quic_protection.c --- 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) diff --git a/src/event/ngx_event_quic_protection.h b/src/event/ngx_event_quic_protection.h --- 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); diff --git a/src/event/ngx_event_quic_transport.c b/src/event/ngx_event_quic_transport.c --- 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; } diff --git a/src/event/ngx_event_quic_transport.h b/src/event/ngx_event_quic_transport.h --- 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; diff --git a/src/http/modules/ngx_http_quic_module.c b/src/http/modules/ngx_http_quic_module.c --- 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; }