/* * nghttp2 - HTTP/2 C Library * * Copyright (c) 2021 Tatsuhiro Tsujikawa * * 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 "shrpx_quic_connection_handler.h" #include #include #include #include "shrpx_worker.h" #include "shrpx_client_handler.h" #include "shrpx_log.h" #include "shrpx_http3_upstream.h" #include "shrpx_connection_handler.h" namespace shrpx { namespace { void stateless_reset_bucket_regen_timercb(struct ev_loop *loop, ev_timer *w, int revents) { auto quic_conn_handler = static_cast(w->data); quic_conn_handler->on_stateless_reset_bucket_regen(); } } // namespace QUICConnectionHandler::QUICConnectionHandler(Worker *worker) : worker_{worker}, stateless_reset_bucket_{SHRPX_QUIC_STATELESS_RESET_BURST} { ev_timer_init(&stateless_reset_bucket_regen_timer_, stateless_reset_bucket_regen_timercb, 0., 1.); stateless_reset_bucket_regen_timer_.data = this; } QUICConnectionHandler::~QUICConnectionHandler() { ev_timer_stop(worker_->get_loop(), &stateless_reset_bucket_regen_timer_); } int QUICConnectionHandler::handle_packet(const UpstreamAddr *faddr, const Address &remote_addr, const Address &local_addr, const uint8_t *data, size_t datalen) { int rv; uint32_t version; const uint8_t *dcid, *scid; size_t dcidlen, scidlen; rv = ngtcp2_pkt_decode_version_cid(&version, &dcid, &dcidlen, &scid, &scidlen, data, datalen, SHRPX_QUIC_SCIDLEN); switch (rv) { case 0: break; case NGTCP2_ERR_VERSION_NEGOTIATION: send_version_negotiation(faddr, version, dcid, dcidlen, scid, scidlen, remote_addr, local_addr); return 0; default: return 0; } auto config = get_config(); ngtcp2_cid dcid_key; ngtcp2_cid_init(&dcid_key, dcid, dcidlen); auto conn_handler = worker_->get_connection_handler(); ClientHandler *handler; auto it = connections_.find(dcid_key); if (it == std::end(connections_)) { if (!std::equal(dcid, dcid + SHRPX_QUIC_CID_PREFIXLEN, worker_->get_cid_prefix())) { auto quic_lwp = conn_handler->match_quic_lingering_worker_process_cid_prefix(dcid, dcidlen); if (quic_lwp) { if (conn_handler->forward_quic_packet_to_lingering_worker_process( quic_lwp, remote_addr, local_addr, data, datalen) == 0) { return 0; } return 0; } } auto it = close_waits_.find(dcid_key); if (it != std::end(close_waits_)) { auto cw = (*it).second; cw->handle_packet(faddr, remote_addr, local_addr, data, datalen); return 0; } // new connection auto &upstreamconf = config->conn.upstream; if (worker_->get_worker_stat()->num_connections >= upstreamconf.worker_connections) { if (LOG_ENABLED(INFO)) { LOG(INFO) << "Too many connections >=" << upstreamconf.worker_connections; } return 0; } ngtcp2_pkt_hd hd; ngtcp2_cid odcid, *podcid = nullptr; const uint8_t *token = nullptr; size_t tokenlen = 0; auto &quicconf = config->quic; switch (ngtcp2_accept(&hd, data, datalen)) { case 0: { // If we get Initial and it has the CID prefix of this worker, it // is likely that client is intentionally use the our prefix. // Just drop it. if (std::equal(dcid, dcid + SHRPX_QUIC_CID_PREFIXLEN, worker_->get_cid_prefix())) { return 0; } if (hd.token.len == 0) { if (quicconf.upstream.require_token) { send_retry(faddr, version, dcid, dcidlen, scid, scidlen, remote_addr, local_addr); return 0; } break; } auto &quic_secret = worker_->get_quic_secret(); auto &secret = quic_secret->token_secret; switch (hd.token.base[0]) { case NGTCP2_CRYPTO_TOKEN_MAGIC_RETRY: if (verify_retry_token(&odcid, hd.token.base, hd.token.len, &hd.dcid, &remote_addr.su.sa, remote_addr.len, secret.data()) != 0) { if (LOG_ENABLED(INFO)) { LOG(INFO) << "Failed to validate Retry token from remote=" << util::to_numeric_addr(&remote_addr); } // 2nd Retry packet is not allowed, so send CONNECTION_CLOSE // with INVALID_TOKEN. send_connection_close(faddr, version, &hd.dcid, &hd.scid, remote_addr, local_addr, NGTCP2_INVALID_TOKEN); return 0; } if (LOG_ENABLED(INFO)) { LOG(INFO) << "Successfully validated Retry token from remote=" << util::to_numeric_addr(&remote_addr); } podcid = &odcid; token = hd.token.base; tokenlen = hd.token.len; break; case NGTCP2_CRYPTO_TOKEN_MAGIC_REGULAR: if (verify_token(hd.token.base, hd.token.len, &remote_addr.su.sa, remote_addr.len, secret.data()) != 0) { if (LOG_ENABLED(INFO)) { LOG(INFO) << "Failed to validate token from remote=" << util::to_numeric_addr(&remote_addr); } if (quicconf.upstream.require_token) { send_retry(faddr, version, dcid, dcidlen, scid, scidlen, remote_addr, local_addr); return 0; } break; } if (LOG_ENABLED(INFO)) { LOG(INFO) << "Successfully validated token from remote=" << util::to_numeric_addr(&remote_addr); } token = hd.token.base; tokenlen = hd.token.len; break; default: if (quicconf.upstream.require_token) { send_retry(faddr, version, dcid, dcidlen, scid, scidlen, remote_addr, local_addr); return 0; } break; } break; } case NGTCP2_ERR_RETRY: send_retry(faddr, version, dcid, dcidlen, scid, scidlen, remote_addr, local_addr); return 0; case NGTCP2_ERR_VERSION_NEGOTIATION: send_version_negotiation(faddr, version, dcid, dcidlen, scid, scidlen, remote_addr, local_addr); return 0; default: if (!config->single_thread && !(data[0] & 0x80) && dcidlen > SHRPX_QUIC_CID_PREFIXLEN && !std::equal(dcid, dcid + SHRPX_QUIC_CID_PREFIXLEN, worker_->get_cid_prefix())) { if (conn_handler->forward_quic_packet(faddr, remote_addr, local_addr, dcid, data, datalen) == 0) { return 0; } } if (!(data[0] & 0x80)) { // TODO Must be rate limited send_stateless_reset(faddr, dcid, dcidlen, remote_addr, local_addr); } return 0; } handler = handle_new_connection(faddr, remote_addr, local_addr, hd, podcid, token, tokenlen); if (handler == nullptr) { return 0; } } else { handler = (*it).second; } if (handler->read_quic(faddr, remote_addr, local_addr, data, datalen) != 0) { delete handler; return 0; } handler->signal_write(); return 0; } ClientHandler *QUICConnectionHandler::handle_new_connection( const UpstreamAddr *faddr, const Address &remote_addr, const Address &local_addr, const ngtcp2_pkt_hd &hd, const ngtcp2_cid *odcid, const uint8_t *token, size_t tokenlen) { std::array host; std::array service; int rv; rv = getnameinfo(&remote_addr.su.sa, remote_addr.len, host.data(), host.size(), service.data(), service.size(), NI_NUMERICHOST | NI_NUMERICSERV); if (rv != 0) { LOG(ERROR) << "getnameinfo() failed: " << gai_strerror(rv); return nullptr; } auto ssl_ctx = worker_->get_quic_sv_ssl_ctx(); assert(ssl_ctx); auto ssl = tls::create_ssl(ssl_ctx); if (ssl == nullptr) { return nullptr; } assert(SSL_is_quic(ssl)); SSL_set_accept_state(ssl); auto config = get_config(); auto &quicconf = config->quic; if (quicconf.upstream.early_data) { SSL_set_quic_early_data_enabled(ssl, 1); } // Disable TLS session ticket if we don't have working ticket // keys. if (!worker_->get_ticket_keys()) { SSL_set_options(ssl, SSL_OP_NO_TICKET); } auto handler = std::make_unique( worker_, faddr->fd, ssl, StringRef{host.data()}, StringRef{service.data()}, remote_addr.su.sa.sa_family, faddr); auto upstream = std::make_unique(handler.get()); if (upstream->init(faddr, remote_addr, local_addr, hd, odcid, token, tokenlen) != 0) { return nullptr; } handler->setup_http3_upstream(std::move(upstream)); return handler.release(); } namespace { uint32_t generate_reserved_version(const Address &addr, uint32_t version) { uint32_t h = 0x811C9DC5u; const uint8_t *p = reinterpret_cast(&addr.su.sa); const uint8_t *ep = p + addr.len; for (; p != ep; ++p) { h ^= *p; h *= 0x01000193u; } version = htonl(version); p = (const uint8_t *)&version; ep = p + sizeof(version); for (; p != ep; ++p) { h ^= *p; h *= 0x01000193u; } h &= 0xf0f0f0f0u; h |= 0x0a0a0a0au; return h; } } // namespace int QUICConnectionHandler::send_retry( const UpstreamAddr *faddr, uint32_t version, const uint8_t *ini_dcid, size_t ini_dcidlen, const uint8_t *ini_scid, size_t ini_scidlen, const Address &remote_addr, const Address &local_addr) { std::array host; std::array port; if (getnameinfo(&remote_addr.su.sa, remote_addr.len, host.data(), host.size(), port.data(), port.size(), NI_NUMERICHOST | NI_NUMERICSERV) != 0) { return -1; } ngtcp2_cid retry_scid; retry_scid.datalen = SHRPX_QUIC_SCIDLEN; // We do not steer packet based on Retry CID. if (RAND_bytes(retry_scid.data, retry_scid.datalen) != 1) { return -1; } std::array token; size_t tokenlen; ngtcp2_cid idcid, iscid; ngtcp2_cid_init(&idcid, ini_dcid, ini_dcidlen); ngtcp2_cid_init(&iscid, ini_scid, ini_scidlen); auto &quic_secret = worker_->get_quic_secret(); auto &secret = quic_secret->token_secret; if (generate_retry_token(token.data(), tokenlen, &remote_addr.su.sa, remote_addr.len, &retry_scid, &idcid, secret.data()) != 0) { return -1; } std::array buf; auto nwrite = ngtcp2_crypto_write_retry(buf.data(), buf.size(), version, &iscid, &retry_scid, &idcid, token.data(), tokenlen); if (nwrite < 0) { LOG(ERROR) << "ngtcp2_crypto_write_retry: " << ngtcp2_strerror(nwrite); return -1; } return quic_send_packet(faddr, &remote_addr.su.sa, remote_addr.len, &local_addr.su.sa, local_addr.len, buf.data(), nwrite, 0); } int QUICConnectionHandler::send_version_negotiation( const UpstreamAddr *faddr, uint32_t version, const uint8_t *ini_dcid, size_t ini_dcidlen, const uint8_t *ini_scid, size_t ini_scidlen, const Address &remote_addr, const Address &local_addr) { std::array sv; sv[0] = generate_reserved_version(remote_addr, version); sv[1] = NGTCP2_PROTO_VER_V1; std::array buf; uint8_t rand_byte; util::random_bytes(&rand_byte, &rand_byte + 1, worker_->get_randgen()); auto nwrite = ngtcp2_pkt_write_version_negotiation( buf.data(), buf.size(), rand_byte, ini_scid, ini_scidlen, ini_dcid, ini_dcidlen, sv.data(), sv.size()); if (nwrite < 0) { LOG(ERROR) << "ngtcp2_pkt_write_version_negotiation: " << ngtcp2_strerror(nwrite); return -1; } return quic_send_packet(faddr, &remote_addr.su.sa, remote_addr.len, &local_addr.su.sa, local_addr.len, buf.data(), nwrite, 0); } int QUICConnectionHandler::send_stateless_reset(const UpstreamAddr *faddr, const uint8_t *dcid, size_t dcidlen, const Address &remote_addr, const Address &local_addr) { if (stateless_reset_bucket_ == 0) { if (LOG_ENABLED(INFO)) { LOG(INFO) << "Stateless Reset bucket has been depleted"; } return 0; } --stateless_reset_bucket_; if (!ev_is_active(&stateless_reset_bucket_regen_timer_)) { ev_timer_again(worker_->get_loop(), &stateless_reset_bucket_regen_timer_); } int rv; std::array token; ngtcp2_cid cid; ngtcp2_cid_init(&cid, dcid, dcidlen); auto &quic_secret = worker_->get_quic_secret(); auto &secret = quic_secret->stateless_reset_secret; rv = generate_quic_stateless_reset_token(token.data(), &cid, secret.data(), secret.size()); if (rv != 0) { return -1; } std::array rand_bytes; if (RAND_bytes(rand_bytes.data(), rand_bytes.size()) != 1) { return -1; } std::array buf; auto nwrite = ngtcp2_pkt_write_stateless_reset(buf.data(), buf.size(), token.data(), rand_bytes.data(), rand_bytes.size()); if (nwrite < 0) { LOG(ERROR) << "ngtcp2_pkt_write_stateless_reset: " << ngtcp2_strerror(nwrite); return -1; } if (LOG_ENABLED(INFO)) { LOG(INFO) << "Send stateless_reset to remote=" << util::to_numeric_addr(&remote_addr) << " dcid=" << util::format_hex(dcid, dcidlen); } return quic_send_packet(faddr, &remote_addr.su.sa, remote_addr.len, &local_addr.su.sa, local_addr.len, buf.data(), nwrite, 0); } int QUICConnectionHandler::send_connection_close( const UpstreamAddr *faddr, uint32_t version, const ngtcp2_cid *ini_dcid, const ngtcp2_cid *ini_scid, const Address &remote_addr, const Address &local_addr, uint64_t error_code) { std::array buf; auto nwrite = ngtcp2_crypto_write_connection_close( buf.data(), buf.size(), version, ini_scid, ini_dcid, error_code); if (nwrite < 0) { LOG(ERROR) << "ngtcp2_crypto_write_connection_close failed"; return -1; } if (LOG_ENABLED(INFO)) { LOG(INFO) << "Send Initial CONNECTION_CLOSE with error_code=" << log::hex << error_code << log::dec << " to remote=" << util::to_numeric_addr(&remote_addr) << " dcid=" << util::format_hex(ini_scid->data, ini_scid->datalen) << " scid=" << util::format_hex(ini_dcid->data, ini_dcid->datalen); } return quic_send_packet(faddr, &remote_addr.su.sa, remote_addr.len, &local_addr.su.sa, local_addr.len, buf.data(), nwrite, 0); } void QUICConnectionHandler::add_connection_id(const ngtcp2_cid *cid, ClientHandler *handler) { connections_.emplace(*cid, handler); } void QUICConnectionHandler::remove_connection_id(const ngtcp2_cid *cid) { connections_.erase(*cid); } void QUICConnectionHandler::add_close_wait(CloseWait *cw) { for (auto &cid : cw->scids) { close_waits_.emplace(cid, cw); } } void QUICConnectionHandler::remove_close_wait(const CloseWait *cw) { for (auto &cid : cw->scids) { close_waits_.erase(cid); } } void QUICConnectionHandler::on_stateless_reset_bucket_regen() { assert(stateless_reset_bucket_ < SHRPX_QUIC_STATELESS_RESET_BURST); if (++stateless_reset_bucket_ == SHRPX_QUIC_STATELESS_RESET_BURST) { ev_timer_stop(worker_->get_loop(), &stateless_reset_bucket_regen_timer_); } } static void close_wait_timeoutcb(struct ev_loop *loop, ev_timer *w, int revents) { auto cw = static_cast(w->data); if (LOG_ENABLED(INFO)) { LOG(INFO) << "close-wait period finished"; } auto quic_conn_handler = cw->worker->get_quic_connection_handler(); quic_conn_handler->remove_close_wait(cw); delete cw; } CloseWait::CloseWait(Worker *worker, std::vector scids, std::vector conn_close, ev_tstamp period) : worker{worker}, scids{std::move(scids)}, conn_close{std::move(conn_close)}, bytes_recv{0}, bytes_sent{0}, num_pkts_recv{0}, next_pkts_recv{1} { ++worker->get_worker_stat()->num_close_waits; ev_timer_init(&timer, close_wait_timeoutcb, period, 0.); timer.data = this; ev_timer_start(worker->get_loop(), &timer); } CloseWait::~CloseWait() { auto loop = worker->get_loop(); ev_timer_stop(loop, &timer); auto worker_stat = worker->get_worker_stat(); --worker_stat->num_close_waits; if (worker->get_graceful_shutdown() && worker_stat->num_connections == 0 && worker_stat->num_close_waits) { ev_break(loop); } } int CloseWait::handle_packet(const UpstreamAddr *faddr, const Address &remote_addr, const Address &local_addr, const uint8_t *data, size_t datalen) { if (conn_close.empty()) { return 0; } ++num_pkts_recv; bytes_recv += datalen; if (bytes_sent + conn_close.size() > 3 * bytes_recv || next_pkts_recv > num_pkts_recv) { return 0; } if (quic_send_packet(faddr, &remote_addr.su.sa, remote_addr.len, &local_addr.su.sa, local_addr.len, conn_close.data(), conn_close.size(), 0) != 0) { return -1; } next_pkts_recv *= 2; bytes_sent += conn_close.size(); return 0; } } // namespace shrpx