# HG changeset patch # User Sergey Kandaurov # Date 1589460438 -10800 # Node ID 7ea34e13937f0b7b97c720cec79a31dba17d8b13 # Parent b7704303a7e588649a74d4bfaefb990ff51be648 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. 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 @@ -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; 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 @@ -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; 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 @@ -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); } 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 @@ -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; } 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 @@ -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); diff --git a/src/http/v3/ngx_http_v3_module.c b/src/http/v3/ngx_http_v3_module.c --- 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; }