From 9c748d20d5ffd57f5c81bdba30047db4e2bfa02f Mon Sep 17 00:00:00 2001 From: Tatsuhiro Tsujikawa Date: Sat, 8 Jun 2019 22:05:25 +0900 Subject: [PATCH] [WIP] Add QUIC to h2load --- configure.ac | 16 + src/Makefile.am | 9 +- src/h2load.cc | 196 ++++++- src/h2load.h | 81 +++ src/h2load_http3_session.cc | 375 ++++++++++++ src/h2load_http3_session.h | 77 +++ src/h2load_quic.cc | 1078 +++++++++++++++++++++++++++++++++++ src/h2load_quic.h | 38 ++ src/quic.cc | 422 ++++++++++++++ src/quic.h | 104 ++++ src/util.cc | 50 ++ src/util.h | 3 + 12 files changed, 2435 insertions(+), 14 deletions(-) create mode 100644 src/h2load_http3_session.cc create mode 100644 src/h2load_http3_session.h create mode 100644 src/h2load_quic.cc create mode 100644 src/h2load_quic.h create mode 100644 src/quic.cc create mode 100644 src/quic.h diff --git a/configure.ac b/configure.ac index 35eb47c8..3fcf05aa 100644 --- a/configure.ac +++ b/configure.ac @@ -454,6 +454,20 @@ if test "x${request_libcares}" = "xyes" && AC_MSG_ERROR([libcares was requested (--with-libcares) but not found]) fi +# ngtcp2 (for src) +PKG_CHECK_MODULES([LIBNGTCP2], [libngtcp2 >= 0.0.0], [have_libngtcp2=yes], + [have_libngtcp2=no]) +if test "x${have_libngtcp2}" = "xno"; then + AC_MSG_NOTICE($LIBNGTCP2_PKG_ERRORS) +fi + +# nghttp3 (for src) +PKG_CHECK_MODULES([LIBNGHTTP3], [libnghttp3 >= 0.0.0], [have_libnghttp3=yes], + [have_libnghttp3=no]) +if test "x${have_libnghttp3}" = "xno"; then + AC_MSG_NOTICE($LIBNGHTTP3_PKT_ERRORS) +fi + # libevent_openssl (for examples) # 2.0.8 is required because we use evconnlistener_set_error_cb() have_libevent_openssl=no @@ -1011,6 +1025,8 @@ AC_MSG_NOTICE([summary of build options: Libxml2: ${have_libxml2} (CFLAGS='${LIBXML2_CFLAGS}' LIBS='${LIBXML2_LIBS}') Libev: ${have_libev} (CFLAGS='${LIBEV_CFLAGS}' LIBS='${LIBEV_LIBS}') Libc-ares: ${have_libcares} (CFLAGS='${LIBCARES_CFLAGS}' LIBS='${LIBCARES_LIBS}') + libngtcp2: ${have_libngtcp2} (CFLAGS='${LIBNGTCP2_CFLAGS}' LIBS='${LIBNGTCP2_LIBS}') + libnghttp3: ${have_libnghttp3} (CFLAGS='${LIBNGHTTP3_CFLAGS}' LIBS='${LIBNGHTTP3_LIBS}') Libevent(SSL): ${have_libevent_openssl} (CFLAGS='${LIBEVENT_OPENSSL_CFLAGS}' LIBS='${LIBEVENT_OPENSSL_LIBS}') Jansson: ${have_jansson} (CFLAGS='${JANSSON_CFLAGS}' LIBS='${JANSSON_LIBS}') Jemalloc: ${have_jemalloc} (CFLAGS='${JEMALLOC_CFLAGS}' LIBS='${JEMALLOC_LIBS}') diff --git a/src/Makefile.am b/src/Makefile.am index 033b7f8f..ee6652f5 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -46,6 +46,8 @@ AM_CPPFLAGS = \ @LIBEV_CFLAGS@ \ @OPENSSL_CFLAGS@ \ @LIBCARES_CFLAGS@ \ + @LIBNGHTTP3_CFLAGS@ \ + @LIBNGTCP2_CFLAGS@ \ @JANSSON_CFLAGS@ \ @ZLIB_CFLAGS@ \ @DEFS@ @@ -59,6 +61,8 @@ LDADD = $(top_builddir)/lib/libnghttp2.la \ @LIBEV_LIBS@ \ @OPENSSL_LIBS@ \ @LIBCARES_LIBS@ \ + @LIBNGHTTP3_LIBS@ \ + @LIBNGTCP2_LIBS@ \ @SYSTEMD_LIBS@ \ @JANSSON_LIBS@ \ @ZLIB_LIBS@ \ @@ -97,7 +101,10 @@ h2load_SOURCES = util.cc util.h \ tls.cc tls.h \ h2load_session.h \ h2load_http2_session.cc h2load_http2_session.h \ - h2load_http1_session.cc h2load_http1_session.h + h2load_http1_session.cc h2load_http1_session.h \ + h2load_http3_session.cc h2load_http3_session.h \ + h2load_quic.cc h2load_quic.h \ + quic.cc quic.h NGHTTPX_SRCS = \ util.cc util.h http2.cc http2.h timegm.c timegm.h base64.h \ diff --git a/src/h2load.cc b/src/h2load.cc index 7d3d8e04..74cbcaf7 100644 --- a/src/h2load.cc +++ b/src/h2load.cc @@ -48,10 +48,14 @@ #include +#include + #include "url-parser/url_parser.h" #include "h2load_http1_session.h" #include "h2load_http2_session.h" +#include "h2load_http3_session.h" +#include "h2load_quic.h" #include "tls.h" #include "http2.h" #include "util.h" @@ -119,6 +123,9 @@ bool Config::is_rate_mode() const { return (this->rate != 0); } bool Config::is_timing_based_mode() const { return (this->duration > 0); } bool Config::has_base_uri() const { return (!this->base_uri.empty()); } bool Config::rps_enabled() const { return this->rps > 0.0; } +bool Config::is_quic() const { + return !npn_list.empty() && npn_list[0] == NGTCP2_ALPN_H3; +} Config config; namespace { @@ -409,6 +416,7 @@ Client::Client(uint32_t id, Worker *worker, size_t req_todo) cstat{}, worker(worker), ssl(nullptr), + quic{}, next_addr(config.addrs), current_addr(nullptr), reqidx(0), @@ -420,6 +428,7 @@ Client::Client(uint32_t id, Worker *worker, size_t req_todo) req_done(0), id(id), fd(-1), + local_addr{}, new_connection_requested(false), final(false), rps_duration_started(0), @@ -449,11 +458,18 @@ Client::Client(uint32_t id, Worker *worker, size_t req_todo) ev_timer_init(&rps_watcher, rps_cb, 0., 0.); rps_watcher.data = this; + + ev_timer_init(&quic.pkt_timer, quic_pkt_timeout_cb, 0., 0.); + quic.pkt_timer.data = this; } Client::~Client() { disconnect(); + if (config.is_quic()) { + quic_free(); + } + if (ssl) { SSL_free(ssl); } @@ -466,6 +482,37 @@ int Client::do_read() { return readfn(*this); } int Client::do_write() { return writefn(*this); } int Client::make_socket(addrinfo *addr) { + int rv; + + if (config.is_quic()) { + fd = util::create_nonblock_udp_socket(addr->ai_family); + if (fd == -1) { + return -1; + } + + rv = util::bind_any_addr_udp(fd, addr->ai_family); + if (rv != 0) { + close(fd); + fd = -1; + return -1; + } + + socklen_t addrlen = sizeof(local_addr.su.storage); + rv = getsockname(fd, &local_addr.su.sa, &addrlen); + if (rv == -1) { + return -1; + } + local_addr.len = addrlen; + + if (quic_init(&local_addr.su.sa, local_addr.len, addr->ai_addr, + addr->ai_addrlen) != 0) { + std::cerr << "quic_init failed" << std::endl; + return -1; + } + + return 0; + } + fd = util::create_nonblock_socket(addr->ai_family); if (fd == -1) { return -1; @@ -475,17 +522,15 @@ int Client::make_socket(addrinfo *addr) { ssl = SSL_new(worker->ssl_ctx); } - auto config = worker->config; - - if (!util::numeric_host(config->host.c_str())) { - SSL_set_tlsext_host_name(ssl, config->host.c_str()); - } - SSL_set_fd(ssl, fd); SSL_set_connect_state(ssl); } - auto rv = ::connect(fd, addr->ai_addr, addr->ai_addrlen); + if (ssl && !util::numeric_host(config.host.c_str())) { + SSL_set_tlsext_host_name(ssl, config.host.c_str()); + } + + rv = ::connect(fd, addr->ai_addr, addr->ai_addrlen); if (rv != 0 && errno != EINPROGRESS) { if (ssl) { SSL_free(ssl); @@ -542,13 +587,20 @@ int Client::connect() { current_addr = addr; } - writefn = &Client::connected; - ev_io_set(&rev, fd, EV_READ); ev_io_set(&wev, fd, EV_WRITE); ev_io_start(worker->loop, &wev); + if (config.is_quic()) { + ev_io_start(worker->loop, &rev); + + readfn = &Client::read_quic; + writefn = &Client::write_quic; + } else { + writefn = &Client::connected; + } + return 0; } @@ -603,6 +655,11 @@ void Client::fail() { void Client::disconnect() { record_client_end_time(); + if (config.is_quic()) { + quic_close_connection(); + } + + ev_timer_stop(worker->loop, &quic.pkt_timer); ev_timer_stop(worker->loop, &conn_inactivity_watcher); ev_timer_stop(worker->loop, &conn_active_watcher); ev_timer_stop(worker->loop, &rps_watcher); @@ -765,6 +822,9 @@ void Client::report_app_info() { } void Client::terminate_session() { + if (config.is_quic()) { + quic.close_requested = true; + } session->terminate(); // http1 session needs writecb to tear down session. signal_write(); @@ -963,7 +1023,15 @@ int Client::connection_made() { if (next_proto) { auto proto = StringRef{next_proto, next_proto_len}; - if (util::check_h2_is_selected(proto)) { + if (config.is_quic()) { + if (util::streq(StringRef{&NGTCP2_ALPN_H3[1]}, proto)) { + auto s = std::make_unique(this); + if (s->init_conn() == -1) { + return -1; + } + session = std::move(s); + } + } else if (util::check_h2_is_selected(proto)) { session = std::make_unique(this); } else if (util::streq(NGHTTP2_H1_1, proto)) { session = std::make_unique(this); @@ -1285,6 +1353,15 @@ int Client::write_tls() { return 0; } +int Client::write_udp(const sockaddr *addr, socklen_t addrlen, + const uint8_t *data, size_t datalen) { + auto nwrite = sendto(fd, data, datalen, MSG_DONTWAIT, addr, addrlen); + if (nwrite < 0) { + std::cerr << "sendto: errno=" << errno << std::endl; + } + return 0; +} + void Client::record_request_time(RequestStat *req_stat) { req_stat->request_time = std::chrono::steady_clock::now(); req_stat->request_wall_time = std::chrono::system_clock::now(); @@ -1698,6 +1775,79 @@ int client_select_next_proto_cb(SSL *ssl, unsigned char **out, } // namespace #endif // !OPENSSL_NO_NEXTPROTONEG +namespace { +int quic_transport_params_add_cb(SSL *ssl, unsigned int ext_type, + unsigned int content, + const unsigned char **out, size_t *outlen, + X509 *x, size_t chainidx, int *al, + void *add_arg) { + auto c = static_cast(SSL_get_app_data(ssl)); + auto conn = c->quic.conn; + + ngtcp2_transport_params params; + + ngtcp2_conn_get_local_transport_params(conn, ¶ms); + + constexpr size_t bufsize = 128; + auto buf = std::make_unique(bufsize); + + auto nwrite = ngtcp2_encode_transport_params( + buf.get(), bufsize, NGTCP2_TRANSPORT_PARAMS_TYPE_CLIENT_HELLO, ¶ms); + if (nwrite < 0) { + std::cerr << "ngtcp2_encode_transport_params: " << ngtcp2_strerror(nwrite) + << std::endl; + *al = SSL_AD_INTERNAL_ERROR; + return -1; + } + + *out = buf.release(); + *outlen = static_cast(nwrite); + + return 1; +} +} // namespace + +namespace { +void quic_transport_params_free_cb(SSL *ssl, unsigned int ext_type, + unsigned int context, + const unsigned char *out, void *add_arg) { + delete[] const_cast(out); +} +} // namespace + +namespace { +int quic_transport_params_parse_cb(SSL *ssl, unsigned int ext_type, + unsigned int context, + const unsigned char *in, size_t inlen, + X509 *x, size_t chainidx, int *al, + void *parse_arg) { + auto c = static_cast(SSL_get_app_data(ssl)); + auto conn = c->quic.conn; + + int rv; + ngtcp2_transport_params params; + + rv = ngtcp2_decode_transport_params( + ¶ms, NGTCP2_TRANSPORT_PARAMS_TYPE_ENCRYPTED_EXTENSIONS, in, inlen); + if (rv != 0) { + std::cerr << "ngtcp2_decode_transport_params: " << ngtcp2_strerror(rv) + << std::endl; + *al = SSL_AD_ILLEGAL_PARAMETER; + return -1; + } + + rv = ngtcp2_conn_set_remote_transport_params(conn, ¶ms); + if (rv != 0) { + std::cerr << "ngtcp2_conn_set_remote_transport_params: " + << ngtcp2_strerror(rv) << std::endl; + *al = SSL_AD_ILLEGAL_PARAMETER; + return -1; + } + + return 1; +} +} // namespace + namespace { constexpr char UNIX_PATH_PREFIX[] = "unix:"; } // namespace @@ -2576,9 +2726,26 @@ int main(int argc, char **argv) { SSL_CTX_set_mode(ssl_ctx, SSL_MODE_AUTO_RETRY); SSL_CTX_set_mode(ssl_ctx, SSL_MODE_RELEASE_BUFFERS); - if (nghttp2::tls::ssl_ctx_set_proto_versions( - ssl_ctx, nghttp2::tls::NGHTTP2_TLS_MIN_VERSION, - nghttp2::tls::NGHTTP2_TLS_MAX_VERSION) != 0) { + if (config.is_quic()) { + SSL_CTX_set_min_proto_version(ssl_ctx, TLS1_3_VERSION); + SSL_CTX_set_max_proto_version(ssl_ctx, TLS1_3_VERSION); + + SSL_CTX_clear_options(ssl_ctx, SSL_OP_ENABLE_MIDDLEBOX_COMPAT); + SSL_CTX_set_mode(ssl_ctx, SSL_MODE_QUIC_HACK); + + if (SSL_CTX_add_custom_ext( + ssl_ctx, NGTCP2_TLSEXT_QUIC_TRANSPORT_PARAMETERS, + SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_3_ENCRYPTED_EXTENSIONS, + quic_transport_params_add_cb, quic_transport_params_free_cb, + nullptr, quic_transport_params_parse_cb, nullptr) != 1) { + std::cerr << "SSL_CTX_add_custom_ext(NGTCP2_TLSEXT_QUIC_TRANSPORT_" + "PARAMETERS) failed: " + << ERR_error_string(ERR_get_error(), nullptr) << std::endl; + exit(EXIT_FAILURE); + } + } else if (nghttp2::tls::ssl_ctx_set_proto_versions( + ssl_ctx, nghttp2::tls::NGHTTP2_TLS_MIN_VERSION, + nghttp2::tls::NGHTTP2_TLS_MAX_VERSION) != 0) { std::cerr << "Could not set TLS versions" << std::endl; exit(EXIT_FAILURE); } @@ -2590,6 +2757,9 @@ int main(int argc, char **argv) { exit(EXIT_FAILURE); } + // TODO Use SSL_CTX_set_ciphersuites to set TLSv1.3 cipher list + // TODO Use SSL_CTX_set1_groups_list to set key share + #ifndef OPENSSL_NO_NEXTPROTONEG SSL_CTX_set_next_proto_select_cb(ssl_ctx, client_select_next_proto_cb, nullptr); diff --git a/src/h2load.h b/src/h2load.h index fead67c5..a9d617b1 100644 --- a/src/h2load.h +++ b/src/h2load.h @@ -45,11 +45,14 @@ #include +#include + #include #include #include "http2.h" +#include "quic.h" #include "memchunk.h" #include "template.h" @@ -124,6 +127,7 @@ struct Config { bool is_timing_based_mode() const; bool has_base_uri() const; bool rps_enabled() const; + bool is_quic() const; }; struct RequestStat { @@ -299,6 +303,13 @@ struct Stream { Stream(); }; +struct Crypto { + Crypto() : datalen(0), acked_offset(0) {} + std::array data; + size_t datalen; + size_t acked_offset; +}; + struct Client { DefaultMemchunks wb; std::unordered_map streams; @@ -309,6 +320,19 @@ struct Client { std::function readfn, writefn; Worker *worker; SSL *ssl; + struct { + ev_timer pkt_timer; + ngtcp2_conn *conn; + quic::Error last_error; + ngtcp2_crypto_level tx_crypto_level; + ngtcp2_crypto_level rx_crypto_level; + std::vector server_handshake; + size_t server_handshake_nread; + // Client never send CRYPTO in Short packet. + std::array crypto; + size_t max_pktlen; + bool close_requested; + } quic; ev_timer request_timeout_watcher; addrinfo *next_addr; // Address for the current address. When try_new_connection() is @@ -332,6 +356,7 @@ struct Client { // The client id per worker uint32_t id; int fd; + Address local_addr; ev_timer conn_active_watcher; ev_timer conn_inactivity_watcher; std::string selected_proto; @@ -419,6 +444,62 @@ struct Client { void record_client_end_time(); void signal_write(); + + // QUIC + int quic_init(const sockaddr *local_addr, socklen_t local_addrlen, + const sockaddr *remote_addr, socklen_t remote_addrlen); + void quic_free(); + int read_quic(); + int write_quic(); + int write_udp(const sockaddr *addr, socklen_t addrlen, const uint8_t *data, + size_t datalen); + void quic_close_connection(); + int quic_setup_initial_crypto(); + + int quic_client_initial(); + int quic_recv_crypto_data(ngtcp2_crypto_level crypto_level, + const uint8_t *data, size_t datalen); + int quic_handshake_completed(); + int quic_in_encrypt(uint8_t *dest, size_t destlen, const uint8_t *plaintext, + size_t plaintextlen, const uint8_t *key, size_t keylen, + const uint8_t *nonce, size_t noncelen, const uint8_t *ad, + size_t adlen); + int quic_in_decrypt(uint8_t *dest, size_t destlen, const uint8_t *ciphertext, + size_t ciphertextlen, const uint8_t *key, size_t keylen, + const uint8_t *nonce, size_t noncelen, const uint8_t *ad, + size_t adlen); + int quic_encrypt(uint8_t *dest, size_t destlen, const uint8_t *plaintext, + size_t plaintextlen, const uint8_t *key, size_t keylen, + const uint8_t *nonce, size_t noncelen, const uint8_t *ad, + size_t adlen); + int quic_decrypt(uint8_t *dest, size_t destlen, const uint8_t *ciphertext, + size_t ciphertextlen, const uint8_t *key, size_t keylen, + const uint8_t *nonce, size_t noncelen, const uint8_t *ad, + size_t adlen); + int quic_in_hp_mask(uint8_t *dest, size_t destlen, const uint8_t *key, + size_t keylen, const uint8_t *sample, size_t samplelen); + int quic_hp_mask(uint8_t *dest, size_t destlen, const uint8_t *key, + size_t keylen, const uint8_t *sample, size_t samplelen); + int quic_recv_stream_data(int64_t stream_id, int fin, const uint8_t *data, + size_t datalen); + int quic_stream_close(int64_t stream_id, uint16_t app_error_code); + int quic_stream_reset(int64_t stream_id, uint16_t app_error_code); + int quic_extend_max_local_streams(); + + int quic_tls_handshake(bool initial = false); + int quic_read_tls(); + + int quic_on_key(int name, const uint8_t *secret, size_t secretlen); + void quic_set_tls_alert(uint8_t alert); + + size_t quic_read_server_handshake(uint8_t *buf, size_t buflen); + int quic_write_server_handshake(ngtcp2_crypto_level crypto_level, + const uint8_t *data, size_t datalen); + void quic_write_client_handshake(const uint8_t *data, size_t datalen); + void quic_write_client_handshake(Crypto &crypto, const uint8_t *data, + size_t datalen); + int quic_pkt_timeout(); + void quic_restart_pkt_timer(); }; } // namespace h2load diff --git a/src/h2load_http3_session.cc b/src/h2load_http3_session.cc new file mode 100644 index 00000000..99d57891 --- /dev/null +++ b/src/h2load_http3_session.cc @@ -0,0 +1,375 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2019 nghttp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "h2load_http3_session.h" + +#include + +#include + +#include "h2load.h" + +namespace h2load { + +Http3Session::Http3Session(Client *client) + : client_(client), conn_(nullptr), npending_request_(0), reqidx_(0) {} + +Http3Session::~Http3Session() { nghttp3_conn_del(conn_); } + +void Http3Session::on_connect() {} + +int Http3Session::submit_request() { + if (npending_request_) { + ++npending_request_; + return 0; + } + + auto config = client_->worker->config; + reqidx_ = client_->reqidx; + + if (++client_->reqidx == config->nva.size()) { + client_->reqidx = 0; + } + + auto stream_id = submit_request_internal(); + if (stream_id < 0) { + if (stream_id == NGTCP2_ERR_STREAM_ID_BLOCKED) { + ++npending_request_; + return 0; + } + return -1; + } + + return 0; +} + +int64_t Http3Session::submit_request_internal() { + int rv; + int64_t stream_id; + + auto config = client_->worker->config; + auto &nva = config->nva[reqidx_]; + + rv = ngtcp2_conn_open_bidi_stream(client_->quic.conn, &stream_id, nullptr); + if (rv != 0) { + return rv; + } + + rv = nghttp3_conn_submit_request(conn_, stream_id, nullptr, + reinterpret_cast(nva.data()), + nva.size(), nullptr, nullptr); + if (rv != 0) { + return rv; + } + + rv = nghttp3_conn_end_stream(conn_, stream_id); + assert(0 == rv); + + client_->on_request(stream_id); + auto req_stat = client_->get_req_stat(stream_id); + assert(req_stat); + client_->record_request_time(req_stat); + + return stream_id; +} + +int Http3Session::on_read(const uint8_t *data, size_t len) { return -1; } + +int Http3Session::on_write() { return -1; } + +void Http3Session::terminate() {} + +size_t Http3Session::max_concurrent_streams() { + return (size_t)client_->worker->config->max_concurrent_streams; +} + +namespace { +int stream_close(nghttp3_conn *conn, int64_t stream_id, uint16_t error_code, + void *user_data, void *stream_user_data) { + auto s = static_cast(user_data); + if (s->stream_close(stream_id, error_code) != 0) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +int Http3Session::stream_close(int64_t stream_id, uint16_t error_code) { + client_->on_stream_close(stream_id, error_code == NGHTTP3_HTTP_NO_ERROR); + return 0; +} + +namespace { +int recv_data(nghttp3_conn *conn, int64_t stream_id, const uint8_t *data, + size_t datalen, void *user_data, void *stream_user_data) { + auto s = static_cast(user_data); + s->recv_data(stream_id, data, datalen); + return 0; +} +} // namespace + +void Http3Session::recv_data(int64_t stream_id, const uint8_t *data, + size_t datalen) { + client_->record_ttfb(); + client_->worker->stats.bytes_body += datalen; + consume(stream_id, datalen); +} + +namespace { +int deferred_consume(nghttp3_conn *conn, int64_t stream_id, size_t nconsumed, + void *user_data, void *stream_user_data) { + auto s = static_cast(user_data); + s->consume(stream_id, nconsumed); + return 0; +} +} // namespace + +void Http3Session::consume(int64_t stream_id, size_t nconsumed) { + ngtcp2_conn_extend_max_stream_offset(client_->quic.conn, stream_id, + nconsumed); + ngtcp2_conn_extend_max_offset(client_->quic.conn, nconsumed); +} + +namespace { +int begin_headers(nghttp3_conn *conn, int64_t stream_id, void *user_data, + void *stream_user_data) { + auto s = static_cast(user_data); + s->begin_headers(stream_id); + return 0; +} +} // namespace + +void Http3Session::begin_headers(int64_t stream_id) { + auto payloadlen = nghttp3_conn_get_frame_payload_left(conn_, stream_id); + assert(payloadlen > 0); + + client_->worker->stats.bytes_head += payloadlen; +} + +namespace { +int recv_header(nghttp3_conn *conn, int64_t stream_id, int32_t token, + nghttp3_rcbuf *name, nghttp3_rcbuf *value, uint8_t flags, + void *user_data, void *stream_user_data) { + auto s = static_cast(user_data); + auto k = nghttp3_rcbuf_get_buf(name); + auto v = nghttp3_rcbuf_get_buf(value); + s->recv_header(stream_id, &k, &v); + return 0; +} +} // namespace + +void Http3Session::recv_header(int64_t stream_id, const nghttp3_vec *name, + const nghttp3_vec *value) { + client_->on_header(stream_id, name->base, name->len, value->base, value->len); + client_->worker->stats.bytes_head_decomp += name->len + value->len; +} + +namespace { +int send_stop_sending(nghttp3_conn *conn, int64_t stream_id, void *user_data, + void *stream_user_data) { + auto s = static_cast(user_data); + if (s->send_stop_sending(stream_id) != 0) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +int Http3Session::send_stop_sending(int64_t stream_id) { + auto rv = ngtcp2_conn_shutdown_stream_read(client_->quic.conn, stream_id, + NGHTTP3_HTTP_PUSH_REFUSED); + if (rv != 0) { + std::cerr << "ngtcp2_conn_shutdown_stream_read: " << ngtcp2_strerror(rv) + << std::endl; + return -1; + } + return 0; +} + +int Http3Session::close_stream(int64_t stream_id, uint16_t error_code) { + auto rv = nghttp3_conn_close_stream(conn_, stream_id, error_code); + if (rv != 0) { + return -1; + } + return 0; +} + +int Http3Session::reset_stream(int64_t stream_id) { + auto rv = nghttp3_conn_reset_stream(conn_, stream_id); + if (rv != 0) { + return -1; + } + return 0; +} + +int Http3Session::extend_max_local_streams() { + auto config = client_->worker->config; + + for (; npending_request_; --npending_request_) { + auto stream_id = submit_request_internal(); + if (stream_id < 0) { + if (stream_id == NGTCP2_ERR_STREAM_ID_BLOCKED) { + return 0; + } + return -1; + } + + if (++reqidx_ == config->nva.size()) { + reqidx_ = 0; + } + } + + return 0; +} + +int Http3Session::init_conn() { + int rv; + + assert(conn_ == nullptr); + + if (ngtcp2_conn_get_max_local_streams_uni(client_->quic.conn) < 3) { + return -1; + } + + nghttp3_conn_callbacks callbacks{ + nullptr, // acked_stream_data + h2load::stream_close, + h2load::recv_data, + h2load::deferred_consume, + h2load::begin_headers, + h2load::recv_header, + nullptr, // end_headers + nullptr, // begin_trailers + h2load::recv_header, + nullptr, // end_trailers + nullptr, // http_begin_push_promise + nullptr, // http_recv_push_promise + nullptr, // http_end_push_promise + nullptr, // http_cancel_push + h2load::send_stop_sending, + nullptr, // push_stream, + }; + + nghttp3_conn_settings settings; + nghttp3_conn_settings_default(&settings); + settings.qpack_max_table_capacity = 4096; + settings.qpack_blocked_streams = 100; + + auto mem = nghttp3_mem_default(); + + rv = nghttp3_conn_client_new(&conn_, &callbacks, &settings, mem, this); + if (rv != 0) { + std::cerr << "nghttp3_conn_client_new: " << nghttp3_strerror(rv) + << std::endl; + return -1; + } + + int64_t ctrl_stream_id; + + rv = ngtcp2_conn_open_uni_stream(client_->quic.conn, &ctrl_stream_id, NULL); + if (rv != 0) { + std::cerr << "ngtcp2_conn_open_uni_stream: " << ngtcp2_strerror(rv) + << std::endl; + return -1; + } + + rv = nghttp3_conn_bind_control_stream(conn_, ctrl_stream_id); + if (rv != 0) { + std::cerr << "nghttp3_conn_bind_control_stream: " << nghttp3_strerror(rv) + << std::endl; + return -1; + } + + int64_t qpack_enc_stream_id, qpack_dec_stream_id; + + rv = ngtcp2_conn_open_uni_stream(client_->quic.conn, &qpack_enc_stream_id, + NULL); + if (rv != 0) { + std::cerr << "ngtcp2_conn_open_uni_stream: " << ngtcp2_strerror(rv) + << std::endl; + return -1; + } + + rv = ngtcp2_conn_open_uni_stream(client_->quic.conn, &qpack_dec_stream_id, + NULL); + if (rv != 0) { + std::cerr << "ngtcp2_conn_open_uni_stream: " << ngtcp2_strerror(rv) + << std::endl; + return -1; + } + + rv = nghttp3_conn_bind_qpack_streams(conn_, qpack_enc_stream_id, + qpack_dec_stream_id); + if (rv != 0) { + std::cerr << "nghttp3_conn_bind_qpack_streams: " << nghttp3_strerror(rv) + << std::endl; + return -1; + } + + return 0; +} + +ssize_t Http3Session::read_stream(int64_t stream_id, const uint8_t *data, + size_t datalen, int fin) { + auto nconsumed = + nghttp3_conn_read_stream(conn_, stream_id, data, datalen, fin); + if (nconsumed < 0) { + std::cerr << "nghttp3_conn_read_stream: " << nghttp3_strerror(nconsumed) + << std::endl; + client_->quic.last_error = quic::err_application(nconsumed); + return -1; + } + return nconsumed; +} + +ssize_t Http3Session::write_stream(int64_t &stream_id, int &fin, + nghttp3_vec *vec, size_t veccnt) { + auto sveccnt = + nghttp3_conn_writev_stream(conn_, &stream_id, &fin, vec, veccnt); + if (sveccnt < 0) { + client_->quic.last_error = quic::err_application(sveccnt); + return -1; + } + return sveccnt; +} + +int Http3Session::block_stream(int64_t stream_id) { + auto rv = nghttp3_conn_block_stream(conn_, stream_id); + if (rv != 0) { + client_->quic.last_error = quic::err_application(rv); + return -1; + } + return 0; +} + +int Http3Session::add_write_offset(int64_t stream_id, size_t ndatalen) { + auto rv = nghttp3_conn_add_write_offset(conn_, stream_id, ndatalen); + if (rv != 0) { + client_->quic.last_error = quic::err_application(rv); + return -1; + } + return 0; +} + +} // namespace h2load diff --git a/src/h2load_http3_session.h b/src/h2load_http3_session.h new file mode 100644 index 00000000..4293c77d --- /dev/null +++ b/src/h2load_http3_session.h @@ -0,0 +1,77 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2019 nghttp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef H2LOAD_HTTP3_SESSION_H +#define H2LOAD_HTTP3_SESSION_H + +#include "h2load_session.h" + +#include + +namespace h2load { + +struct Client; + +class Http3Session : public Session { +public: + Http3Session(Client *client); + virtual ~Http3Session(); + virtual void on_connect(); + virtual int submit_request(); + virtual int on_read(const uint8_t *data, size_t len); + virtual int on_write(); + virtual void terminate(); + virtual size_t max_concurrent_streams(); + + int init_conn(); + int stream_close(int64_t stream_id, uint16_t error_code); + void recv_data(int64_t stream_id, const uint8_t *data, size_t datalen); + void consume(int64_t stream_id, size_t nconsumed); + void begin_headers(int64_t stream_id); + void recv_header(int64_t stream_id, const nghttp3_vec *name, + const nghttp3_vec *value); + int send_stop_sending(int64_t stream_id); + + int close_stream(int64_t stream_id, uint16_t error_code); + int reset_stream(int64_t stream_id); + int extend_max_local_streams(); + int64_t submit_request_internal(); + + ssize_t read_stream(int64_t stream_id, const uint8_t *data, size_t datalen, + int fin); + ssize_t write_stream(int64_t &stream_id, int &fin, nghttp3_vec *vec, + size_t veccnt); + int block_stream(int64_t stream_id); + int add_write_offset(int64_t stream_id, size_t ndatalen); + +private: + Client *client_; + nghttp3_conn *conn_; + size_t npending_request_; + size_t reqidx_; +}; + +} // namespace h2load + +#endif // H2LOAD_HTTP3_SESSION_H diff --git a/src/h2load_quic.cc b/src/h2load_quic.cc new file mode 100644 index 00000000..a8158400 --- /dev/null +++ b/src/h2load_quic.cc @@ -0,0 +1,1078 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2019 nghttp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "h2load_quic.h" + +#include + +#include +#include + +#include "h2load_http3_session.h" + +namespace h2load { + +namespace { +auto randgen = util::make_mt19937(); +} // namespace + +namespace { +int client_initial(ngtcp2_conn *conn, void *user_data) { + auto c = static_cast(user_data); + + if (c->quic_client_initial() != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +int Client::quic_client_initial() { + if (quic_tls_handshake(true) != 0) { + return -1; + } + return 0; +} + +namespace { +int recv_crypto_data(ngtcp2_conn *conn, ngtcp2_crypto_level crypto_level, + uint64_t offset, const uint8_t *data, size_t datalen, + void *user_data) { + auto c = static_cast(user_data); + + if (c->quic_recv_crypto_data(crypto_level, data, datalen) != 0) { + return NGTCP2_ERR_CRYPTO; + } + + return 0; +} +} // namespace + +int Client::quic_recv_crypto_data(ngtcp2_crypto_level crypto_level, + const uint8_t *data, size_t datalen) { + if (quic_write_server_handshake(crypto_level, data, datalen) != 0) { + return -1; + } + + if (!ngtcp2_conn_get_handshake_completed(quic.conn)) { + if (quic_tls_handshake() != 0) { + return -1; + } + return 0; + } + + // SSL_do_handshake() might not consume all data (e.g., + // NewSessionTicket). + return quic_read_tls(); +} + +namespace { +int handshake_completed(ngtcp2_conn *conn, void *user_data) { + auto c = static_cast(user_data); + + if (c->quic_handshake_completed() != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +int Client::quic_handshake_completed() { + quic.tx_crypto_level = NGTCP2_CRYPTO_LEVEL_APP; + + // TODO Create Http3Session here. + return connection_made(); +} + +namespace { +ssize_t in_encrypt(ngtcp2_conn *conn, uint8_t *dest, size_t destlen, + const uint8_t *plaintext, size_t plaintextlen, + const uint8_t *key, size_t keylen, const uint8_t *nonce, + size_t noncelen, const uint8_t *ad, size_t adlen, + void *user_data) { + auto c = static_cast(user_data); + + auto nwrite = c->quic_in_encrypt(dest, destlen, plaintext, plaintextlen, key, + keylen, nonce, noncelen, ad, adlen); + if (nwrite < 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return nwrite; +} +} // namespace + +int Client::quic_in_encrypt(uint8_t *dest, size_t destlen, + const uint8_t *plaintext, size_t plaintextlen, + const uint8_t *key, size_t keylen, + const uint8_t *nonce, size_t noncelen, + const uint8_t *ad, size_t adlen) { + return quic::encrypt(dest, destlen, plaintext, plaintextlen, key, keylen, + nonce, noncelen, ad, adlen, EVP_aes_128_gcm()); +} + +namespace { +ssize_t in_decrypt(ngtcp2_conn *conn, uint8_t *dest, size_t destlen, + const uint8_t *ciphertext, size_t ciphertextlen, + const uint8_t *key, size_t keylen, const uint8_t *nonce, + size_t noncelen, const uint8_t *ad, size_t adlen, + void *user_data) { + auto c = static_cast(user_data); + + auto nwrite = c->quic_in_decrypt(dest, destlen, ciphertext, ciphertextlen, + key, keylen, nonce, noncelen, ad, adlen); + if (nwrite < 0) { + return NGTCP2_ERR_TLS_DECRYPT; + } + + return nwrite; +} +} // namespace + +int Client::quic_in_decrypt(uint8_t *dest, size_t destlen, + const uint8_t *ciphertext, size_t ciphertextlen, + const uint8_t *key, size_t keylen, + const uint8_t *nonce, size_t noncelen, + const uint8_t *ad, size_t adlen) { + return quic::decrypt(dest, destlen, ciphertext, ciphertextlen, key, keylen, + nonce, noncelen, ad, adlen, EVP_aes_128_gcm()); +} + +namespace { +ssize_t encrypt(ngtcp2_conn *conn, uint8_t *dest, size_t destlen, + const uint8_t *plaintext, size_t plaintextlen, + const uint8_t *key, size_t keylen, const uint8_t *nonce, + size_t noncelen, const uint8_t *ad, size_t adlen, + void *user_data) { + auto c = static_cast(user_data); + + auto nwrite = c->quic_encrypt(dest, destlen, plaintext, plaintextlen, key, + keylen, nonce, noncelen, ad, adlen); + if (nwrite < 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return nwrite; +} +} // namespace + +int Client::quic_encrypt(uint8_t *dest, size_t destlen, + const uint8_t *plaintext, size_t plaintextlen, + const uint8_t *key, size_t keylen, + const uint8_t *nonce, size_t noncelen, + const uint8_t *ad, size_t adlen) { + return quic::encrypt(dest, destlen, plaintext, plaintextlen, key, keylen, + nonce, noncelen, ad, adlen, quic::aead(ssl)); +} + +namespace { +ssize_t decrypt(ngtcp2_conn *conn, uint8_t *dest, size_t destlen, + const uint8_t *ciphertext, size_t ciphertextlen, + const uint8_t *key, size_t keylen, const uint8_t *nonce, + size_t noncelen, const uint8_t *ad, size_t adlen, + void *user_data) { + auto c = static_cast(user_data); + + auto nwrite = c->quic_decrypt(dest, destlen, ciphertext, ciphertextlen, key, + keylen, nonce, noncelen, ad, adlen); + if (nwrite < 0) { + return NGTCP2_ERR_TLS_DECRYPT; + } + + return nwrite; +} +} // namespace + +int Client::quic_decrypt(uint8_t *dest, size_t destlen, + const uint8_t *ciphertext, size_t ciphertextlen, + const uint8_t *key, size_t keylen, + const uint8_t *nonce, size_t noncelen, + const uint8_t *ad, size_t adlen) { + return quic::decrypt(dest, destlen, ciphertext, ciphertextlen, key, keylen, + nonce, noncelen, ad, adlen, quic::aead(ssl)); +} + +namespace { +ssize_t in_hp_mask(ngtcp2_conn *conn, uint8_t *dest, size_t destlen, + const uint8_t *key, size_t keylen, const uint8_t *sample, + size_t samplelen, void *user_data) { + auto c = static_cast(user_data); + + auto nwrite = + c->quic_in_hp_mask(dest, destlen, key, keylen, sample, samplelen); + if (nwrite < 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return nwrite; +} +} // namespace + +int Client::quic_in_hp_mask(uint8_t *dest, size_t destlen, const uint8_t *key, + size_t keylen, const uint8_t *sample, + size_t samplelen) { + return quic::hp_mask(dest, destlen, key, keylen, sample, samplelen, + EVP_aes_128_ctr()); +} + +namespace { +ssize_t hp_mask(ngtcp2_conn *conn, uint8_t *dest, size_t destlen, + const uint8_t *key, size_t keylen, const uint8_t *sample, + size_t samplelen, void *user_data) { + auto c = static_cast(user_data); + + auto nwrite = c->quic_hp_mask(dest, destlen, key, keylen, sample, samplelen); + if (nwrite < 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return nwrite; +} +} // namespace + +int Client::quic_hp_mask(uint8_t *dest, size_t destlen, const uint8_t *key, + size_t keylen, const uint8_t *sample, + size_t samplelen) { + return quic::hp_mask(dest, destlen, key, keylen, sample, samplelen, + quic::hp(ssl)); +} + +namespace { +int recv_stream_data(ngtcp2_conn *conn, int64_t stream_id, int fin, + uint64_t offset, const uint8_t *data, size_t datalen, + void *user_data, void *stream_user_data) { + auto c = static_cast(user_data); + if (c->quic_recv_stream_data(stream_id, fin, data, datalen) != 0) { + // TODO Better to do this gracefully rather than + // NGTCP2_ERR_CALLBACK_FAILURE. Perhaps, call + // ngtcp2_conn_write_application_close() ? + return -1; + } + return 0; +} +} // namespace + +int Client::quic_recv_stream_data(int64_t stream_id, int fin, + const uint8_t *data, size_t datalen) { + auto s = static_cast(session.get()); + auto nconsumed = s->read_stream(stream_id, data, datalen, fin); + if (nconsumed == -1) { + return -1; + } + + ngtcp2_conn_extend_max_stream_offset(quic.conn, stream_id, nconsumed); + ngtcp2_conn_extend_max_offset(quic.conn, nconsumed); + + return 0; +} + +namespace { +int stream_close(ngtcp2_conn *conn, int64_t stream_id, uint16_t app_error_code, + void *user_data, void *stream_user_data) { + auto c = static_cast(user_data); + if (c->quic_stream_close(stream_id, app_error_code) != 0) { + return -1; + } + return 0; +} +} // namespace + +int Client::quic_stream_close(int64_t stream_id, uint16_t app_error_code) { + auto s = static_cast(session.get()); + if (s->close_stream(stream_id, app_error_code) != 0) { + return -1; + } + return 0; +} + +namespace { +int stream_reset(ngtcp2_conn *conn, int64_t stream_id, uint64_t final_size, + uint16_t app_error_code, void *user_data, + void *stream_user_data) { + auto c = static_cast(user_data); + if (c->quic_stream_reset(stream_id, app_error_code) != 0) { + return -1; + } + return 0; +} +} // namespace + +int Client::quic_stream_reset(int64_t stream_id, uint16_t app_error_code) { + auto s = static_cast(session.get()); + if (s->reset_stream(stream_id) != 0) { + return -1; + } + return 0; +} + +namespace { +int extend_max_local_streams_bidi(ngtcp2_conn *conn, uint64_t max_streams, + void *user_data) { + auto c = static_cast(user_data); + + if (c->quic_extend_max_local_streams() != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +int Client::quic_extend_max_local_streams() { + auto s = static_cast(session.get()); + if (s->extend_max_local_streams() != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + return 0; +} + +namespace { +int get_new_connection_id(ngtcp2_conn *conn, ngtcp2_cid *cid, uint8_t *token, + size_t cidlen, void *user_data) { + auto dis = std::uniform_int_distribution( + 0, std::numeric_limits::max()); + auto f = [&dis]() { return dis(randgen); }; + + std::generate_n(cid->data, cidlen, f); + cid->datalen = cidlen; + std::generate_n(token, NGTCP2_STATELESS_RESET_TOKENLEN, f); + + return 0; +} +} // namespace + +namespace { +void debug_log_printf(void *user_data, const char *fmt, ...) { + va_list ap; + + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); +} +} // namespace + +namespace { +void generate_cid(ngtcp2_cid &dest) { + auto dis = std::uniform_int_distribution( + 0, std::numeric_limits::max()); + dest.datalen = 8; + std::generate_n(dest.data, dest.datalen, [&dis]() { return dis(randgen); }); +} +} // namespace + +namespace { +ngtcp2_tstamp timestamp(struct ev_loop *loop) { + return ev_now(loop) * NGTCP2_SECONDS; +} +} // namespace + +namespace { +int bio_write(BIO *b, const char *buf, int len) { + assert(0); + return -1; +} +} // namespace + +namespace { +int bio_read(BIO *b, char *buf, int len) { + BIO_clear_retry_flags(b); + + auto c = static_cast(BIO_get_data(b)); + + len = c->quic_read_server_handshake(reinterpret_cast(buf), len); + if (len == 0) { + BIO_set_retry_read(b); + return -1; + } + + return len; +} +} // namespace + +namespace { +int bio_puts(BIO *b, const char *str) { return bio_write(b, str, strlen(str)); } +} // namespace + +namespace { +int bio_gets(BIO *b, char *buf, int len) { return -1; } +} // namespace + +namespace { +long bio_ctrl(BIO *b, int cmd, long num, void *ptr) { + switch (cmd) { + case BIO_CTRL_FLUSH: + return 1; + } + + return 0; +} +} // namespace + +namespace { +int bio_create(BIO *b) { + BIO_set_init(b, 1); + return 1; +} +} // namespace + +namespace { +int bio_destroy(BIO *b) { + if (b == nullptr) { + return 0; + } + + return 1; +} +} // namespace + +namespace { +BIO_METHOD *create_bio_method() { + static auto meth = BIO_meth_new(BIO_TYPE_FD, "bio"); + BIO_meth_set_write(meth, bio_write); + BIO_meth_set_read(meth, bio_read); + BIO_meth_set_puts(meth, bio_puts); + BIO_meth_set_gets(meth, bio_gets); + BIO_meth_set_ctrl(meth, bio_ctrl); + BIO_meth_set_create(meth, bio_create); + BIO_meth_set_destroy(meth, bio_destroy); + return meth; +} +} // namespace + +namespace { +int key_cb(SSL *ssl, int name, const unsigned char *secret, size_t secretlen, + void *arg) { + auto c = static_cast(arg); + + if (c->quic_on_key(name, secret, secretlen) != 0) { + return 0; + } + + return 1; +} +} // namespace + +namespace { +void msg_cb(int write_p, int version, int content_type, const void *buf, + size_t len, SSL *ssl, void *arg) { + if (!write_p) { + return; + } + + auto c = static_cast(arg); + auto msg = reinterpret_cast(buf); + + switch (content_type) { + case SSL3_RT_HANDSHAKE: + break; + case SSL3_RT_ALERT: + assert(len == 2); + if (msg[0] != 2 /* FATAL */) { + return; + } + c->quic_set_tls_alert(msg[1]); + return; + default: + return; + } + + c->quic_write_client_handshake(reinterpret_cast(buf), len); +} +} // namespace + +int Client::quic_init(const sockaddr *local_addr, socklen_t local_addrlen, + const sockaddr *remote_addr, socklen_t remote_addrlen) { + int rv; + + if (!ssl) { + ssl = SSL_new(worker->ssl_ctx); + + auto bio = BIO_new(create_bio_method()); + BIO_set_data(bio, this); + + SSL_set_bio(ssl, bio, bio); + SSL_set_app_data(ssl, this); + SSL_set_connect_state(ssl); + SSL_set_msg_callback(ssl, msg_cb); + SSL_set_msg_callback_arg(ssl, this); + SSL_set_key_callback(ssl, key_cb, this); + } + + switch (remote_addr->sa_family) { + case AF_INET: + quic.max_pktlen = NGTCP2_MAX_PKTLEN_IPV4; + break; + case AF_INET6: + quic.max_pktlen = NGTCP2_MAX_PKTLEN_IPV6; + break; + default: + return -1; + } + + auto callbacks = ngtcp2_conn_callbacks{ + h2load::client_initial, + nullptr, // recv_client_initial + h2load::recv_crypto_data, + h2load::handshake_completed, + nullptr, // recv_version_negotiation + h2load::in_encrypt, + h2load::in_decrypt, + h2load::encrypt, + h2load::decrypt, + h2load::in_hp_mask, + h2load::hp_mask, + h2load::recv_stream_data, + nullptr, // acked_crypto_offset + nullptr, // acked_stream_data_offset + nullptr, // stream_open + h2load::stream_close, + nullptr, // recv_stateless_reset + nullptr, // recv_retry + h2load::extend_max_local_streams_bidi, + nullptr, // extend_max_local_streams_uni + nullptr, // rand + get_new_connection_id, + nullptr, // remove_connection_id + nullptr, // update_key + nullptr, // path_validation + nullptr, // select_preferred_address + h2load::stream_reset, + nullptr, // extend_max_remote_streams_bidi + nullptr, // extend_max_remote_streams_uni + nullptr, // extend_max_stream_data + }; + + ngtcp2_cid scid, dcid; + generate_cid(scid); + generate_cid(dcid); + + ngtcp2_settings settings; + ngtcp2_settings_default(&settings); + settings.log_printf = debug_log_printf; + settings.log_printf = nullptr; + settings.initial_ts = timestamp(worker->loop); + settings.max_stream_data_bidi_local = 256_k; + settings.max_stream_data_bidi_remote = 256_k; + settings.max_stream_data_uni = 256_k; + settings.max_data = 1_m; + settings.max_streams_bidi = 1; + settings.max_streams_uni = 100; + settings.idle_timeout = 30 * NGTCP2_SECONDS; + + auto path = ngtcp2_path{ + {local_addrlen, + const_cast(reinterpret_cast(local_addr))}, + {remote_addrlen, + const_cast(reinterpret_cast(remote_addr))}, + }; + + rv = ngtcp2_conn_client_new(&quic.conn, &dcid, &scid, &path, NGTCP2_PROTO_VER, + &callbacks, &settings, nullptr, this); + if (rv != 0) { + return -1; + } + + rv = quic_setup_initial_crypto(); + if (rv != 0) { + ngtcp2_conn_del(quic.conn); + quic.conn = nullptr; + return -1; + } + + return 0; +} + +void Client::quic_free() { ngtcp2_conn_del(quic.conn); } + +void Client::quic_close_connection() { + if (!quic.conn) { + return; + } + + std::array buf; + ssize_t nwrite; + ngtcp2_path_storage ps; + ngtcp2_path_storage_zero(&ps); + + switch (quic.last_error.type) { + case quic::ErrorType::TransportVersionNegotiation: + return; + case quic::ErrorType::Transport: + nwrite = ngtcp2_conn_write_connection_close( + quic.conn, &ps.path, buf.data(), quic.max_pktlen, quic.last_error.code, + timestamp(worker->loop)); + break; + case quic::ErrorType::Application: + nwrite = ngtcp2_conn_write_application_close( + quic.conn, &ps.path, buf.data(), quic.max_pktlen, quic.last_error.code, + timestamp(worker->loop)); + break; + default: + assert(0); + } + + if (nwrite < 0) { + return; + } + + write_udp(reinterpret_cast(ps.path.remote.addr), + ps.path.remote.addrlen, buf.data(), nwrite); +} + +int Client::quic_setup_initial_crypto() { + int rv; + + std::array initial_secret, secret; + auto dcid = ngtcp2_conn_get_dcid(quic.conn); + rv = quic::derive_initial_secret( + initial_secret.data(), initial_secret.size(), dcid->data, dcid->datalen, + reinterpret_cast(NGTCP2_INITIAL_SALT), + str_size(NGTCP2_INITIAL_SALT)); + if (rv != 0) { + std::cerr << "quic::derive_initial_secret() failed" << std::endl; + return -1; + } + + auto aead = EVP_aes_128_gcm(); + auto prf = EVP_sha256(); + + rv = quic::derive_client_initial_secret(secret.data(), secret.size(), + initial_secret.data(), + initial_secret.size()); + if (rv != 0) { + std::cerr << "quic::derive_client_initial_secret() failed" << std::endl; + return -1; + } + + std::array key, iv, hp; + auto keylen = key.size(); + auto ivlen = iv.size(); + rv = quic::derive_packet_protection_key(key.data(), keylen, iv.data(), ivlen, + secret.data(), secret.size(), aead, + prf); + if (rv != 0) { + return -1; + } + + auto hplen = hp.size(); + rv = quic::derive_header_protection_key(hp.data(), hplen, secret.data(), + secret.size(), aead, prf); + if (rv != 0) { + return -1; + } + + ngtcp2_conn_install_initial_tx_keys(quic.conn, key.data(), keylen, iv.data(), + ivlen, hp.data(), hplen); + + rv = quic::derive_server_initial_secret(secret.data(), secret.size(), + initial_secret.data(), + initial_secret.size()); + if (rv != 0) { + std::cerr << "quic::derive_server_initial_secret() failed" << std::endl; + return -1; + } + + keylen = key.size(); + ivlen = iv.size(); + rv = quic::derive_packet_protection_key(key.data(), keylen, iv.data(), ivlen, + secret.data(), secret.size(), aead, + prf); + if (rv != 0) { + return -1; + } + + hplen = hp.size(); + rv = quic::derive_header_protection_key(hp.data(), hplen, secret.data(), + secret.size(), aead, prf); + if (rv != 0) { + return -1; + } + + ngtcp2_conn_install_initial_rx_keys(quic.conn, key.data(), keylen, iv.data(), + ivlen, hp.data(), hplen); + + return 0; +} + +int Client::quic_on_key(int name, const uint8_t *secret, size_t secretlen) { + int rv; + + switch (name) { + case SSL_KEY_CLIENT_EARLY_TRAFFIC: + case SSL_KEY_CLIENT_HANDSHAKE_TRAFFIC: + case SSL_KEY_SERVER_HANDSHAKE_TRAFFIC: + case SSL_KEY_CLIENT_APPLICATION_TRAFFIC: + case SSL_KEY_SERVER_APPLICATION_TRAFFIC: + break; + default: + return 0; + } + + auto aead = quic::aead(ssl); + auto prf = quic::prf(ssl); + + std::array key, iv, hp; + auto keylen = key.size(); + auto ivlen = iv.size(); + rv = quic::derive_packet_protection_key(key.data(), keylen, iv.data(), ivlen, + secret, secretlen, aead, prf); + if (rv != 0) { + return -1; + } + + auto hplen = hp.size(); + rv = quic::derive_header_protection_key(hp.data(), hplen, secret, secretlen, + aead, prf); + if (rv != 0) { + return -1; + } + + // TODO Just call this once. + ngtcp2_conn_set_aead_overhead(quic.conn, quic::aead_max_overhead(aead)); + + switch (name) { + case SSL_KEY_CLIENT_EARLY_TRAFFIC: + ngtcp2_conn_install_early_keys(quic.conn, key.data(), keylen, iv.data(), + ivlen, hp.data(), hplen); + break; + case SSL_KEY_CLIENT_HANDSHAKE_TRAFFIC: + ngtcp2_conn_install_handshake_tx_keys(quic.conn, key.data(), keylen, + iv.data(), ivlen, hp.data(), hplen); + quic.tx_crypto_level = NGTCP2_CRYPTO_LEVEL_HANDSHAKE; + break; + case SSL_KEY_CLIENT_APPLICATION_TRAFFIC: + ngtcp2_conn_install_tx_keys(quic.conn, key.data(), keylen, iv.data(), ivlen, + hp.data(), hplen); + break; + case SSL_KEY_SERVER_HANDSHAKE_TRAFFIC: + ngtcp2_conn_install_handshake_rx_keys(quic.conn, key.data(), keylen, + iv.data(), ivlen, hp.data(), hplen); + quic.rx_crypto_level = NGTCP2_CRYPTO_LEVEL_HANDSHAKE; + break; + case SSL_KEY_SERVER_APPLICATION_TRAFFIC: + ngtcp2_conn_install_rx_keys(quic.conn, key.data(), keylen, iv.data(), ivlen, + hp.data(), hplen); + quic.rx_crypto_level = NGTCP2_CRYPTO_LEVEL_APP; + break; + } + + return 0; +} + +int Client::quic_tls_handshake(bool initial) { + ERR_clear_error(); + + int rv; + + rv = SSL_do_handshake(ssl); + if (rv <= 0) { + auto err = SSL_get_error(ssl, rv); + switch (err) { + case SSL_ERROR_WANT_READ: + case SSL_ERROR_WANT_WRITE: + return 0; + case SSL_ERROR_SSL: + std::cerr << "TLS handshake error: " + << ERR_error_string(ERR_get_error(), nullptr) << std::endl; + return -1; + default: + std::cerr << "TLS handshake error: " << err << std::endl; + return -1; + } + } + + ngtcp2_conn_handshake_completed(quic.conn); + + if (quic_read_tls() != 0) { + return -1; + } + + return 0; +} + +int Client::quic_read_tls() { + ERR_clear_error(); + + std::array buf; + size_t nread; + + for (;;) { + auto rv = SSL_read_ex(ssl, buf.data(), buf.size(), &nread); + if (rv == 1) { + continue; + } + auto err = SSL_get_error(ssl, 0); + switch (err) { + case SSL_ERROR_WANT_READ: + case SSL_ERROR_WANT_WRITE: + return 0; + case SSL_ERROR_SSL: + case SSL_ERROR_ZERO_RETURN: + std::cerr << "TLS read error: " + << ERR_error_string(ERR_get_error(), nullptr) << std::endl; + return NGTCP2_ERR_CRYPTO; + default: + std::cerr << "TLS read error: " << err << std::endl; + return NGTCP2_ERR_CRYPTO; + } + } +} + +void Client::quic_set_tls_alert(uint8_t alert) { + quic.last_error = quic::err_transport_tls(alert); +} + +size_t Client::quic_read_server_handshake(uint8_t *buf, size_t buflen) { + auto n = std::min(buflen, + quic.server_handshake.size() - quic.server_handshake_nread); + std::copy_n(std::begin(quic.server_handshake) + quic.server_handshake_nread, + n, buf); + quic.server_handshake_nread += n; + return n; +} + +int Client::quic_write_server_handshake(ngtcp2_crypto_level crypto_level, + const uint8_t *data, size_t datalen) { + if (quic.rx_crypto_level != crypto_level) { + std::cerr << "Got crypto level " + << ", want " << quic.rx_crypto_level << std::endl; + return -1; + } + std::copy_n(data, datalen, std::back_inserter(quic.server_handshake)); + return 0; +} + +void Client::quic_write_client_handshake(const uint8_t *data, size_t datalen) { + assert(quic.tx_crypto_level < 2); + quic_write_client_handshake(quic.crypto[quic.tx_crypto_level], data, datalen); +} + +void Client::quic_write_client_handshake(Crypto &crypto, const uint8_t *data, + size_t datalen) { + assert(crypto.data.size() >= crypto.datalen + datalen); + + auto p = std::begin(crypto.data) + crypto.datalen; + std::copy_n(data, datalen, p); + crypto.datalen += datalen; + + ngtcp2_conn_submit_crypto_data(quic.conn, quic.tx_crypto_level, p, datalen); +} + +void quic_pkt_timeout_cb(struct ev_loop *loop, ev_timer *w, int revents) { + auto c = static_cast(w->data); + + if (c->quic_pkt_timeout() != 0) { + c->fail(); + c->worker->free_client(c); + delete c; + return; + } +} + +int Client::quic_pkt_timeout() { + int rv; + auto now = timestamp(worker->loop); + auto should_write = false; + + if (ngtcp2_conn_loss_detection_expiry(quic.conn) <= now) { + rv = ngtcp2_conn_on_loss_detection_timer(quic.conn, now); + if (rv != 0) { + quic.last_error = quic::err_transport(NGTCP2_ERR_INTERNAL); + return -1; + } + should_write = true; + } + if (ngtcp2_conn_ack_delay_expiry(quic.conn) <= now) { + ngtcp2_conn_cancel_expired_ack_delay_timer(quic.conn, now); + should_write = true; + } + if (should_write) { + return write_quic(); + } + return 0; +} + +void Client::quic_restart_pkt_timer() { + auto expiry = ngtcp2_conn_get_expiry(quic.conn); + auto now = timestamp(worker->loop); + auto t = expiry < now ? 1e-9 + : static_cast(expiry - now) / NGTCP2_SECONDS; + quic.pkt_timer.repeat = t; + ev_timer_again(worker->loop, &quic.pkt_timer); +} + +int Client::read_quic() { + std::array buf; + sockaddr_union su; + socklen_t addrlen = sizeof(su); + int rv; + + auto nread = + recvfrom(fd, buf.data(), buf.size(), MSG_DONTWAIT, &su.sa, &addrlen); + if (nread == -1) { + return 0; + } + + assert(quic.conn); + + auto path = ngtcp2_path{ + {local_addr.len, reinterpret_cast(&local_addr.su.sa)}, + {addrlen, reinterpret_cast(&su.sa)}, + }; + + rv = ngtcp2_conn_read_pkt(quic.conn, &path, buf.data(), nread, + timestamp(worker->loop)); + if (rv != 0) { + std::cerr << "ngtcp2_conn_read_pkt: " << ngtcp2_strerror(rv) << std::endl; + return -1; + } + + if (worker->current_phase == Phase::MAIN_DURATION) { + worker->stats.bytes_total += nread; + } + + return 0; +} + +int Client::write_quic() { + if (quic.close_requested) { + return -1; + } + + auto s = static_cast(session.get()); + + std::array vec; + std::array buf; + ngtcp2_path_storage ps; + + ngtcp2_path_storage_zero(&ps); + + for (;;) { + int64_t stream_id; + int fin; + ssize_t sveccnt = 0; + + if (s && ngtcp2_conn_get_max_data_left(quic.conn)) { + sveccnt = s->write_stream(stream_id, fin, vec.data(), vec.size()); + if (sveccnt == -1) { + return -1; + } + } + + ssize_t ndatalen; + if (sveccnt == 0) { + auto nwrite = + ngtcp2_conn_write_pkt(quic.conn, &ps.path, buf.data(), + quic.max_pktlen, timestamp(worker->loop)); + if (nwrite < 0) { + quic.last_error = quic::err_transport(nwrite); + return -1; + } + + quic_restart_pkt_timer(); + + if (nwrite == 0) { + ev_io_stop(worker->loop, &wev); + return 0; + } + + write_udp(reinterpret_cast(ps.path.remote.addr), + ps.path.remote.addrlen, buf.data(), nwrite); + + ev_io_start(worker->loop, &wev); + + return 0; + } + + auto v = vec.data(); + auto vcnt = static_cast(sveccnt); + for (;;) { + auto nwrite = ngtcp2_conn_writev_stream( + quic.conn, &ps.path, buf.data(), quic.max_pktlen, &ndatalen, + NGTCP2_WRITE_STREAM_FLAG_MORE, stream_id, fin, + reinterpret_cast(v), vcnt, + timestamp(worker->loop)); + if (nwrite < 0) { + auto should_break = false; + switch (nwrite) { + case NGTCP2_ERR_STREAM_DATA_BLOCKED: + if (ngtcp2_conn_get_max_data_left(quic.conn) == 0) { + return 0; + } + + if (s->block_stream(stream_id) != 0) { + return -1; + } + should_break = true; + break; + case NGTCP2_ERR_EARLY_DATA_REJECTED: + case NGTCP2_ERR_STREAM_SHUT_WR: + case NGTCP2_ERR_STREAM_NOT_FOUND: // This means that stream is + // closed. + assert(0); + // TODO Perhaps, close stream or this should not happen? + break; + case NGTCP2_ERR_WRITE_STREAM_MORE: + assert(ndatalen > 0); + if (s->add_write_offset(stream_id, ndatalen) != 0) { + return -1; + } + should_break = true; + break; + } + + if (should_break) { + break; + } + + quic.last_error = quic::err_transport(nwrite); + return -1; + } + + quic_restart_pkt_timer(); + + if (nwrite == 0) { + ev_io_stop(worker->loop, &wev); + return 0; + } + + if (ndatalen > 0) { + if (s->add_write_offset(stream_id, ndatalen) != 0) { + return -1; + } + } + + write_udp(reinterpret_cast(ps.path.remote.addr), + ps.path.remote.addrlen, buf.data(), nwrite); + + ev_io_start(worker->loop, &wev); + + return 0; + } + } +} + +} // namespace h2load diff --git a/src/h2load_quic.h b/src/h2load_quic.h new file mode 100644 index 00000000..225f00fb --- /dev/null +++ b/src/h2load_quic.h @@ -0,0 +1,38 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2019 nghttp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef H2LOAD_QUIC_H +#define H2LOAD_QUIC_H + +#include "nghttp2_config.h" + +#include + +#include "h2load.h" + +namespace h2load { +void quic_pkt_timeout_cb(struct ev_loop *loop, ev_timer *w, int revents); +} // namespace h2load + +#endif // H2LOAD_QUIC_H diff --git a/src/quic.cc b/src/quic.cc new file mode 100644 index 00000000..c9ddd664 --- /dev/null +++ b/src/quic.cc @@ -0,0 +1,422 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2019 nghttp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "quic.h" + +#include + +#include + +#include +#include + +#include "template.h" + +using namespace nghttp2; + +namespace quic { + +const EVP_CIPHER *aead(SSL *ssl) { + switch (SSL_CIPHER_get_id(SSL_get_current_cipher(ssl))) { + case 0x03001301u: // TLS_AES_128_GCM_SHA256 + return EVP_aes_128_gcm(); + case 0x03001302u: // TLS_AES_256_GCM_SHA384 + return EVP_aes_256_gcm(); + case 0x03001303u: // TLS_CHACHA20_POLY1305_SHA256 + return EVP_chacha20_poly1305(); + default: + assert(0); + } +} + +const EVP_CIPHER *hp(SSL *ssl) { + switch (SSL_CIPHER_get_id(SSL_get_current_cipher(ssl))) { + case 0x03001301u: // TLS_AES_128_GCM_SHA256 + return EVP_aes_128_ctr(); + case 0x03001302u: // TLS_AES_256_GCM_SHA384 + return EVP_aes_256_ctr(); + case 0x03001303u: // TLS_CHACHA20_POLY1305_SHA256 + return EVP_chacha20(); + default: + assert(0); + } +} + +const EVP_MD *prf(SSL *ssl) { + switch (SSL_CIPHER_get_id(SSL_get_current_cipher(ssl))) { + case 0x03001301u: // TLS_AES_128_GCM_SHA256 + case 0x03001303u: // TLS_CHACHA20_POLY1305_SHA256 + return EVP_sha256(); + case 0x03001302u: // TLS_AES_256_GCM_SHA384 + return EVP_sha384(); + default: + assert(0); + } +} + +namespace { +size_t aead_key_length(const EVP_CIPHER *aead) { + return EVP_CIPHER_key_length(aead); +} +} // namespace + +namespace { +size_t aead_nonce_length(const EVP_CIPHER *aead) { + return EVP_CIPHER_iv_length(aead); +} +} // namespace + +namespace { +size_t aead_tag_length(const EVP_CIPHER *aead) { + if (aead == EVP_aes_128_gcm() || aead == EVP_aes_256_gcm()) { + return EVP_GCM_TLS_TAG_LEN; + } + if (aead == EVP_chacha20_poly1305()) { + return EVP_CHACHAPOLY_TLS_TAG_LEN; + } + assert(0); +} +} // namespace + +size_t aead_max_overhead(const EVP_CIPHER *aead) { + return aead_tag_length(aead); +} + +int hkdf_extract(uint8_t *dest, size_t destlen, const uint8_t *secret, + size_t secretlen, const uint8_t *salt, size_t saltlen, + const EVP_MD *prf) { + auto pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, nullptr); + if (pctx == nullptr) { + return -1; + } + + auto pctx_d = defer(EVP_PKEY_CTX_free, pctx); + + if (EVP_PKEY_derive_init(pctx) != 1 || + EVP_PKEY_CTX_hkdf_mode(pctx, EVP_PKEY_HKDEF_MODE_EXTRACT_ONLY) != 1 || + EVP_PKEY_CTX_set_hkdf_md(pctx, prf) != 1 || + EVP_PKEY_CTX_set1_hkdf_salt(pctx, salt, saltlen) != 1 || + EVP_PKEY_CTX_set1_hkdf_key(pctx, secret, secretlen) != 1 || + EVP_PKEY_derive(pctx, dest, &destlen) != 1) { + return -1; + } + + return 0; +} + +int hkdf_expand(uint8_t *dest, size_t destlen, const uint8_t *secret, + size_t secretlen, const uint8_t *info, size_t infolen, + const EVP_MD *prf) { + auto pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, nullptr); + if (pctx == nullptr) { + return -1; + } + + auto pctx_d = defer(EVP_PKEY_CTX_free, pctx); + + if (EVP_PKEY_derive_init(pctx) != 1 || + EVP_PKEY_CTX_hkdf_mode(pctx, EVP_PKEY_HKDEF_MODE_EXPAND_ONLY) != 1 || + EVP_PKEY_CTX_set_hkdf_md(pctx, prf) != 1 || + EVP_PKEY_CTX_set1_hkdf_salt(pctx, "", 0) != 1 || + EVP_PKEY_CTX_set1_hkdf_key(pctx, secret, secretlen) != 1 || + EVP_PKEY_CTX_add1_hkdf_info(pctx, info, infolen) != 1 || + EVP_PKEY_derive(pctx, dest, &destlen) != 1) { + return -1; + } + + return 0; +} + +int hkdf_expand_label(uint8_t *dest, size_t destlen, const uint8_t *secret, + size_t secretlen, const uint8_t *label, size_t labellen, + const EVP_MD *prf) { + std::array info; + static constexpr const uint8_t LABEL[] = "tls13 "; + + auto p = std::begin(info); + *p++ = destlen / 256; + *p++ = destlen % 256; + *p++ = str_size(LABEL) + labellen; + p = std::copy_n(LABEL, str_size(LABEL), p); + p = std::copy_n(label, labellen, p); + *p++ = 0; + + return hkdf_expand(dest, destlen, secret, secretlen, info.data(), + p - std::begin(info), prf); +} + +int derive_initial_secret(uint8_t *dest, size_t destlen, const uint8_t *secret, + size_t secretlen, const uint8_t *salt, + size_t saltlen) { + return hkdf_extract(dest, destlen, secret, secretlen, salt, saltlen, + EVP_sha256()); +} + +int derive_client_initial_secret(uint8_t *dest, size_t destlen, + const uint8_t *secret, size_t secretlen) { + static constexpr uint8_t LABEL[] = "client in"; + return hkdf_expand_label(dest, destlen, secret, secretlen, LABEL, + str_size(LABEL), EVP_sha256()); +} + +int derive_server_initial_secret(uint8_t *dest, size_t destlen, + const uint8_t *secret, size_t secretlen) { + static constexpr uint8_t LABEL[] = "server in"; + return hkdf_expand_label(dest, destlen, secret, secretlen, LABEL, + str_size(LABEL), EVP_sha256()); +} + +int derive_packet_protection_key(uint8_t *key, size_t &keylen, uint8_t *iv, + size_t &ivlen, const uint8_t *secret, + size_t secretlen, const EVP_CIPHER *aead, + const EVP_MD *prf) { + int rv; + static constexpr uint8_t KEY_LABEL[] = "quic key"; + static constexpr uint8_t IV_LABEL[] = "quic iv"; + + auto req_keylen = aead_key_length(aead); + if (req_keylen > keylen) { + return -1; + } + + keylen = req_keylen; + rv = hkdf_expand_label(key, keylen, secret, secretlen, KEY_LABEL, + str_size(KEY_LABEL), prf); + if (rv != 0) { + return -1; + } + + auto req_ivlen = std::max(static_cast(8), aead_nonce_length(aead)); + if (req_ivlen > ivlen) { + return -1; + } + + ivlen = req_ivlen; + rv = hkdf_expand_label(iv, ivlen, secret, secretlen, IV_LABEL, + str_size(IV_LABEL), prf); + if (rv != 0) { + return -1; + } + + return 0; +} + +int derive_header_protection_key(uint8_t *key, size_t &keylen, + const uint8_t *secret, size_t secretlen, + const EVP_CIPHER *aead, const EVP_MD *prf) { + int rv; + static constexpr uint8_t LABEL[] = "quic hp"; + + auto req_keylen = aead_key_length(aead); + if (req_keylen > keylen) { + return -1; + } + + keylen = req_keylen; + rv = hkdf_expand_label(key, keylen, secret, secretlen, LABEL, str_size(LABEL), + prf); + if (rv != 0) { + return -1; + } + + return 0; +} + +ssize_t encrypt(uint8_t *dest, size_t destlen, const uint8_t *plaintext, + size_t plaintextlen, const uint8_t *key, size_t keylen, + const uint8_t *nonce, size_t noncelen, const uint8_t *ad, + size_t adlen, const EVP_CIPHER *aead) { + auto taglen = aead_tag_length(aead); + + if (destlen < plaintextlen + taglen) { + return -1; + } + + auto actx = EVP_CIPHER_CTX_new(); + if (actx == nullptr) { + return -1; + } + + auto actx_d = defer(EVP_CIPHER_CTX_free, actx); + + if (EVP_EncryptInit_ex(actx, aead, nullptr, nullptr, nullptr) != 1) { + return -1; + } + + if (EVP_CIPHER_CTX_ctrl(actx, EVP_CTRL_AEAD_SET_IVLEN, noncelen, nullptr) != + 1) { + return -1; + } + + if (EVP_EncryptInit_ex(actx, nullptr, nullptr, key, nonce) != 1) { + return -1; + } + + size_t outlen = 0; + int len; + + if (EVP_EncryptUpdate(actx, nullptr, &len, ad, adlen) != 1) { + return -1; + } + + if (EVP_EncryptUpdate(actx, dest, &len, plaintext, plaintextlen) != 1) { + return -1; + } + + outlen = len; + + if (EVP_EncryptFinal_ex(actx, dest + outlen, &len) != 1) { + return -1; + } + + outlen += len; + + assert(outlen + taglen <= destlen); + + if (EVP_CIPHER_CTX_ctrl(actx, EVP_CTRL_AEAD_GET_TAG, taglen, dest + outlen) != + 1) { + return -1; + } + + outlen += taglen; + + return outlen; +} + +ssize_t decrypt(uint8_t *dest, size_t destlen, const uint8_t *ciphertext, + size_t ciphertextlen, const uint8_t *key, size_t keylen, + const uint8_t *nonce, size_t noncelen, const uint8_t *ad, + size_t adlen, const EVP_CIPHER *aead) { + auto taglen = aead_tag_length(aead); + + if (taglen > ciphertextlen || destlen + taglen < ciphertextlen) { + return -1; + } + + ciphertextlen -= taglen; + auto tag = ciphertext + ciphertextlen; + + auto actx = EVP_CIPHER_CTX_new(); + if (actx == nullptr) { + return -1; + } + + auto actx_d = defer(EVP_CIPHER_CTX_free, actx); + + if (EVP_DecryptInit_ex(actx, aead, nullptr, nullptr, nullptr) != 1) { + return -1; + } + + if (EVP_CIPHER_CTX_ctrl(actx, EVP_CTRL_AEAD_SET_IVLEN, noncelen, nullptr) != + 1) { + return -1; + } + + if (EVP_DecryptInit_ex(actx, nullptr, nullptr, key, nonce) != 1) { + return -1; + } + + size_t outlen; + int len; + + if (EVP_DecryptUpdate(actx, nullptr, &len, ad, adlen) != 1) { + return -1; + } + + if (EVP_DecryptUpdate(actx, dest, &len, ciphertext, ciphertextlen) != 1) { + return -1; + } + + outlen = len; + + if (EVP_CIPHER_CTX_ctrl(actx, EVP_CTRL_AEAD_SET_TAG, taglen, + const_cast(tag)) != 1) { + return -1; + } + + if (EVP_DecryptFinal_ex(actx, dest + outlen, &len) != 1) { + return -1; + } + + outlen += len; + + return outlen; +} + +ssize_t hp_mask(uint8_t *dest, size_t destlen, const uint8_t *key, + size_t keylen, const uint8_t *sample, size_t samplelen, + const EVP_CIPHER *cipher) { + static constexpr uint8_t PLAINTEXT[] = "\x00\x00\x00\x00\x00"; + + auto actx = EVP_CIPHER_CTX_new(); + if (actx == nullptr) { + return -1; + } + + auto actx_d = defer(EVP_CIPHER_CTX_free, actx); + + if (EVP_EncryptInit_ex(actx, cipher, nullptr, key, sample) != 1) { + return -1; + } + + size_t outlen = 0; + int len; + + if (EVP_EncryptUpdate(actx, dest, &len, PLAINTEXT, str_size(PLAINTEXT)) != + 1) { + return -1; + } + + assert(len == 5); + + outlen = len; + + if (EVP_EncryptFinal_ex(actx, dest + outlen, &len) != 1) { + return -1; + } + + assert(len == 0); + + return outlen; +} + +Error err_transport(int liberr) { + if (liberr == NGTCP2_ERR_RECV_VERSION_NEGOTIATION) { + return {ErrorType::TransportVersionNegotiation, 0}; + } + return {ErrorType::Transport, + ngtcp2_err_infer_quic_transport_error_code(liberr)}; +} + +Error err_transport_tls(int alert) { + return {ErrorType::Transport, ngtcp2_err_infer_quic_transport_error_code( + NGTCP2_CRYPTO_ERROR | alert)}; +} + +Error err_application(int liberr) { + return {ErrorType::Application, + nghttp3_err_infer_quic_app_error_code(liberr)}; +} + +} // namespace quic diff --git a/src/quic.h b/src/quic.h new file mode 100644 index 00000000..4b4bb1d6 --- /dev/null +++ b/src/quic.h @@ -0,0 +1,104 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2019 nghttp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef QUIC_H +#define QUIC_H + +#include "nghttp2_config.h" + +#include + +namespace quic { + +const EVP_CIPHER *aead(SSL *ssl); +const EVP_CIPHER *hp(SSL *ssl); +const EVP_MD *prf(SSL *ssl); +size_t aead_max_overhead(const EVP_CIPHER *aead); + +int hkdf_extract(uint8_t *dest, size_t destlen, const uint8_t *secret, + size_t secretlen, const uint8_t *salt, size_t saltlen, + const EVP_MD *prf); + +int hkdf_expand(uint8_t *dest, size_t destlen, const uint8_t *secret, + size_t secretlen, const uint8_t *info, size_t infolen, + const EVP_MD *prf); + +int hkdf_expand_label(uint8_t *dest, size_t destlen, const uint8_t *secret, + size_t secretlen, const uint8_t *label, size_t labellen, + const EVP_MD *prf); + +int derive_initial_secret(uint8_t *dest, size_t destlen, const uint8_t *secret, + size_t secretlen, const uint8_t *salt, + size_t saltlen); + +int derive_client_initial_secret(uint8_t *dest, size_t destlen, + const uint8_t *secret, size_t secretlen); + +int derive_server_initial_secret(uint8_t *dest, size_t destlen, + const uint8_t *secret, size_t secretlen); + +int derive_packet_protection_key(uint8_t *key, size_t &keylen, uint8_t *iv, + size_t &ivlen, const uint8_t *secret, + size_t secretlen, const EVP_CIPHER *aead, + const EVP_MD *prf); + +int derive_header_protection_key(uint8_t *key, size_t &keylen, + const uint8_t *secret, size_t secretlen, + const EVP_CIPHER *aead, const EVP_MD *prf); + +ssize_t encrypt(uint8_t *dest, size_t destlen, const uint8_t *plaintext, + size_t plaintextlen, const uint8_t *key, size_t keylen, + const uint8_t *nonce, size_t noncelen, const uint8_t *ad, + size_t adlen, const EVP_CIPHER *aead); + +ssize_t decrypt(uint8_t *dest, size_t destlen, const uint8_t *ciphertext, + size_t ciphertextlen, const uint8_t *key, size_t keylen, + const uint8_t *nonce, size_t noncelen, const uint8_t *ad, + size_t adlen, const EVP_CIPHER *aead); + +ssize_t hp_mask(uint8_t *dest, size_t destlen, const uint8_t *key, + size_t keylen, const uint8_t *sample, size_t samplelen, + const EVP_CIPHER *cipher); + +enum class ErrorType { + Transport, + TransportVersionNegotiation, + Application, +}; + +struct Error { + Error(ErrorType type, uint16_t code) : type(type), code(code) {} + Error() : type(ErrorType::Transport), code(0) {} + + ErrorType type; + uint16_t code; +}; + +Error err_transport(int liberr); +Error err_transport_tls(int alert); +Error err_application(int liberr); + +} // namespace quic + +#endif // QUIC_H diff --git a/src/util.cc b/src/util.cc index 187fd3a8..d6abdb55 100644 --- a/src/util.cc +++ b/src/util.cc @@ -945,6 +945,56 @@ int create_nonblock_socket(int family) { return fd; } +int create_nonblock_udp_socket(int family) { +#ifdef SOCK_NONBLOCK + auto fd = socket(family, SOCK_DGRAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); + + if (fd == -1) { + return -1; + } +#else // !SOCK_NONBLOCK + auto fd = socket(family, SOCK_STREAM, 0); + + if (fd == -1) { + return -1; + } + + make_socket_nonblocking(fd); + make_socket_closeonexec(fd); +#endif // !SOCK_NONBLOCK + + return fd; +} + +int bind_any_addr_udp(int fd, int family) { + addrinfo hints{}; + addrinfo *res, *rp; + int rv; + + hints.ai_family = family; + hints.ai_socktype = SOCK_DGRAM; + hints.ai_flags = AI_PASSIVE; + + rv = getaddrinfo(nullptr, "0", &hints, &res); + if (rv != 0) { + return -1; + } + + for (rp = res; rp; rp = rp->ai_next) { + if (bind(fd, rp->ai_addr, rp->ai_addrlen) != -1) { + break; + } + } + + freeaddrinfo(res); + + if (!rp) { + return -1; + } + + return 0; +} + bool check_socket_connected(int fd) { int error; socklen_t len = sizeof(error); diff --git a/src/util.h b/src/util.h index 3443fe05..586d39c7 100644 --- a/src/util.h +++ b/src/util.h @@ -626,6 +626,9 @@ int make_socket_nonblocking(int fd); int make_socket_nodelay(int fd); int create_nonblock_socket(int family); +int create_nonblock_udp_socket(int family); + +int bind_any_addr_udp(int fd, int family); bool check_socket_connected(int fd);