diff --git a/api/BUILD b/api/BUILD index 0a9934e0207b..915c7bce63f7 100644 --- a/api/BUILD +++ b/api/BUILD @@ -309,6 +309,7 @@ proto_library( "//envoy/extensions/upstreams/http/generic/v3:pkg", "//envoy/extensions/upstreams/http/http/v3:pkg", "//envoy/extensions/upstreams/http/tcp/v3:pkg", + "//envoy/extensions/upstreams/http/udp/v3:pkg", "//envoy/extensions/upstreams/http/v3:pkg", "//envoy/extensions/upstreams/tcp/generic/v3:pkg", "//envoy/extensions/upstreams/tcp/v3:pkg", diff --git a/api/envoy/extensions/upstreams/http/udp/v3/BUILD b/api/envoy/extensions/upstreams/http/udp/v3/BUILD new file mode 100644 index 000000000000..ee92fb652582 --- /dev/null +++ b/api/envoy/extensions/upstreams/http/udp/v3/BUILD @@ -0,0 +1,9 @@ +# DO NOT EDIT. This file is generated by tools/proto_format/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = ["@com_github_cncf_udpa//udpa/annotations:pkg"], +) diff --git a/api/envoy/extensions/upstreams/http/udp/v3/udp_connection_pool.proto b/api/envoy/extensions/upstreams/http/udp/v3/udp_connection_pool.proto new file mode 100644 index 000000000000..07d637d322f9 --- /dev/null +++ b/api/envoy/extensions/upstreams/http/udp/v3/udp_connection_pool.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +package envoy.extensions.upstreams.http.udp.v3; + +import "udpa/annotations/status.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.upstreams.http.udp.v3"; +option java_outer_classname = "UdpConnectionPoolProtoOuterClass"; +option java_multiple_files = true; +option go_package = "github.com/envoyproxy/go-control-plane/envoy/extensions/upstreams/http/udp/v3;udpv3"; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Udp Connection Pool] + +// A connection pool which forwards downstream HTTP as UDP to upstream, +// [#extension: envoy.upstreams.http.udp] +message UdpConnectionPoolProto { +} diff --git a/api/versioning/BUILD b/api/versioning/BUILD index a256f1a4cada..58405a717c4d 100644 --- a/api/versioning/BUILD +++ b/api/versioning/BUILD @@ -248,6 +248,7 @@ proto_library( "//envoy/extensions/upstreams/http/generic/v3:pkg", "//envoy/extensions/upstreams/http/http/v3:pkg", "//envoy/extensions/upstreams/http/tcp/v3:pkg", + "//envoy/extensions/upstreams/http/udp/v3:pkg", "//envoy/extensions/upstreams/http/v3:pkg", "//envoy/extensions/upstreams/tcp/generic/v3:pkg", "//envoy/extensions/upstreams/tcp/v3:pkg", diff --git a/bazel/external/quiche.BUILD b/bazel/external/quiche.BUILD index 1b90026a4a7c..e7492a72efa7 100644 --- a/bazel/external/quiche.BUILD +++ b/bazel/external/quiche.BUILD @@ -3177,6 +3177,21 @@ envoy_cc_library( ], ) +envoy_cc_library( + name = "quiche_common_connect_udp_datagram_payload_lib", + srcs = ["quiche/common/masque/connect_udp_datagram_payload.cc"], + hdrs = ["quiche/common/masque/connect_udp_datagram_payload.h"], + copts = quiche_copts, + repository = "@envoy", + visibility = ["//visibility:public"], + deps = [ + ":quic_core_data_lib", + ":quiche_common_platform_bug_tracker", + ":quiche_common_platform_logging", + "@com_google_absl//absl/strings", + ], +) + envoy_cc_library( name = "quiche_common_quiche_stream_lib", srcs = [], diff --git a/changelogs/current.yaml b/changelogs/current.yaml index 1c6d195cba49..7afc3e73b97d 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -240,6 +240,10 @@ new_features: added Runtime feature ``envoy.reloadable_features.max_request_headers_size_kb`` to override the default value of :ref:`max request headers size `. +- area: http + change: | + added support for CONNECT-UDP (RFC 9298). Can be disabled by setting runtime feature + ``envoy.reloadable_features.enable_connect_udp_support`` to false. - area: listeners change: | added :ref:`max_connections_to_accept_per_socket_event diff --git a/envoy/network/connection.h b/envoy/network/connection.h index 9bc3b4fe1b28..301366c4e54f 100644 --- a/envoy/network/connection.h +++ b/envoy/network/connection.h @@ -149,7 +149,7 @@ class Connection : public Event::DeferredDeletable, /** * @return Event::Dispatcher& the dispatcher backing this connection. */ - virtual Event::Dispatcher& dispatcher() PURE; + virtual Event::Dispatcher& dispatcher() const PURE; /** * @return uint64_t the unique local ID of this connection. diff --git a/envoy/router/router.h b/envoy/router/router.h index 7cb57a8d02b9..7f9326160ba5 100644 --- a/envoy/router/router.h +++ b/envoy/router/router.h @@ -1523,6 +1523,11 @@ using GenericConnPoolPtr = std::unique_ptr; */ class GenericConnPoolFactory : public Envoy::Config::TypedFactory { public: + /* + * Protocol used by the upstream sockets. + */ + enum class UpstreamProtocol { HTTP, TCP, UDP }; + ~GenericConnPoolFactory() override = default; /* @@ -1530,7 +1535,8 @@ class GenericConnPoolFactory : public Envoy::Config::TypedFactory { * @return may be null */ virtual GenericConnPoolPtr - createGenericConnPool(Upstream::ThreadLocalCluster& thread_local_cluster, bool is_connect, + createGenericConnPool(Upstream::ThreadLocalCluster& thread_local_cluster, + GenericConnPoolFactory::UpstreamProtocol upstream_protocol, const RouteEntry& route_entry, absl::optional downstream_protocol, Upstream::LoadBalancerContext* ctx) const PURE; diff --git a/envoy/stream_info/stream_info.h b/envoy/stream_info/stream_info.h index 198cd6255759..b5fbd9108fd3 100644 --- a/envoy/stream_info/stream_info.h +++ b/envoy/stream_info/stream_info.h @@ -135,6 +135,8 @@ struct ResponseCodeDetailValues { const std::string InvalidEnvoyRequestHeaders = "request_headers_failed_strict_check"; // The request was rejected due to a missing Path or :path header field. const std::string MissingPath = "missing_path_rejected"; + // The request was rejected due to an invalid Path or :path header field. + const std::string InvalidPath = "invalid_path"; // The request was rejected due to using an absolute path on a route not supporting them. const std::string AbsolutePath = "absolute_path_rejected"; // The request was rejected because path normalization was configured on and failed, probably due diff --git a/source/common/http/conn_manager_impl.cc b/source/common/http/conn_manager_impl.cc index 31fcaeec15cd..8dacc437d0b4 100644 --- a/source/common/http/conn_manager_impl.cc +++ b/source/common/http/conn_manager_impl.cc @@ -1177,6 +1177,15 @@ void ConnectionManagerImpl::ActiveStream::decodeHeaders(RequestHeaderMapSharedPt return; } + // Rewrites the host of CONNECT-UDP requests. + if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.enable_connect_udp_support") && + HeaderUtility::isConnectUdp(*request_headers_) && + !HeaderUtility::rewriteAuthorityForConnectUdp(*request_headers_)) { + sendLocalReply(Code::NotFound, "The path is incorrect for CONNECT-UDP", nullptr, absl::nullopt, + StreamInfo::ResponseCodeDetails::get().InvalidPath); + return; + } + // Currently we only support relative paths at the application layer. if (!request_headers_->getPathValue().empty() && request_headers_->getPathValue()[0] != '/') { connection_manager_.stats_.named_.downstream_rq_non_relative_path_.inc(); diff --git a/source/common/http/header_utility.cc b/source/common/http/header_utility.cc index 71f1942eb788..b5ce5a118b68 100644 --- a/source/common/http/header_utility.cc +++ b/source/common/http/header_utility.cc @@ -243,6 +243,11 @@ bool HeaderUtility::isConnect(const RequestHeaderMap& headers) { return headers.Method() && headers.Method()->value() == Http::Headers::get().MethodValues.Connect; } +bool HeaderUtility::isConnectUdp(const RequestHeaderMap& headers) { + return headers.Upgrade() && + headers.Upgrade()->value() == Http::Headers::get().UpgradeValues.ConnectUdp; +} + bool HeaderUtility::isConnectResponse(const RequestHeaderMap* request_headers, const ResponseHeaderMap& response_headers) { return request_headers && isConnect(*request_headers) && @@ -250,6 +255,50 @@ bool HeaderUtility::isConnectResponse(const RequestHeaderMap* request_headers, Http::Code::OK; } +bool HeaderUtility::rewriteAuthorityForConnectUdp(RequestHeaderMap& headers) { + // Per RFC 9298, the URI template must only contain ASCII characters in the range 0x21-0x7E. + absl::string_view path = headers.getPathValue(); + for (char c : path) { + unsigned char ascii_code = static_cast(c); + if (ascii_code < 0x21 || ascii_code > 0x7e) { + ENVOY_LOG_MISC(warn, "CONNECT-UDP request with a bad character in the path {}", path); + return false; + } + } + + // Extract target host and port from path using default template. + if (!absl::StartsWith(path, "/.well-known/masque/udp/")) { + ENVOY_LOG_MISC(warn, "CONNECT-UDP request path is not a well-known URI: {}", path); + return false; + } + + std::vector path_split = absl::StrSplit(path, '/'); + if (path_split.size() != 7 || path_split[4].empty() || path_split[5].empty() || + !path_split[6].empty()) { + ENVOY_LOG_MISC(warn, "CONNECT-UDP request with a malformed URI template in the path {}", path); + return false; + } + + // Utility::PercentEncoding::decode never returns an empty string if the input argument is not + // empty. + std::string target_host = Utility::PercentEncoding::decode(path_split[4]); + // Per RFC 9298, IPv6 Zone ID is not supported. + if (target_host.find('%') != std::string::npos) { + ENVOY_LOG_MISC(warn, "CONNECT-UDP request with a non-escpaed char (%) in the path {}", path); + return false; + } + std::string target_port = Utility::PercentEncoding::decode(path_split[5]); + + // If the host is an IPv6 address, surround the address with square brackets. + in6_addr sin6_addr; + bool is_ipv6 = (inet_pton(AF_INET6, target_host.c_str(), &sin6_addr) == 1); + std::string new_host = + absl::StrCat((is_ipv6 ? absl::StrCat("[", target_host, "]") : target_host), ":", target_port); + headers.setHost(new_host); + + return true; +} + #ifdef ENVOY_ENABLE_HTTP_DATAGRAMS bool HeaderUtility::isCapsuleProtocol(const RequestOrResponseHeaderMap& headers) { Http::HeaderMap::GetResult capsule_protocol = diff --git a/source/common/http/header_utility.h b/source/common/http/header_utility.h index bd63fe4c0bbe..4022a7059636 100644 --- a/source/common/http/header_utility.h +++ b/source/common/http/header_utility.h @@ -171,12 +171,23 @@ class HeaderUtility { */ static bool isConnect(const RequestHeaderMap& headers); + /** + * @brief a helper function to determine if the headers represent a CONNECT-UDP request. + */ + static bool isConnectUdp(const RequestHeaderMap& headers); + /** * @brief a helper function to determine if the headers represent an accepted CONNECT response. */ static bool isConnectResponse(const RequestHeaderMap* request_headers, const ResponseHeaderMap& response_headers); + /** + * @brief Rewrites the authority header field by parsing the path using the default CONNECT-UDP + * URI template. Returns true if the parsing was successful, otherwise returns false. + */ + static bool rewriteAuthorityForConnectUdp(RequestHeaderMap& headers); + #ifdef ENVOY_ENABLE_HTTP_DATAGRAMS /** * @brief Returns true if the Capsule-Protocol header field (RFC 9297) is set to true. If the diff --git a/source/common/http/headers.h b/source/common/http/headers.h index 1cda4d521eea..5c1252a8e2c6 100644 --- a/source/common/http/headers.h +++ b/source/common/http/headers.h @@ -142,6 +142,7 @@ class HeaderValues { const LowerCaseString ProxyAuthenticate{"proxy-authenticate"}; const LowerCaseString ProxyAuthorization{"proxy-authorization"}; + const LowerCaseString CapsuleProtocol{"capsule-protocol"}; const LowerCaseString ClientTraceId{"x-client-trace-id"}; const LowerCaseString Connection{"connection"}; const LowerCaseString ContentLength{"content-length"}; @@ -246,6 +247,7 @@ class HeaderValues { struct { const std::string H2c{"h2c"}; const std::string WebSocket{"websocket"}; + const std::string ConnectUdp{"connect-udp"}; } UpgradeValues; struct { diff --git a/source/common/network/connection_impl_base.h b/source/common/network/connection_impl_base.h index 4d01684ee34e..21ee7530a45d 100644 --- a/source/common/network/connection_impl_base.h +++ b/source/common/network/connection_impl_base.h @@ -23,7 +23,7 @@ class ConnectionImplBase : public FilterManagerConnection, // Network::Connection void addConnectionCallbacks(ConnectionCallbacks& cb) override; void removeConnectionCallbacks(ConnectionCallbacks& cb) override; - Event::Dispatcher& dispatcher() override { return dispatcher_; } + Event::Dispatcher& dispatcher() const override { return dispatcher_; } uint64_t id() const override { return id_; } void hashKey(std::vector& hash) const override; void setConnectionStats(const ConnectionStats& stats) override; diff --git a/source/common/network/multi_connection_base_impl.cc b/source/common/network/multi_connection_base_impl.cc index 96c232992fd2..839ad3af4cad 100644 --- a/source/common/network/multi_connection_base_impl.cc +++ b/source/common/network/multi_connection_base_impl.cc @@ -346,7 +346,7 @@ void MultiConnectionBaseImpl::close(ConnectionCloseType type, absl::string_view connections_[0]->close(type, details); } -Event::Dispatcher& MultiConnectionBaseImpl::dispatcher() { +Event::Dispatcher& MultiConnectionBaseImpl::dispatcher() const { ASSERT(&dispatcher_ == &connections_[0]->dispatcher()); return connections_[0]->dispatcher(); } diff --git a/source/common/network/multi_connection_base_impl.h b/source/common/network/multi_connection_base_impl.h index c5574fe3334f..a6521d0a0254 100644 --- a/source/common/network/multi_connection_base_impl.h +++ b/source/common/network/multi_connection_base_impl.h @@ -124,7 +124,7 @@ class MultiConnectionBaseImpl : public ClientConnection, // Methods implemented largely by this class itself. uint64_t id() const override; - Event::Dispatcher& dispatcher() override; + Event::Dispatcher& dispatcher() const override; void close(ConnectionCloseType type) override { close(type, ""); } void close(ConnectionCloseType type, absl::string_view details) override; bool readEnabled() const override; diff --git a/source/common/quic/envoy_quic_client_session.h b/source/common/quic/envoy_quic_client_session.h index e3582d4c333c..a5dff63fb883 100644 --- a/source/common/quic/envoy_quic_client_session.h +++ b/source/common/quic/envoy_quic_client_session.h @@ -65,9 +65,11 @@ class EnvoyQuicClientSession : public QuicFilterManagerConnectionImpl, std::unique_ptr encrypter) override; quic::HttpDatagramSupport LocalHttpDatagramSupport() override { - // TODO(https://github.com/envoyproxy/envoy/issues/23564): Http3 Datagram support should be - // turned on by returning quic::HttpDatagramSupport::kRfc once the CONNECT-UDP support work is - // completed. +#ifdef ENVOY_ENABLE_HTTP_DATAGRAMS + if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.enable_connect_udp_support")) { + return quic::HttpDatagramSupport::kRfc; + } +#endif return quic::HttpDatagramSupport::kNone; } diff --git a/source/common/quic/envoy_quic_client_stream.cc b/source/common/quic/envoy_quic_client_stream.cc index e62805e28b2e..dca38b6518cc 100644 --- a/source/common/quic/envoy_quic_client_stream.cc +++ b/source/common/quic/envoy_quic_client_stream.cc @@ -87,6 +87,13 @@ Http::Status EnvoyQuicClientStream::encodeHeaders(const Http::RequestHeaderMap& if (headers.Method()->value() == "HEAD") { sent_head_request_ = true; } +#endif +#ifdef ENVOY_ENABLE_HTTP_DATAGRAMS + if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.enable_connect_udp_support") && + (Http::HeaderUtility::isCapsuleProtocol(headers) || + Http::HeaderUtility::isConnectUdp(headers))) { + useCapsuleProtocol(); + } #endif { IncrementalBytesSentTracker tracker(*this, *mutableBytesMeter(), true); diff --git a/source/common/quic/envoy_quic_client_stream.h b/source/common/quic/envoy_quic_client_stream.h index bb55bd680155..7c40341b422f 100644 --- a/source/common/quic/envoy_quic_client_stream.h +++ b/source/common/quic/envoy_quic_client_stream.h @@ -51,12 +51,6 @@ class EnvoyQuicClientStream : public quic::QuicSpdyClientStream, void OnConnectionClosed(quic::QuicErrorCode error, quic::ConnectionCloseSource source) override; void clearWatermarkBuffer(); -#ifdef ENVOY_ENABLE_HTTP_DATAGRAMS - // Makes the QUIC stream use Capsule Protocol. Once this method is called, any calls to encodeData - // are expected to contain capsules which will be sent along as HTTP Datagrams. Also, the stream - // starts to receive HTTP/3 Datagrams and decode into Capsules. - void useCapsuleProtocol(); -#endif protected: // EnvoyQuicStream @@ -83,6 +77,13 @@ class EnvoyQuicClientStream : public quic::QuicSpdyClientStream, // Deliver awaiting trailers if body has been delivered. void maybeDecodeTrailers(); +#ifdef ENVOY_ENABLE_HTTP_DATAGRAMS + // Makes the QUIC stream use Capsule Protocol. Once this method is called, any calls to encodeData + // are expected to contain capsules which will be sent along as HTTP Datagrams. Also, the stream + // starts to receive HTTP/3 Datagrams and decode into Capsules. + void useCapsuleProtocol(); +#endif + Http::ResponseDecoder* response_decoder_{nullptr}; bool decoded_1xx_{false}; #ifdef ENVOY_ENABLE_HTTP_DATAGRAMS diff --git a/source/common/quic/envoy_quic_server_session.h b/source/common/quic/envoy_quic_server_session.h index 1858c4e78833..94e4f145f47a 100644 --- a/source/common/quic/envoy_quic_server_session.h +++ b/source/common/quic/envoy_quic_server_session.h @@ -115,9 +115,11 @@ class EnvoyQuicServerSession : public quic::QuicServerSessionBase, quic::QuicSpdyStream* CreateOutgoingUnidirectionalStream() override; quic::HttpDatagramSupport LocalHttpDatagramSupport() override { - // TODO(jeongseokson): Http3 Datagram support should be turned on by returning - // quic::HttpDatagramSupport::kRfc once the CONNECT-UDP support work is completed. +#ifdef ENVOY_ENABLE_HTTP_DATAGRAMS + return quic::HttpDatagramSupport::kRfc; +#else return quic::HttpDatagramSupport::kNone; +#endif } // QuicFilterManagerConnectionImpl diff --git a/source/common/quic/envoy_quic_server_stream.cc b/source/common/quic/envoy_quic_server_stream.cc index 123ac025aa14..7a12c93e7b08 100644 --- a/source/common/quic/envoy_quic_server_stream.cc +++ b/source/common/quic/envoy_quic_server_stream.cc @@ -41,6 +41,11 @@ EnvoyQuicServerStream::EnvoyQuicServerStream( stats_gatherer_ = new QuicStatsGatherer(&filterManagerConnection()->dispatcher().timeSource()); set_ack_listener(stats_gatherer_); + // TODO(https://github.com/envoyproxy/envoy/issues/23564): Remove this line when the QUICHE is + // updated with a more reasonable default expiry time for QUIC Datagrams. + if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.enable_connect_udp_support")) { + SetMaxDatagramTimeInQueue(::quic::QuicTime::Delta::FromMilliseconds(100)); + } } void EnvoyQuicServerStream::encode1xxHeaders(const Http::ResponseHeaderMap& headers) { @@ -261,6 +266,14 @@ void EnvoyQuicServerStream::OnInitialHeadersComplete(bool fin, size_t frame_len, } #endif +#ifdef ENVOY_ENABLE_HTTP_DATAGRAMS + if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.enable_connect_udp_support") && + (Http::HeaderUtility::isCapsuleProtocol(*headers) || + Http::HeaderUtility::isConnectUdp(*headers))) { + useCapsuleProtocol(); + } +#endif + request_decoder_->decodeHeaders(std::move(headers), /*end_stream=*/fin); ConsumeHeaderList(); } diff --git a/source/common/quic/envoy_quic_server_stream.h b/source/common/quic/envoy_quic_server_stream.h index a262dc15af27..ea9b4bf07ad8 100644 --- a/source/common/quic/envoy_quic_server_stream.h +++ b/source/common/quic/envoy_quic_server_stream.h @@ -79,13 +79,6 @@ class EnvoyQuicServerStream : public quic::QuicSpdyServerStreamBase, Http::HeaderUtility::HeaderValidationResult validateHeader(absl::string_view header_name, absl::string_view header_value) override; -#ifdef ENVOY_ENABLE_HTTP_DATAGRAMS - // Makes the QUIC stream use Capsule Protocol. Once this method is called, any calls to encodeData - // are expected to contain capsules which will be sent along as HTTP Datagrams. Also, the stream - // starts to receive HTTP/3 Datagrams and decode into Capsules. - void useCapsuleProtocol(); -#endif - protected: // EnvoyQuicStream void switchStreamBlockState() override; @@ -113,6 +106,13 @@ class EnvoyQuicServerStream : public quic::QuicSpdyServerStreamBase, // Deliver awaiting trailers if body has been delivered. void maybeDecodeTrailers(); +#ifdef ENVOY_ENABLE_HTTP_DATAGRAMS + // Makes the QUIC stream use Capsule Protocol. Once this method is called, any calls to encodeData + // are expected to contain capsules which will be sent along as HTTP Datagrams. Also, the stream + // starts to receive HTTP/3 Datagrams and decode into Capsules. + void useCapsuleProtocol(); +#endif + Http::RequestDecoder* request_decoder_{nullptr}; envoy::config::core::v3::HttpProtocolOptions::HeadersWithUnderscoresAction headers_with_underscores_action_; diff --git a/source/common/quic/http_datagram_handler.cc b/source/common/quic/http_datagram_handler.cc index 804ed5c08fd3..1af50e90a510 100644 --- a/source/common/quic/http_datagram_handler.cc +++ b/source/common/quic/http_datagram_handler.cc @@ -51,9 +51,14 @@ bool HttpDatagramHandler::OnCapsule(const quiche::Capsule& capsule) { if (status == quic::MessageStatus::MESSAGE_STATUS_SUCCESS) { return true; } - // Drops the Datagram and move on without reporting a failure in the following statuses. - if (status == quic::MessageStatus::MESSAGE_STATUS_BLOCKED || - status == quic::MessageStatus::MESSAGE_STATUS_TOO_LARGE) { + // When SendHttp3Datagram cannot send a datagram immediately, it puts it into the queue and + // returns MESSAGE_STATUS_BLOCKED. + if (status == quic::MessageStatus::MESSAGE_STATUS_BLOCKED) { + ENVOY_LOG(trace, fmt::format("SendHttpH3Datagram failed: status = {}, buffers the Datagram.", + quic::MessageStatusToString(status))); + return true; + } + if (status == quic::MessageStatus::MESSAGE_STATUS_TOO_LARGE) { ENVOY_LOG(warn, fmt::format("SendHttpH3Datagram failed: status = {}, drops the Datagram.", quic::MessageStatusToString(status))); return true; diff --git a/source/common/quic/quic_filter_manager_connection_impl.h b/source/common/quic/quic_filter_manager_connection_impl.h index 0b8a6ec92d15..1520e01400e4 100644 --- a/source/common/quic/quic_filter_manager_connection_impl.h +++ b/source/common/quic/quic_filter_manager_connection_impl.h @@ -59,7 +59,7 @@ class QuicFilterManagerConnectionImpl : public Network::ConnectionImplBase, } close(type); } - Event::Dispatcher& dispatcher() override { return dispatcher_; } + Event::Dispatcher& dispatcher() const override { return dispatcher_; } std::string nextProtocol() const override { return EMPTY_STRING; } // No-op. TCP_NODELAY doesn't apply to UDP. void noDelay(bool /*enable*/) override {} diff --git a/source/common/router/config_impl.cc b/source/common/router/config_impl.cc index 29d631dc1f37..891af09a8e37 100644 --- a/source/common/router/config_impl.cc +++ b/source/common/router/config_impl.cc @@ -649,7 +649,9 @@ RouteEntryImplBase::RouteEntryImplBase(const CommonVirtualHostSharedPtr& vhost, if (!success) { throw EnvoyException(absl::StrCat("Duplicate upgrade ", upgrade_config.upgrade_type())); } - if (upgrade_config.upgrade_type() == Http::Headers::get().MethodValues.Connect) { + if (upgrade_config.upgrade_type() == Http::Headers::get().MethodValues.Connect || + (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.enable_connect_udp_support") && + upgrade_config.upgrade_type() == Http::Headers::get().UpgradeValues.ConnectUdp)) { connect_config_ = std::make_unique(upgrade_config.connect_config()); } else if (upgrade_config.has_connect_config()) { throw EnvoyException(absl::StrCat("Non-CONNECT upgrade type ", upgrade_config.upgrade_type(), @@ -1600,7 +1602,9 @@ ConnectRouteEntryImpl::currentUrlPathAfterRewrite(const Http::RequestHeaderMap& RouteConstSharedPtr ConnectRouteEntryImpl::matches(const Http::RequestHeaderMap& headers, const StreamInfo::StreamInfo& stream_info, uint64_t random_value) const { - if (Http::HeaderUtility::isConnect(headers) && + if ((Http::HeaderUtility::isConnect(headers) || + (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.enable_connect_udp_support") && + Http::HeaderUtility::isConnectUdp(headers))) && RouteEntryImplBase::matchRoute(headers, stream_info, random_value)) { return clusterEntry(headers, random_value); } diff --git a/source/common/router/router.cc b/source/common/router/router.cc index 6e50cd72c999..db2e0b440723 100644 --- a/source/common/router/router.cc +++ b/source/common/router/router.cc @@ -769,18 +769,21 @@ Filter::createConnPool(Upstream::ThreadLocalCluster& thread_local_cluster) { factory = &config_.router_context_.genericConnPoolFactory(); } - bool should_tcp_proxy = false; - + using UpstreamProtocol = Envoy::Router::GenericConnPoolFactory::UpstreamProtocol; + UpstreamProtocol upstream_protocol = UpstreamProtocol::HTTP; if (route_entry_->connectConfig().has_value()) { auto method = downstream_headers_->getMethodValue(); - should_tcp_proxy = (method == Http::Headers::get().MethodValues.Connect); - - // Allow POST for proxying raw TCP if it is configured. - if (!should_tcp_proxy && route_entry_->connectConfig()->allow_post()) { - should_tcp_proxy = (method == Http::Headers::get().MethodValues.Post); + if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.enable_connect_udp_support") && + Http::HeaderUtility::isConnectUdp(*downstream_headers_)) { + upstream_protocol = UpstreamProtocol::UDP; + } else if (method == Http::Headers::get().MethodValues.Connect || + (route_entry_->connectConfig()->allow_post() && + method == Http::Headers::get().MethodValues.Post)) { + // Allow POST for proxying raw TCP if it is configured. + upstream_protocol = UpstreamProtocol::TCP; } } - return factory->createGenericConnPool(thread_local_cluster, should_tcp_proxy, *route_entry_, + return factory->createGenericConnPool(thread_local_cluster, upstream_protocol, *route_entry_, callbacks_->streamInfo().protocol(), this); } diff --git a/source/common/runtime/runtime_features.cc b/source/common/runtime/runtime_features.cc index 214a6ee911a0..b4fe9e99cbef 100644 --- a/source/common/runtime/runtime_features.cc +++ b/source/common/runtime/runtime_features.cc @@ -38,6 +38,7 @@ RUNTIME_GUARD(envoy_reloadable_features_count_unused_mapped_pages_as_free); RUNTIME_GUARD(envoy_reloadable_features_dfp_mixed_scheme); RUNTIME_GUARD(envoy_reloadable_features_enable_aws_credentials_file); RUNTIME_GUARD(envoy_reloadable_features_enable_compression_bomb_protection); +RUNTIME_GUARD(envoy_reloadable_features_enable_connect_udp_support); RUNTIME_GUARD(envoy_reloadable_features_enable_intermediate_ca); RUNTIME_GUARD(envoy_reloadable_features_enable_update_listener_socket_options); RUNTIME_GUARD(envoy_reloadable_features_expand_agnostic_stream_lifetime); diff --git a/source/extensions/extensions_build_config.bzl b/source/extensions/extensions_build_config.bzl index 49f995c7bc32..1b777ecfb4c7 100644 --- a/source/extensions/extensions_build_config.bzl +++ b/source/extensions/extensions_build_config.bzl @@ -299,6 +299,7 @@ EXTENSIONS = { "envoy.upstreams.http.http": "//source/extensions/upstreams/http/http:config", "envoy.upstreams.http.tcp": "//source/extensions/upstreams/http/tcp:config", + "envoy.upstreams.http.udp": "//source/extensions/upstreams/http/udp:config", # # Watchdog actions diff --git a/source/extensions/extensions_metadata.yaml b/source/extensions/extensions_metadata.yaml index 9dc8d4f64040..92dd4929745b 100644 --- a/source/extensions/extensions_metadata.yaml +++ b/source/extensions/extensions_metadata.yaml @@ -1221,6 +1221,11 @@ envoy.upstreams.http.tcp: - envoy.upstreams security_posture: robust_to_untrusted_downstream status: stable +envoy.upstreams.http.udp: + categories: + - envoy.upstreams + security_posture: robust_to_untrusted_downstream + status: alpha envoy.upstreams.tcp.generic: categories: - envoy.upstreams diff --git a/source/extensions/upstreams/http/generic/BUILD b/source/extensions/upstreams/http/generic/BUILD index 759f4626f205..a15d7ac05eed 100644 --- a/source/extensions/upstreams/http/generic/BUILD +++ b/source/extensions/upstreams/http/generic/BUILD @@ -20,6 +20,7 @@ envoy_cc_extension( deps = [ "//source/extensions/upstreams/http/http:upstream_request_lib", "//source/extensions/upstreams/http/tcp:upstream_request_lib", + "//source/extensions/upstreams/http/udp:upstream_request_lib", "@envoy_api//envoy/extensions/upstreams/http/generic/v3:pkg_cc_proto", ], ) diff --git a/source/extensions/upstreams/http/generic/config.cc b/source/extensions/upstreams/http/generic/config.cc index b2b02e28ac56..9b09233137c1 100644 --- a/source/extensions/upstreams/http/generic/config.cc +++ b/source/extensions/upstreams/http/generic/config.cc @@ -2,6 +2,7 @@ #include "source/extensions/upstreams/http/http/upstream_request.h" #include "source/extensions/upstreams/http/tcp/upstream_request.h" +#include "source/extensions/upstreams/http/udp/upstream_request.h" namespace Envoy { namespace Extensions { @@ -9,19 +10,32 @@ namespace Upstreams { namespace Http { namespace Generic { +using UpstreamProtocol = Envoy::Router::GenericConnPoolFactory::UpstreamProtocol; + Router::GenericConnPoolPtr GenericGenericConnPoolFactory::createGenericConnPool( - Upstream::ThreadLocalCluster& thread_local_cluster, bool is_connect, + Upstream::ThreadLocalCluster& thread_local_cluster, UpstreamProtocol upstream_protocol, const Router::RouteEntry& route_entry, absl::optional downstream_protocol, Upstream::LoadBalancerContext* ctx) const { - if (is_connect) { - auto ret = std::make_unique( - thread_local_cluster, is_connect, route_entry, downstream_protocol, ctx); - return (ret->valid() ? std::move(ret) : nullptr); + switch (upstream_protocol) { + case UpstreamProtocol::HTTP: { + auto http_conn_pool = std::make_unique( + thread_local_cluster, route_entry, downstream_protocol, ctx); + return (http_conn_pool->valid() ? std::move(http_conn_pool) : nullptr); + } + case UpstreamProtocol::TCP: { + auto tcp_conn_pool = + std::make_unique(thread_local_cluster, route_entry, ctx); + return (tcp_conn_pool->valid() ? std::move(tcp_conn_pool) : nullptr); + } + case UpstreamProtocol::UDP: { + auto udp_conn_pool = + std::make_unique(thread_local_cluster, ctx); + return (udp_conn_pool->valid() ? std::move(udp_conn_pool) : nullptr); } - auto ret = std::make_unique( - thread_local_cluster, is_connect, route_entry, downstream_protocol, ctx); - return (ret->valid() ? std::move(ret) : nullptr); + } + + return nullptr; } REGISTER_FACTORY(GenericGenericConnPoolFactory, Router::GenericConnPoolFactory); diff --git a/source/extensions/upstreams/http/generic/config.h b/source/extensions/upstreams/http/generic/config.h index 62f9d72602c7..524f3f22444d 100644 --- a/source/extensions/upstreams/http/generic/config.h +++ b/source/extensions/upstreams/http/generic/config.h @@ -18,7 +18,8 @@ class GenericGenericConnPoolFactory : public Router::GenericConnPoolFactory { std::string name() const override { return "envoy.filters.connection_pools.http.generic"; } std::string category() const override { return "envoy.upstreams"; } Router::GenericConnPoolPtr - createGenericConnPool(Upstream::ThreadLocalCluster& thread_local_cluster, bool is_connect, + createGenericConnPool(Upstream::ThreadLocalCluster& thread_local_cluster, + Router::GenericConnPoolFactory::UpstreamProtocol upstream_protocol, const Router::RouteEntry& route_entry, absl::optional downstream_protocol, Upstream::LoadBalancerContext* ctx) const override; diff --git a/source/extensions/upstreams/http/http/config.cc b/source/extensions/upstreams/http/http/config.cc index 620915a11d66..7004c49caa9c 100644 --- a/source/extensions/upstreams/http/http/config.cc +++ b/source/extensions/upstreams/http/http/config.cc @@ -8,13 +8,15 @@ namespace Upstreams { namespace Http { namespace Http { +using UpstreamProtocol = Envoy::Router::GenericConnPoolFactory::UpstreamProtocol; + Router::GenericConnPoolPtr HttpGenericConnPoolFactory::createGenericConnPool( - Upstream::ThreadLocalCluster& thread_local_cluster, bool is_connect, + Upstream::ThreadLocalCluster& thread_local_cluster, UpstreamProtocol, const Router::RouteEntry& route_entry, absl::optional downstream_protocol, Upstream::LoadBalancerContext* ctx) const { - auto ret = std::make_unique(thread_local_cluster, is_connect, route_entry, - downstream_protocol, ctx); + auto ret = + std::make_unique(thread_local_cluster, route_entry, downstream_protocol, ctx); return (ret->valid() ? std::move(ret) : nullptr); } diff --git a/source/extensions/upstreams/http/http/config.h b/source/extensions/upstreams/http/http/config.h index e438999e7dac..717d028e4d5e 100644 --- a/source/extensions/upstreams/http/http/config.h +++ b/source/extensions/upstreams/http/http/config.h @@ -18,7 +18,8 @@ class HttpGenericConnPoolFactory : public Router::GenericConnPoolFactory { std::string name() const override { return "envoy.filters.connection_pools.http.http"; } std::string category() const override { return "envoy.upstreams"; } Router::GenericConnPoolPtr - createGenericConnPool(Upstream::ThreadLocalCluster& thread_local_cluster, bool is_connect, + createGenericConnPool(Upstream::ThreadLocalCluster& thread_local_cluster, + Router::GenericConnPoolFactory::UpstreamProtocol upstream_protocol, const Router::RouteEntry& route_entry, absl::optional downstream_protocol, Upstream::LoadBalancerContext* ctx) const override; diff --git a/source/extensions/upstreams/http/http/upstream_request.h b/source/extensions/upstreams/http/http/upstream_request.h index b1e82f06ca22..88c437c8e3f7 100644 --- a/source/extensions/upstreams/http/http/upstream_request.h +++ b/source/extensions/upstreams/http/http/upstream_request.h @@ -21,11 +21,10 @@ namespace Http { class HttpConnPool : public Router::GenericConnPool, public Envoy::Http::ConnectionPool::Callbacks { public: // GenericConnPool - HttpConnPool(Upstream::ThreadLocalCluster& thread_local_cluster, bool is_connect, + HttpConnPool(Upstream::ThreadLocalCluster& thread_local_cluster, const Router::RouteEntry& route_entry, absl::optional downstream_protocol, Upstream::LoadBalancerContext* ctx) { - ASSERT(!is_connect); pool_data_ = thread_local_cluster.httpConnPool(route_entry.priority(), downstream_protocol, ctx); } diff --git a/source/extensions/upstreams/http/tcp/config.cc b/source/extensions/upstreams/http/tcp/config.cc index b0773c0a9535..8cd71bbb21ba 100644 --- a/source/extensions/upstreams/http/tcp/config.cc +++ b/source/extensions/upstreams/http/tcp/config.cc @@ -9,12 +9,10 @@ namespace Http { namespace Tcp { Router::GenericConnPoolPtr TcpGenericConnPoolFactory::createGenericConnPool( - Upstream::ThreadLocalCluster& thread_local_cluster, bool is_connect, - const Router::RouteEntry& route_entry, - absl::optional downstream_protocol, - Upstream::LoadBalancerContext* ctx) const { - auto ret = std::make_unique(thread_local_cluster, is_connect, route_entry, - downstream_protocol, ctx); + Upstream::ThreadLocalCluster& thread_local_cluster, + Router::GenericConnPoolFactory::UpstreamProtocol, const Router::RouteEntry& route_entry, + absl::optional, Upstream::LoadBalancerContext* ctx) const { + auto ret = std::make_unique(thread_local_cluster, route_entry, ctx); return (ret->valid() ? std::move(ret) : nullptr); } diff --git a/source/extensions/upstreams/http/tcp/config.h b/source/extensions/upstreams/http/tcp/config.h index 784e8f15b502..bf92975f9180 100644 --- a/source/extensions/upstreams/http/tcp/config.h +++ b/source/extensions/upstreams/http/tcp/config.h @@ -18,7 +18,8 @@ class TcpGenericConnPoolFactory : public Router::GenericConnPoolFactory { std::string name() const override { return "envoy.filters.connection_pools.http.tcp"; } std::string category() const override { return "envoy.upstreams"; } Router::GenericConnPoolPtr - createGenericConnPool(Upstream::ThreadLocalCluster& thread_local_cluster, bool is_connect, + createGenericConnPool(Upstream::ThreadLocalCluster& thread_local_cluster, + Router::GenericConnPoolFactory::UpstreamProtocol upstream_protocol, const Router::RouteEntry& route_entry, absl::optional downstream_protocol, Upstream::LoadBalancerContext* ctx) const override; diff --git a/source/extensions/upstreams/http/tcp/upstream_request.cc b/source/extensions/upstreams/http/tcp/upstream_request.cc index aa96aa176d8d..2e9a3eb1357b 100644 --- a/source/extensions/upstreams/http/tcp/upstream_request.cc +++ b/source/extensions/upstreams/http/tcp/upstream_request.cc @@ -71,7 +71,7 @@ Envoy::Http::Status TcpUpstream::encodeHeaders(const Envoy::Http::RequestHeaderM Envoy::Http::ResponseHeaderMapPtr headers{ Envoy::Http::createHeaderMap( {{Envoy::Http::Headers::get().Status, "200"}})}; - upstream_request_->decodeHeaders(std::move(headers), false); + upstream_request_->decodeHeaders(std::move(headers), /*end_stream=*/false); return Envoy::Http::okStatus(); } diff --git a/source/extensions/upstreams/http/tcp/upstream_request.h b/source/extensions/upstreams/http/tcp/upstream_request.h index e397e8b8c3e9..e2f5fa861d20 100644 --- a/source/extensions/upstreams/http/tcp/upstream_request.h +++ b/source/extensions/upstreams/http/tcp/upstream_request.h @@ -22,9 +22,8 @@ namespace Tcp { class TcpConnPool : public Router::GenericConnPool, public Envoy::Tcp::ConnectionPool::Callbacks { public: - TcpConnPool(Upstream::ThreadLocalCluster& thread_local_cluster, bool, - const Router::RouteEntry& route_entry, absl::optional, - Upstream::LoadBalancerContext* ctx) { + TcpConnPool(Upstream::ThreadLocalCluster& thread_local_cluster, + const Router::RouteEntry& route_entry, Upstream::LoadBalancerContext* ctx) { conn_pool_data_ = thread_local_cluster.tcpConnPool(route_entry.priority(), ctx); } void newStream(Router::GenericConnectionPoolCallbacks* callbacks) override { diff --git a/source/extensions/upstreams/http/udp/BUILD b/source/extensions/upstreams/http/udp/BUILD new file mode 100644 index 000000000000..1967e17642e2 --- /dev/null +++ b/source/extensions/upstreams/http/udp/BUILD @@ -0,0 +1,56 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_extension", + "envoy_cc_library", + "envoy_extension_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_extension_package() + +envoy_cc_extension( + name = "config", + srcs = [ + "config.cc", + ], + hdrs = [ + "config.h", + ], + visibility = ["//visibility:public"], + deps = [ + ":upstream_request_lib", + "@envoy_api//envoy/extensions/upstreams/http/udp/v3:pkg_cc_proto", + ], +) + +envoy_cc_library( + name = "upstream_request_lib", + srcs = [ + "upstream_request.cc", + ], + hdrs = [ + "upstream_request.h", + ], + visibility = ["//visibility:public"], + deps = [ + "//envoy/http:codes_interface", + "//envoy/http:filter_interface", + "//envoy/upstream:upstream_interface", + "//source/common/common:assert_lib", + "//source/common/common:minimal_logger_lib", + "//source/common/common:utility_lib", + "//source/common/http:codes_lib", + "//source/common/http:header_map_lib", + "//source/common/http:headers_lib", + "//source/common/http:message_lib", + "//source/common/network:application_protocol_lib", + "//source/common/network:transport_socket_options_lib", + "//source/common/router:router_lib", + "//source/common/upstream:load_balancer_lib", + "//source/extensions/common/proxy_protocol:proxy_protocol_header_lib", + "@com_github_google_quiche//:quic_core_http_spdy_session_lib", + "@com_github_google_quiche//:quic_core_types_lib", + "@com_github_google_quiche//:quiche_common_connect_udp_datagram_payload_lib", + ], +) diff --git a/source/extensions/upstreams/http/udp/config.cc b/source/extensions/upstreams/http/udp/config.cc new file mode 100644 index 000000000000..54ea9e1e4c19 --- /dev/null +++ b/source/extensions/upstreams/http/udp/config.cc @@ -0,0 +1,25 @@ +#include "source/extensions/upstreams/http/udp/config.h" + +#include "source/extensions/upstreams/http/udp/upstream_request.h" + +namespace Envoy { +namespace Extensions { +namespace Upstreams { +namespace Http { +namespace Udp { + +Router::GenericConnPoolPtr UdpGenericConnPoolFactory::createGenericConnPool( + Upstream::ThreadLocalCluster& thread_local_cluster, + Router::GenericConnPoolFactory::UpstreamProtocol, const Router::RouteEntry&, + absl::optional, Upstream::LoadBalancerContext* ctx) const { + auto ret = std::make_unique(thread_local_cluster, ctx); + return (ret->valid() ? std::move(ret) : nullptr); +} + +REGISTER_FACTORY(UdpGenericConnPoolFactory, Router::GenericConnPoolFactory); + +} // namespace Udp +} // namespace Http +} // namespace Upstreams +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/upstreams/http/udp/config.h b/source/extensions/upstreams/http/udp/config.h new file mode 100644 index 000000000000..7bc42d649dd8 --- /dev/null +++ b/source/extensions/upstreams/http/udp/config.h @@ -0,0 +1,37 @@ +#pragma once + +#include "envoy/extensions/upstreams/http/udp/v3/udp_connection_pool.pb.h" +#include "envoy/registry/registry.h" +#include "envoy/router/router.h" + +namespace Envoy { +namespace Extensions { +namespace Upstreams { +namespace Http { +namespace Udp { + +/** + * Config registration for the UdpConnPool. @see Router::GenericConnPoolFactory + */ +class UdpGenericConnPoolFactory : public Router::GenericConnPoolFactory { +public: + std::string name() const override { return "envoy.filters.connection_pools.http.udp"; } + std::string category() const override { return "envoy.upstreams"; } + Router::GenericConnPoolPtr + createGenericConnPool(Upstream::ThreadLocalCluster& thread_local_cluster, + Router::GenericConnPoolFactory::UpstreamProtocol upstream_protocol, + const Router::RouteEntry& route_entry, + absl::optional downstream_protocol, + Upstream::LoadBalancerContext* ctx) const override; + ProtobufTypes::MessagePtr createEmptyConfigProto() override { + return std::make_unique(); + } +}; + +DECLARE_FACTORY(UdpGenericConnPoolFactory); + +} // namespace Udp +} // namespace Http +} // namespace Upstreams +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/upstreams/http/udp/upstream_request.cc b/source/extensions/upstreams/http/udp/upstream_request.cc new file mode 100644 index 000000000000..5a11cc0f5ff7 --- /dev/null +++ b/source/extensions/upstreams/http/udp/upstream_request.cc @@ -0,0 +1,159 @@ +#include "source/extensions/upstreams/http/udp/upstream_request.h" + +#include +#include + +#include "envoy/upstream/upstream.h" + +#include "source/common/common/assert.h" +#include "source/common/common/logger.h" +#include "source/common/common/utility.h" +#include "source/common/http/codes.h" +#include "source/common/http/header_map_impl.h" +#include "source/common/http/headers.h" +#include "source/common/http/message_impl.h" +#include "source/common/network/transport_socket_options_impl.h" +#include "source/common/router/router.h" +#include "source/extensions/common/proxy_protocol/proxy_protocol_header.h" + +#include "quiche/common/masque/connect_udp_datagram_payload.h" +#include "quiche/common/simple_buffer_allocator.h" +#include "quiche/quic/core/http/quic_spdy_stream.h" + +namespace Envoy { +namespace Extensions { +namespace Upstreams { +namespace Http { +namespace Udp { + +void UdpConnPool::newStream(Router::GenericConnectionPoolCallbacks* callbacks) { + Router::UpstreamToDownstream& upstream_to_downstream = callbacks->upstreamToDownstream(); + Network::SocketPtr socket = createSocket(host_); + const Network::ConnectionInfoProvider& connection_info_provider = + socket->connectionInfoProvider(); + ASSERT(upstream_to_downstream.connection().has_value()); + Event::Dispatcher& dispatcher = upstream_to_downstream.connection()->dispatcher(); + auto upstream = + std::make_unique(&upstream_to_downstream, std::move(socket), host_, dispatcher); + StreamInfo::StreamInfoImpl stream_info(dispatcher.timeSource(), nullptr); + callbacks->onPoolReady(std::move(upstream), host_, connection_info_provider, stream_info, {}); +} + +UdpUpstream::UdpUpstream(Router::UpstreamToDownstream* upstream_to_downstream, + Network::SocketPtr socket, Upstream::HostConstSharedPtr host, + Event::Dispatcher& dispatcher) + : upstream_to_downstream_(upstream_to_downstream), socket_(std::move(socket)), host_(host), + dispatcher_(dispatcher) { + socket_->ioHandle().initializeFileEvent( + dispatcher_, [this](uint32_t) { onSocketReadReady(); }, Event::PlatformDefaultTriggerType, + Event::FileReadyType::Read); +} + +void UdpUpstream::encodeData(Buffer::Instance& data, bool end_stream) { + for (const Buffer::RawSlice& slice : data.getRawSlices()) { + absl::string_view mem_slice(static_cast(slice.mem_), slice.len_); + if (!capsule_parser_.IngestCapsuleFragment(mem_slice)) { + ENVOY_LOG_MISC(error, "Capsule ingestion error occured: slice = {}", mem_slice); + break; + } + } + if (end_stream) { + capsule_parser_.ErrorIfThereIsRemainingBufferedData(); + } +} + +Envoy::Http::Status UdpUpstream::encodeHeaders(const Envoy::Http::RequestHeaderMap& /*headers*/, + bool end_stream) { + // For successful CONNECT-UDP handshakes, synthesizes the 200 response headers downstream. + Envoy::Http::ResponseHeaderMapPtr response_headers{ + Envoy::Http::createHeaderMap( + {{Envoy::Http::Headers::get().Status, "200"}, + {Envoy::Http::Headers::get().CapsuleProtocol, "?1"}})}; + if (end_stream) { + // If the request header is the end of the stream, responds with 400 Bad Request. Does not + // return an error code to avoid replying with 503 Service Unavailable. + response_headers->setStatus("400"); + response_headers->remove(Envoy::Http::Headers::get().CapsuleProtocol); + upstream_to_downstream_->onResetStream(Envoy::Http::StreamResetReason::ConnectError, ""); + } else { + Api::SysCallIntResult rc = socket_->connect(host_->address()); + if (SOCKET_FAILURE(rc.return_value_)) { + return absl::InternalError("Upstream socket connect failure."); + } + } + // Indicates the end of stream for the subsequent filters in the chain. + upstream_to_downstream_->decodeHeaders(std::move(response_headers), end_stream); + return Envoy::Http::okStatus(); +} + +void UdpUpstream::resetStream() { + upstream_to_downstream_ = nullptr; + socket_->close(); +} + +void UdpUpstream::onSocketReadReady() { + uint32_t packets_dropped = 0; + const Api::IoErrorPtr result = Network::Utility::readPacketsFromSocket( + socket_->ioHandle(), *socket_->connectionInfoProvider().localAddress(), *this, + dispatcher_.timeSource(), /*prefer_gro=*/true, packets_dropped); + if (result == nullptr) { + socket_->ioHandle().activateFileEvents(Event::FileReadyType::Read); + return; + } +} + +// The local and peer addresses are not used in this method since the socket is already bound and +// connected to the upstream server in the encodeHeaders method. +void UdpUpstream::processPacket(Network::Address::InstanceConstSharedPtr /*local_address*/, + Network::Address::InstanceConstSharedPtr /*peer_address*/, + Buffer::InstancePtr buffer, MonotonicTime /*receive_time*/) { + std::string data = buffer->toString(); + quiche::ConnectUdpDatagramUdpPacketPayload payload(data); + quiche::QuicheBuffer serialized_capsule = + SerializeCapsule(quiche::Capsule::Datagram(payload.Serialize()), &capsule_buffer_allocator_); + + Buffer::InstancePtr capsule_data = std::make_unique(); + capsule_data->add(serialized_capsule.AsStringView()); + bytes_meter_->addWireBytesReceived(capsule_data->length()); + upstream_to_downstream_->decodeData(*capsule_data, /*end_stream=*/false); +} + +bool UdpUpstream::OnCapsule(const quiche::Capsule& capsule) { + quiche::CapsuleType capsule_type = capsule.capsule_type(); + if (capsule_type != quiche::CapsuleType::DATAGRAM) { + // Silently drops capsules with an unknown type. + return true; + } + + std::unique_ptr connect_udp_datagram_payload = + quiche::ConnectUdpDatagramPayload::Parse(capsule.datagram_capsule().http_datagram_payload); + if (!connect_udp_datagram_payload) { + // Indicates parsing failure to reset the data stream. + return false; + } + + if (connect_udp_datagram_payload->GetType() != + quiche::ConnectUdpDatagramPayload::Type::kUdpPacket) { + // Silently drops Datagrams with an unknown Context ID. + return true; + } + + Buffer::InstancePtr buffer = std::make_unique(); + buffer->add(connect_udp_datagram_payload->GetUdpProxyingPayload()); + bytes_meter_->addWireBytesSent(buffer->length()); + Api::IoCallUint64Result rc = Network::Utility::writeToSocket( + socket_->ioHandle(), *buffer, /*local_ip=*/nullptr, *host_->address()); + // TODO(https://github.com/envoyproxy/envoy/issues/23564): Handle some socket errors here. + return true; +} + +void UdpUpstream::OnCapsuleParseFailure(absl::string_view error_message) { + upstream_to_downstream_->onResetStream(Envoy::Http::StreamResetReason::ProtocolError, + error_message); +} + +} // namespace Udp +} // namespace Http +} // namespace Upstreams +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/upstreams/http/udp/upstream_request.h b/source/extensions/upstreams/http/udp/upstream_request.h new file mode 100644 index 000000000000..ce363e57ae17 --- /dev/null +++ b/source/extensions/upstreams/http/udp/upstream_request.h @@ -0,0 +1,116 @@ +#pragma once + +#include +#include + +#include "envoy/http/codec.h" +#include "envoy/tcp/conn_pool.h" +#include "envoy/upstream/thread_local_cluster.h" + +#include "source/common/buffer/watermark_buffer.h" +#include "source/common/common/cleanup.h" +#include "source/common/common/logger.h" +#include "source/common/config/well_known_names.h" +#include "source/common/network/utility.h" +#include "source/common/router/upstream_request.h" +#include "source/common/stream_info/stream_info_impl.h" + +#include "quiche/common/simple_buffer_allocator.h" +#include "quiche/quic/core/http/quic_spdy_stream.h" + +namespace Envoy { +namespace Extensions { +namespace Upstreams { +namespace Http { +namespace Udp { + +// Creates a UDP socket for a UDP upstream connection. When a new UDP upstream is requested by the +// UpstreamRequest of Router, creates a UDPUpstream object and hands over the created socket to it. +class UdpConnPool : public Router::GenericConnPool { +public: + UdpConnPool(Upstream::ThreadLocalCluster& thread_local_cluster, + Upstream::LoadBalancerContext* ctx) + : host_(thread_local_cluster.loadBalancer().chooseHost(ctx)) {} + + // Creates a UDPUpstream object for a new stream. + void newStream(Router::GenericConnectionPoolCallbacks* callbacks) override; + + bool cancelAnyPendingStream() override { + // Unlike TCP, UDP Upstreams do not have any pending streams because the upstream connection is + // created immediately without a handshake. + return false; + } + + Upstream::HostDescriptionConstSharedPtr host() const override { return host_; } + + Network::SocketPtr createSocket(const Upstream::HostConstSharedPtr& host) { + return std::make_unique(Network::Socket::Type::Datagram, host->address(), + /*remote_address=*/nullptr, + Network::SocketCreationOptions{}); + } + + bool valid() { return host_ != nullptr; } + +private: + Upstream::HostConstSharedPtr host_; +}; + +// Maintains data relevant to a UDP upstream connection including the socket for the upstream. +// When a CONNECT-UDP request comes in, connects the socket to a node in the upstream cluster. +// Also, adds appropriate header entries to the CONNECT-UDP response. +class UdpUpstream : public Router::GenericUpstream, + public Network::UdpPacketProcessor, + public quiche::CapsuleParser::Visitor { +public: + UdpUpstream(Router::UpstreamToDownstream* upstream_to_downstream, Network::SocketPtr socket, + Upstream::HostConstSharedPtr host, Event::Dispatcher& dispatcher); + + // GenericUpstream + void encodeData(Buffer::Instance& data, bool end_stream) override; + void encodeMetadata(const Envoy::Http::MetadataMapVector&) override {} + Envoy::Http::Status encodeHeaders(const Envoy::Http::RequestHeaderMap&, bool end_stream) override; + void encodeTrailers(const Envoy::Http::RequestTrailerMap&) override {} + void readDisable(bool) override {} + void resetStream() override; + void setAccount(Buffer::BufferMemoryAccountSharedPtr) override {} + const StreamInfo::BytesMeterSharedPtr& bytesMeter() override { return bytes_meter_; } + + // Network::UdpPacketProcessor + // Handles data received from the UDP Upstream. + void processPacket(Network::Address::InstanceConstSharedPtr local_address, + Network::Address::InstanceConstSharedPtr peer_address, + Buffer::InstancePtr buffer, MonotonicTime receive_time) override; + uint64_t maxDatagramSize() const override { return Network::DEFAULT_UDP_MAX_DATAGRAM_SIZE; } + void onDatagramsDropped(uint32_t dropped) override { + // TODO(https://github.com/envoyproxy/envoy/issues/23564): Add statistics for CONNECT-UDP + // upstreams. + ENVOY_LOG_MISC(warn, "{} UDP datagrams were dropped.", dropped); + datagrams_dropped_ += dropped; + } + size_t numPacketsExpectedPerEventLoop() const override { + return Network::MAX_NUM_PACKETS_PER_EVENT_LOOP; + } + uint32_t numOfDroppedDatagrams() { return datagrams_dropped_; } + + // quiche::CapsuleParser::Visitor + bool OnCapsule(const quiche::Capsule& capsule) override; + void OnCapsuleParseFailure(absl::string_view error_message) override; + +private: + void onSocketReadReady(); + + Router::UpstreamToDownstream* upstream_to_downstream_; + const Network::SocketPtr socket_; + Upstream::HostConstSharedPtr host_; + Event::Dispatcher& dispatcher_; + StreamInfo::BytesMeterSharedPtr bytes_meter_{std::make_shared()}; + quiche::CapsuleParser capsule_parser_{this}; + quiche::SimpleBufferAllocator capsule_buffer_allocator_; + uint32_t datagrams_dropped_ = 0; +}; + +} // namespace Udp +} // namespace Http +} // namespace Upstreams +} // namespace Extensions +} // namespace Envoy diff --git a/source/server/api_listener_impl.h b/source/server/api_listener_impl.h index 1f18ffe1efac..8508c852e7b1 100644 --- a/source/server/api_listener_impl.h +++ b/source/server/api_listener_impl.h @@ -122,7 +122,7 @@ class ApiListenerImplBase : public ApiListener, } void close(Network::ConnectionCloseType) override {} void close(Network::ConnectionCloseType, absl::string_view) override {} - Event::Dispatcher& dispatcher() override { return dispatcher_; } + Event::Dispatcher& dispatcher() const override { return dispatcher_; } uint64_t id() const override { return 12345; } void hashKey(std::vector&) const override {} std::string nextProtocol() const override { return EMPTY_STRING; } diff --git a/test/common/http/hcm_router_fuzz_test.cc b/test/common/http/hcm_router_fuzz_test.cc index b21618137d15..cc8a618d62a7 100644 --- a/test/common/http/hcm_router_fuzz_test.cc +++ b/test/common/http/hcm_router_fuzz_test.cc @@ -396,11 +396,13 @@ class FuzzGenericConnPoolFactory : public Router::GenericConnPoolFactory { : cluster_manager_(cluster_manager) {} std::string name() const override { return "envoy.filters.connection_pools.http.generic"; } std::string category() const override { return "envoy.upstreams"; } - Router::GenericConnPoolPtr createGenericConnPool(Upstream::ThreadLocalCluster&, bool is_connect, - const Router::RouteEntry& route_entry, - absl::optional protocol, - Upstream::LoadBalancerContext*) const override { - if (is_connect) { + Router::GenericConnPoolPtr + createGenericConnPool(Upstream::ThreadLocalCluster&, + Router::GenericConnPoolFactory::UpstreamProtocol upstream_protocol, + const Router::RouteEntry& route_entry, + absl::optional protocol, + Upstream::LoadBalancerContext*) const override { + if (upstream_protocol != UpstreamProtocol::HTTP) { return nullptr; } FuzzCluster* cluster = cluster_manager_.selectClusterByName(route_entry.clusterName()); diff --git a/test/common/http/header_utility_test.cc b/test/common/http/header_utility_test.cc index f8ad125c256f..a5e3f92af29b 100644 --- a/test/common/http/header_utility_test.cc +++ b/test/common/http/header_utility_test.cc @@ -1148,6 +1148,15 @@ TEST(HeaderIsValidTest, IsConnect) { EXPECT_FALSE(HeaderUtility::isConnect(Http::TestRequestHeaderMapImpl{})); } +TEST(HeaderIsValidTest, IsConnectUdp) { + EXPECT_TRUE( + HeaderUtility::isConnectUdp(Http::TestRequestHeaderMapImpl{{"upgrade", "connect-udp"}})); + // Extended CONNECT requests should be normalized to HTTP/1.1. + EXPECT_FALSE(HeaderUtility::isConnectUdp( + Http::TestRequestHeaderMapImpl{{":method", "CONNECT"}, {":protocol", "connect-udp"}})); + EXPECT_FALSE(HeaderUtility::isConnectUdp(Http::TestRequestHeaderMapImpl{})); +} + TEST(HeaderIsValidTest, IsConnectResponse) { RequestHeaderMapPtr connect_request{new TestRequestHeaderMapImpl{{":method", "CONNECT"}}}; RequestHeaderMapPtr get_request{new TestRequestHeaderMapImpl{{":method", "GET"}}}; @@ -1160,6 +1169,45 @@ TEST(HeaderIsValidTest, IsConnectResponse) { EXPECT_FALSE(HeaderUtility::isConnectResponse(get_request.get(), success_response)); } +TEST(HeaderIsValidTest, RewriteAuthorityForConnectUdp) { + TestRequestHeaderMapImpl connect_udp_request{ + {":path", "/.well-known/masque/udp/foo.lyft.com/80/"}, {":authority", "example.org"}}; + EXPECT_TRUE(HeaderUtility::rewriteAuthorityForConnectUdp(connect_udp_request)); + EXPECT_EQ(connect_udp_request.getHostValue(), "foo.lyft.com:80"); + + TestRequestHeaderMapImpl connect_udp_request_ipv6{ + {":path", "/.well-known/masque/udp/2001%3A0db8%3A85a3%3A%3A8a2e%3A0370%3A7334/80/"}, + {":authority", "example.org"}}; + EXPECT_TRUE(HeaderUtility::rewriteAuthorityForConnectUdp(connect_udp_request_ipv6)); + EXPECT_EQ(connect_udp_request_ipv6.getHostValue(), "[2001:0db8:85a3::8a2e:0370:7334]:80"); + + TestRequestHeaderMapImpl connect_udp_request_ipv6_not_escaped{ + {":path", "/.well-known/masque/udp/2001:0db8:85a3::8a2e:0370:7334/80"}, + {":authority", "example.org"}}; + EXPECT_FALSE(HeaderUtility::rewriteAuthorityForConnectUdp(connect_udp_request_ipv6_not_escaped)); + + TestRequestHeaderMapImpl connect_udp_request_ipv6_zoneid{ + {":path", "/.well-known/masque/udp/fe80::a%25ee1/80/"}, {":authority", "example.org"}}; + EXPECT_FALSE(HeaderUtility::rewriteAuthorityForConnectUdp(connect_udp_request_ipv6_zoneid)); + + TestRequestHeaderMapImpl connect_udp_malformed1{ + {":path", "/well-known/masque/udp/foo.lyft.com/80/"}}; + EXPECT_FALSE(HeaderUtility::rewriteAuthorityForConnectUdp(connect_udp_malformed1)); + + TestRequestHeaderMapImpl connect_udp_malformed2{{":path", "/masque/udp/foo.lyft.com/80/"}}; + EXPECT_FALSE(HeaderUtility::rewriteAuthorityForConnectUdp(connect_udp_malformed2)); + + TestRequestHeaderMapImpl connect_udp_no_path{{":authority", "example.org"}}; + EXPECT_FALSE(HeaderUtility::rewriteAuthorityForConnectUdp(connect_udp_no_path)); + + TestRequestHeaderMapImpl connect_udp_empty_host{{":path", "/.well-known/masque/udp//80/"}}; + EXPECT_FALSE(HeaderUtility::rewriteAuthorityForConnectUdp(connect_udp_empty_host)); + + TestRequestHeaderMapImpl connect_udp_empty_port{ + {":path", "/.well-known/masque/udp/foo.lyft.com//"}}; + EXPECT_FALSE(HeaderUtility::rewriteAuthorityForConnectUdp(connect_udp_empty_port)); +} + #ifdef ENVOY_ENABLE_HTTP_DATAGRAMS TEST(HeaderIsValidTest, IsCapsuleProtocol) { EXPECT_TRUE( diff --git a/test/common/quic/envoy_quic_client_stream_test.cc b/test/common/quic/envoy_quic_client_stream_test.cc index e548e237909d..ef4caa13d87e 100644 --- a/test/common/quic/envoy_quic_client_stream_test.cc +++ b/test/common/quic/envoy_quic_client_stream_test.cc @@ -134,7 +134,6 @@ class EnvoyQuicClientStreamTest : public testing::Test { #ifdef ENVOY_ENABLE_HTTP_DATAGRAMS void setUpCapsuleProtocol(bool close_send_stream, bool close_recv_stream) { EXPECT_TRUE(quic_session_.OnSetting(quic::SETTINGS_H3_DATAGRAM, 1)); - quic_stream_->useCapsuleProtocol(); // Encodes a CONNECT-UDP request. Http::TestRequestHeaderMapImpl request_headers = { diff --git a/test/common/quic/envoy_quic_server_stream_test.cc b/test/common/quic/envoy_quic_server_stream_test.cc index 7b2f5f9e05ef..d56e63ad0f76 100644 --- a/test/common/quic/envoy_quic_server_stream_test.cc +++ b/test/common/quic/envoy_quic_server_stream_test.cc @@ -118,8 +118,6 @@ class EnvoyQuicServerStreamTest : public testing::Test { #ifdef ENVOY_ENABLE_HTTP_DATAGRAMS void setUpCapsuleProtocol(bool close_send_stream, bool close_recv_stream) { - quic_stream_->useCapsuleProtocol(); - // Decodes a CONNECT-UDP request. EXPECT_CALL(stream_decoder_, decodeHeaders_(_, _)) .WillOnce(Invoke([](const Http::RequestHeaderMapSharedPtr& headers, bool) { diff --git a/test/config/utility.cc b/test/config/utility.cc index 21781be3852e..65525dfead0b 100644 --- a/test/config/utility.cc +++ b/test/config/utility.cc @@ -852,6 +852,28 @@ void ConfigHelper::setConnectConfig( } } +void ConfigHelper::setConnectUdpConfig( + envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& hcm, + bool terminate_connect, bool http3) { + auto* route_config = hcm.mutable_route_config(); + ASSERT_EQ(1, route_config->virtual_hosts_size()); + auto* route = route_config->mutable_virtual_hosts(0)->mutable_routes(0); + auto* match = route->mutable_match(); + match->Clear(); + match->mutable_connect_matcher(); + + if (terminate_connect) { + auto* upgrade = route->mutable_route()->add_upgrade_configs(); + upgrade->set_upgrade_type("connect-udp"); + } + + hcm.add_upgrade_configs()->set_upgrade_type("connect-udp"); + hcm.mutable_http2_protocol_options()->set_allow_connect(true); + if (http3) { + hcm.mutable_http3_protocol_options()->set_allow_extended_connect(true); + } +} + void ConfigHelper::applyConfigModifiers() { for (const auto& config_modifier : config_modifiers_) { config_modifier(bootstrap_); diff --git a/test/config/utility.h b/test/config/utility.h index 800446c20395..ce3cb3c04cd4 100644 --- a/test/config/utility.h +++ b/test/config/utility.h @@ -405,6 +405,10 @@ class ConfigHelper { bool http3 = false, absl::optional proxy_protocol_version = absl::nullopt); + // Given an HCM with the default config, set the matcher to be a connect matcher and enable + // CONNECT-UDP requests. + static void setConnectUdpConfig(HttpConnectionManager& hcm, bool terminate_connect, + bool http3 = false); void setLocalReply( const envoy::extensions::filters::network::http_connection_manager::v3::LocalReplyConfig& diff --git a/test/extensions/upstreams/http/generic/BUILD b/test/extensions/upstreams/http/generic/BUILD new file mode 100644 index 000000000000..17cb97dc79a7 --- /dev/null +++ b/test/extensions/upstreams/http/generic/BUILD @@ -0,0 +1,20 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_test", + "envoy_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_cc_test( + name = "config_test", + srcs = ["config_test.cc"], + deps = [ + "//source/extensions/upstreams/http/generic:config", + "//test/mocks:common_lib", + "//test/mocks/router:router_mocks", + "//test/mocks/upstream:upstream_mocks", + ], +) diff --git a/test/extensions/upstreams/http/generic/config_test.cc b/test/extensions/upstreams/http/generic/config_test.cc new file mode 100644 index 000000000000..3c51f53b782d --- /dev/null +++ b/test/extensions/upstreams/http/generic/config_test.cc @@ -0,0 +1,58 @@ +#include "source/extensions/upstreams/http/generic/config.h" + +#include "test/mocks/router/mocks.h" +#include "test/mocks/upstream/thread_local_cluster.h" +#include "test/test_common/utility.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace Upstreams { +namespace Http { +namespace Generic { + +using ::testing::NiceMock; + +class GenericGenericConnPoolFactoryTest : public ::testing::Test { +public: + GenericGenericConnPoolFactoryTest() = default; + +protected: + NiceMock thread_local_cluster_; + NiceMock route_entry_; + Upstream::HostConstSharedPtr host_; + GenericGenericConnPoolFactory factory_; +}; + +TEST_F(GenericGenericConnPoolFactoryTest, CreateValidHttpConnPool) { + EXPECT_TRUE(factory_.createGenericConnPool(thread_local_cluster_, + Router::GenericConnPoolFactory::UpstreamProtocol::HTTP, + route_entry_, Envoy::Http::Protocol::Http2, nullptr)); +} + +TEST_F(GenericGenericConnPoolFactoryTest, CreateValidTcpConnPool) { + EXPECT_TRUE(factory_.createGenericConnPool(thread_local_cluster_, + Router::GenericConnPoolFactory::UpstreamProtocol::TCP, + route_entry_, Envoy::Http::Protocol::Http2, nullptr)); +} + +TEST_F(GenericGenericConnPoolFactoryTest, CreateValidUdpConnPool) { + EXPECT_TRUE(factory_.createGenericConnPool(thread_local_cluster_, + Router::GenericConnPoolFactory::UpstreamProtocol::UDP, + route_entry_, Envoy::Http::Protocol::Http2, nullptr)); +} + +TEST_F(GenericGenericConnPoolFactoryTest, InvalidConnPool) { + // Passes an invalid UpstreamProtocol and check a nullptr is returned. + EXPECT_FALSE(factory_.createGenericConnPool( + thread_local_cluster_, static_cast(0xff), + route_entry_, Envoy::Http::Protocol::Http2, nullptr)); +} + +} // namespace Generic +} // namespace Http +} // namespace Upstreams +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/upstreams/http/tcp/upstream_request_test.cc b/test/extensions/upstreams/http/tcp/upstream_request_test.cc index aa51aa515cbc..d2745bdc529e 100644 --- a/test/extensions/upstreams/http/tcp/upstream_request_test.cc +++ b/test/extensions/upstreams/http/tcp/upstream_request_test.cc @@ -41,8 +41,7 @@ class TcpConnPoolTest : public ::testing::Test { cm.initializeThreadLocalClusters({"fake_cluster"}); EXPECT_CALL(cm.thread_local_cluster_, tcpConnPool(_, _)) .WillOnce(Return(Upstream::TcpPoolData([]() {}, &mock_pool_))); - conn_pool_ = std::make_unique(cm.thread_local_cluster_, true, route_entry, - Envoy::Http::Protocol::Http11, nullptr); + conn_pool_ = std::make_unique(cm.thread_local_cluster_, route_entry, nullptr); } std::unique_ptr conn_pool_; diff --git a/test/extensions/upstreams/http/udp/BUILD b/test/extensions/upstreams/http/udp/BUILD new file mode 100644 index 000000000000..e94d6b501009 --- /dev/null +++ b/test/extensions/upstreams/http/udp/BUILD @@ -0,0 +1,45 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_test", + "envoy_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_cc_test( + name = "upstream_request_test", + srcs = ["upstream_request_test.cc"], + deps = [ + "//source/common/buffer:buffer_lib", + "//source/common/network:address_lib", + "//source/common/router:router_lib", + "//source/common/upstream:upstream_includes", + "//source/common/upstream:upstream_lib", + "//source/extensions/upstreams/http/udp:upstream_request_lib", + "//test/common/http:common_lib", + "//test/mocks:common_lib", + "//test/mocks/network:network_mocks", + "//test/mocks/router:router_filter_interface", + "//test/mocks/router:router_mocks", + "//test/mocks/server:factory_context_mocks", + "//test/mocks/server:instance_mocks", + "//test/mocks/upstream:upstream_mocks", + "//test/test_common:environment_lib", + "//test/test_common:simulated_time_system_lib", + "//test/test_common:utility_lib", + ], +) + +envoy_cc_test( + name = "config_test", + srcs = ["config_test.cc"], + deps = [ + "//source/extensions/upstreams/http/udp:config", + "//source/extensions/upstreams/http/udp:upstream_request_lib", + "//test/mocks:common_lib", + "//test/mocks/router:router_mocks", + "//test/mocks/upstream:upstream_mocks", + ], +) diff --git a/test/extensions/upstreams/http/udp/config_test.cc b/test/extensions/upstreams/http/udp/config_test.cc new file mode 100644 index 000000000000..258940e111bf --- /dev/null +++ b/test/extensions/upstreams/http/udp/config_test.cc @@ -0,0 +1,49 @@ +#include "source/extensions/upstreams/http/udp/config.h" + +#include "test/mocks/router/mocks.h" +#include "test/mocks/upstream/thread_local_cluster.h" +#include "test/test_common/utility.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace Upstreams { +namespace Http { +namespace Udp { + +using ::testing::NiceMock; +using ::testing::Return; + +class UdpGenericConnPoolFactoryTest : public ::testing::Test { +public: + UdpGenericConnPoolFactoryTest() = default; + +protected: + NiceMock thread_local_cluster_; + NiceMock route_entry_; + Upstream::HostConstSharedPtr host_; + UdpGenericConnPoolFactory factory_; +}; + +TEST_F(UdpGenericConnPoolFactoryTest, CreateValidUdpConnPool) { + auto host = std::make_shared(); + EXPECT_CALL(thread_local_cluster_.lb_, chooseHost).WillOnce(Return(host)); + EXPECT_TRUE(factory_.createGenericConnPool(thread_local_cluster_, + Router::GenericConnPoolFactory::UpstreamProtocol::UDP, + route_entry_, Envoy::Http::Protocol::Http2, nullptr)); +} + +TEST_F(UdpGenericConnPoolFactoryTest, CreateInvalidUdpConnPool) { + EXPECT_CALL(thread_local_cluster_.lb_, chooseHost).WillOnce(Return(nullptr)); + EXPECT_FALSE(factory_.createGenericConnPool(thread_local_cluster_, + Router::GenericConnPoolFactory::UpstreamProtocol::UDP, + route_entry_, Envoy::Http::Protocol::Http2, nullptr)); +} + +} // namespace Udp +} // namespace Http +} // namespace Upstreams +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/upstreams/http/udp/upstream_request_test.cc b/test/extensions/upstreams/http/udp/upstream_request_test.cc new file mode 100644 index 000000000000..3f89801321e2 --- /dev/null +++ b/test/extensions/upstreams/http/udp/upstream_request_test.cc @@ -0,0 +1,192 @@ +#include "envoy/http/header_map.h" + +#include "source/common/buffer/buffer_impl.h" +#include "source/common/network/address_impl.h" +#include "source/common/network/utility.h" +#include "source/common/router/config_impl.h" +#include "source/common/router/router.h" +#include "source/common/router/upstream_codec_filter.h" +#include "source/common/router/upstream_request.h" +#include "source/extensions/common/proxy_protocol/proxy_protocol_header.h" +#include "source/extensions/upstreams/http/udp/upstream_request.h" + +#include "test/common/http/common.h" +#include "test/mocks/common.h" +#include "test/mocks/router/mocks.h" +#include "test/mocks/router/router_filter_interface.h" +#include "test/mocks/server/factory_context.h" +#include "test/mocks/server/instance.h" +#include "test/test_common/utility.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace Upstreams { +namespace Http { +namespace Udp { + +using ::testing::NiceMock; +using ::testing::Return; + +class UdpUpstreamTest : public ::testing::Test { +public: + UdpUpstreamTest() { + auto mock_socket = std::make_unique>(); + mock_socket_ = mock_socket.get(); + EXPECT_CALL(*mock_socket_->io_handle_, createFileEvent_); + auto mock_host = std::make_shared>(); + mock_host_ = mock_host.get(); + ON_CALL(*mock_host_, address) + .WillByDefault( + Return(Network::Utility::parseInternetAddressAndPortNoThrow("127.0.0.1:80", false))); + udp_upstream_ = + std::make_unique(&mock_upstream_to_downstream_, std::move(mock_socket), + std::move(mock_host), mock_dispatcher_); + } + +protected: + ::Envoy::Http::TestRequestHeaderMapImpl connect_udp_headers_{ + {":path", "/.well-known/masque/udp/foo.lyft.com/80/"}, + {"upgrade", "connect-udp"}, + {"connection", "upgrade"}, + {":authority", "example.org"}}; + + NiceMock mock_upstream_to_downstream_; + NiceMock* mock_socket_; + NiceMock mock_dispatcher_; + NiceMock* mock_host_; + std::unique_ptr udp_upstream_; +}; + +TEST_F(UdpUpstreamTest, ExchangeCapsules) { + // Swallow the request headers and generate response headers. + EXPECT_CALL(mock_upstream_to_downstream_, decodeHeaders) + .WillOnce([](Envoy::Http::ResponseHeaderMapPtr&& headers, bool end_stream) { + EXPECT_EQ(headers->getStatusValue(), "200"); + EXPECT_FALSE(end_stream); + }); + EXPECT_TRUE(udp_upstream_->encodeHeaders(connect_udp_headers_, false).ok()); + + // Swallow read disable. + udp_upstream_->readDisable(false); + + // Sends a capsule to upstream. + const std::string sent_capsule_fragment = + absl::HexStringToBytes("00" // DATAGRAM Capsule Type + "08" // Capsule Length + "00" // Context ID + "a1a2a3a4a5a6a7" // UDP Proxying Payload + ); + Buffer::OwnedImpl sent_capsule(sent_capsule_fragment); + EXPECT_CALL(*mock_socket_->io_handle_, sendmsg(_, _, _, _, _)) + .WillOnce([](const Buffer::RawSlice* slices, uint64_t num_slice, int /*flags*/, + const Network::Address::Ip* /*self_ip*/, + const Network::Address::Instance& /*peer_address*/) { + Buffer::OwnedImpl buffer(absl::HexStringToBytes("a1a2a3a4a5a6a7")); + EXPECT_TRUE(TestUtility::rawSlicesEqual(buffer.getRawSlices().data(), slices, num_slice)); + return Api::ioCallUint64ResultNoError(); + }); + udp_upstream_->encodeData(sent_capsule, false); + + // Receives data from upstream and converts it to capsule. + const std::string decoded_capsule_fragment = + absl::HexStringToBytes("00" // DATAGRAM Capsule Type + "08" // Capsule Length + "00" // Context ID + "b1b2b3b4b5b6b7" // UDP Proxying Payload + ); + Buffer::InstancePtr received_data = + std::make_unique(absl::HexStringToBytes("b1b2b3b4b5b6b7")); + EXPECT_CALL(mock_upstream_to_downstream_, + decodeData(BufferStringEqual(decoded_capsule_fragment), false)); + Envoy::MonotonicTime timestamp; + udp_upstream_->processPacket(nullptr, nullptr, std::move(received_data), timestamp); +} + +TEST_F(UdpUpstreamTest, HeaderOnlyRequest) { + EXPECT_CALL(mock_upstream_to_downstream_, decodeHeaders) + .WillOnce([](Envoy::Http::ResponseHeaderMapPtr&& headers, bool end_stream) { + EXPECT_EQ(headers->getStatusValue(), "400"); + EXPECT_TRUE(end_stream); + }); + EXPECT_TRUE(udp_upstream_->encodeHeaders(connect_udp_headers_, true).ok()); +} + +TEST_F(UdpUpstreamTest, SwallowMetadata) { + Envoy::Http::MetadataMapVector metadata_map_vector; + udp_upstream_->encodeMetadata(metadata_map_vector); + EXPECT_CALL(*mock_socket_->io_handle_, sendmsg).Times(0); +} + +TEST_F(UdpUpstreamTest, SwallowTrailers) { + Envoy::Http::TestRequestTrailerMapImpl trailers{{"foo", "bar"}}; + udp_upstream_->encodeTrailers(trailers); + EXPECT_CALL(*mock_socket_->io_handle_, sendmsg).Times(0); +} + +TEST_F(UdpUpstreamTest, DatagramsDropped) { + udp_upstream_->onDatagramsDropped(1); + EXPECT_EQ(udp_upstream_->numOfDroppedDatagrams(), 1); + udp_upstream_->onDatagramsDropped(3); + EXPECT_EQ(udp_upstream_->numOfDroppedDatagrams(), 4); +} + +TEST_F(UdpUpstreamTest, InvalidCapsule) { + EXPECT_TRUE(udp_upstream_->encodeHeaders(connect_udp_headers_, false).ok()); + // Sends an invalid capsule. + const std::string invalid_capsule_fragment = + absl::HexStringToBytes("0x1eca6a00" // DATAGRAM Capsule Type + "01" // Capsule Length + "c0" // Invalid VarInt62 + ); + Buffer::OwnedImpl invalid_capsule(invalid_capsule_fragment); + EXPECT_CALL(mock_upstream_to_downstream_, onResetStream); + udp_upstream_->encodeData(invalid_capsule, true); +} + +TEST_F(UdpUpstreamTest, MalformedContextIdDatagram) { + EXPECT_TRUE(udp_upstream_->encodeHeaders(connect_udp_headers_, false).ok()); + // Sends a capsule with an invalid variable length integer. + const std::string invalid_context_id_fragment = + absl::HexStringToBytes("00" // DATAGRAM Capsule Type + "01" // Capsule Length + "c0" // Context ID (Invalid VarInt62) + ); + Buffer::OwnedImpl invalid_context_id_capsule(invalid_context_id_fragment); + EXPECT_CALL(mock_upstream_to_downstream_, onResetStream); + udp_upstream_->encodeData(invalid_context_id_capsule, true); +} + +TEST_F(UdpUpstreamTest, RemainingDataWhenStreamEnded) { + EXPECT_CALL(mock_upstream_to_downstream_, decodeHeaders) + .WillOnce([](Envoy::Http::ResponseHeaderMapPtr&& headers, bool end_stream) { + EXPECT_EQ(headers->getStatusValue(), "200"); + EXPECT_FALSE(end_stream); + }); + EXPECT_TRUE(udp_upstream_->encodeHeaders(connect_udp_headers_, false).ok()); + + // Sends a capsule to upstream with a large length value. + const std::string sent_capsule_fragment = + absl::HexStringToBytes("00" // DATAGRAM Capsule Type + "ff" // Capsule Length + "00" // Context ID + "a1a2a3a4a5a6a7" // UDP Proxying Payload + ); + Buffer::OwnedImpl sent_capsule(sent_capsule_fragment); + EXPECT_CALL(mock_upstream_to_downstream_, onResetStream); + udp_upstream_->encodeData(sent_capsule, true); +} + +TEST_F(UdpUpstreamTest, SocketConnectError) { + EXPECT_CALL(mock_upstream_to_downstream_, decodeHeaders).Times(0); + EXPECT_CALL(*mock_socket_, connect(_)).WillOnce(Return(Api::SysCallIntResult{-1, EADDRINUSE})); + EXPECT_FALSE(udp_upstream_->encodeHeaders(connect_udp_headers_, false).ok()); +} + +} // namespace Udp +} // namespace Http +} // namespace Upstreams +} // namespace Extensions +} // namespace Envoy diff --git a/test/integration/BUILD b/test/integration/BUILD index 2f53109884c5..451b949851b4 100644 --- a/test/integration/BUILD +++ b/test/integration/BUILD @@ -1739,6 +1739,27 @@ envoy_cc_test( ], ) +envoy_cc_test( + name = "udp_tunneling_integration_test", + size = "large", + srcs = [ + "udp_tunneling_integration_test.cc", + ], + data = [ + "//test/config/integration/certs", + ], + shard_count = 16, + deps = [ + ":http_integration_lib", + ":http_protocol_integration_lib", + "//source/extensions/upstreams/http/udp:config", + "@envoy_api//envoy/config/bootstrap/v3:pkg_cc_proto", + "@envoy_api//envoy/config/core/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/access_loggers/file/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/upstreams/http/udp/v3:pkg_cc_proto", + ], +) + envoy_cc_test( name = "tcp_conn_pool_integration_test", size = "large", diff --git a/test/integration/udp_tunneling_integration_test.cc b/test/integration/udp_tunneling_integration_test.cc new file mode 100644 index 000000000000..44e240ec45ae --- /dev/null +++ b/test/integration/udp_tunneling_integration_test.cc @@ -0,0 +1,251 @@ +#include + +#include "envoy/config/bootstrap/v3/bootstrap.pb.h" +#include "envoy/config/core/v3/proxy_protocol.pb.h" +#include "envoy/extensions/access_loggers/file/v3/file.pb.h" +#include "envoy/extensions/upstreams/http/udp/v3/udp_connection_pool.pb.h" + +#include "test/integration/http_integration.h" +#include "test/integration/http_protocol_integration.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace { + +// Terminates CONNECT-UDP and sends raw UDP datagrams upstream. +class ConnectUdpTerminationIntegrationTest : public HttpProtocolIntegrationTest { +public: + ConnectUdpTerminationIntegrationTest() = default; + + ~ConnectUdpTerminationIntegrationTest() override { + // Since the upstream is a UDP server, there is nothing to check on the upstream side. Simply + // make sure that the connection is closed to avoid TSAN error. + if (codec_client_) { + codec_client_->close(); + } + } + + void initialize() override { + config_helper_.addConfigModifier( + [&](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& + hcm) { + hcm.mutable_delayed_close_timeout()->set_seconds(1); + if (enable_timeout_) { + hcm.mutable_stream_idle_timeout()->set_seconds(0); + hcm.mutable_stream_idle_timeout()->set_nanos(200 * 1000 * 1000); + } + if (!host_to_match_.empty()) { + auto* route_config = hcm.mutable_route_config(); + ASSERT_EQ(1, route_config->virtual_hosts_size()); + route_config->mutable_virtual_hosts(0)->clear_domains(); + route_config->mutable_virtual_hosts(0)->add_domains(host_to_match_); + } + ConfigHelper::setConnectUdpConfig(hcm, true, + downstream_protocol_ == Http::CodecType::HTTP3); + }); + setUdpFakeUpstream(FakeUpstreamConfig::UdpConfig()); + HttpIntegrationTest::initialize(); + } + + void setUpConnection() { + codec_client_ = makeHttpConnection(lookupPort("http")); + auto encoder_decoder = codec_client_->startRequest(connect_udp_headers_); + request_encoder_ = &encoder_decoder.first; + response_ = std::move(encoder_decoder.second); + response_->waitForHeaders(); + } + + void sendBidirectionalData(const std::string downstream_send_data = "hello", + const std::string upstream_received_data = "hello", + const std::string upstream_send_data = "there!", + const std::string downstream_received_data = "there!") { + // Send some data upstream. + codec_client_->sendData(*request_encoder_, downstream_send_data, false); + Network::UdpRecvData request_datagram; + ASSERT_TRUE(fake_upstreams_[0]->waitForUdpDatagram(request_datagram)); + EXPECT_EQ(upstream_received_data, request_datagram.buffer_->toString()); + + // Send some data downstream. + fake_upstreams_[0]->sendUdpDatagram(upstream_send_data, request_datagram.addresses_.peer_); + response_->waitForBodyData(downstream_received_data.length()); + EXPECT_EQ(downstream_received_data, response_->body()); + } + + void exchangeValidCapsules() { + const std::string sent_capsule_fragment = + absl::HexStringToBytes("00" // DATAGRAM Capsule Type + "08" // Capsule Length + "00" // Context ID + "a1a2a3a4a5a6a7" // UDP Proxying Payload + ); + const std::string received_capsule_fragment = + absl::HexStringToBytes("00" // DATAGRAM Capsule Type + "08" // Capsule Length + "00" // Context ID + "b1b2b3b4b5b6b7" // UDP Proxying Payload + ); + + sendBidirectionalData(sent_capsule_fragment, absl::HexStringToBytes("a1a2a3a4a5a6a7"), + absl::HexStringToBytes("b1b2b3b4b5b6b7"), received_capsule_fragment); + } + + // The Envoy HTTP/2 and HTTP/3 clients expect the request header map to be in the form of HTTP/1 + // upgrade to issue an extended CONNECT request. + Http::TestRequestHeaderMapImpl connect_udp_headers_{ + {":method", "GET"}, {":path", "/.well-known/masque/udp/foo.lyft.com/80/"}, + {"upgrade", "connect-udp"}, {"connection", "upgrade"}, + {":scheme", "https"}, {":authority", "example.org"}, + {"capsule-protocol", "?1"}}; + + IntegrationStreamDecoderPtr response_; + bool enable_timeout_{}; + std::string host_to_match_{}; +}; + +TEST_P(ConnectUdpTerminationIntegrationTest, ExchangeCapsules) { + initialize(); + setUpConnection(); + exchangeValidCapsules(); +} + +TEST_P(ConnectUdpTerminationIntegrationTest, ExchangeCapsulesWithHostMatch) { + host_to_match_ = "foo.lyft.com:80"; + initialize(); + setUpConnection(); + exchangeValidCapsules(); +} + +TEST_P(ConnectUdpTerminationIntegrationTest, IncorrectHostMatch) { + host_to_match_ = "foo.lyft.com:80"; + connect_udp_headers_.setPath("/.well-known/masque/udp/bar.lyft.com/80/"); + initialize(); + setUpConnection(); + EXPECT_EQ("404", response_->headers().getStatusValue()); +} + +TEST_P(ConnectUdpTerminationIntegrationTest, IncorrectPortMatch) { + host_to_match_ = "foo.lyft.com:80"; + connect_udp_headers_.setPath("/.well-known/masque/udp/foo.lyft.com/8080/"); + initialize(); + setUpConnection(); + EXPECT_EQ("404", response_->headers().getStatusValue()); +} + +TEST_P(ConnectUdpTerminationIntegrationTest, IPv4HostMatch) { + host_to_match_ = "179.0.112.43:80"; + connect_udp_headers_.setPath("/.well-known/masque/udp/179.0.112.43/80/"); + initialize(); + setUpConnection(); +} + +TEST_P(ConnectUdpTerminationIntegrationTest, IPv6HostMatch) { + host_to_match_ = "[2001:0db8:85a3::8a2e:0370:7334]:80"; + connect_udp_headers_.setPath("/.well-known/masque/udp/2001:0db8:85a3::8a2e:0370:7334/80/"); + initialize(); + setUpConnection(); +} + +TEST_P(ConnectUdpTerminationIntegrationTest, IPv6WithZoneIdHostMatch) { + host_to_match_ = "[fe80::a%ee1]:80"; + connect_udp_headers_.setPath("/.well-known/masque/udp/fe80::a%25ee1/80/"); + initialize(); + setUpConnection(); + EXPECT_EQ("404", response_->headers().getStatusValue()); +} + +TEST_P(ConnectUdpTerminationIntegrationTest, ExchangeCapsulesWithoutCapsuleProtocolHeader) { + initialize(); + connect_udp_headers_.remove(Envoy::Http::Headers::get().CapsuleProtocol); + setUpConnection(); + exchangeValidCapsules(); +} + +TEST_P(ConnectUdpTerminationIntegrationTest, StreamIdleTimeout) { + enable_timeout_ = true; + initialize(); + setUpConnection(); + + // Wait for the timeout to close the connection. + ASSERT_TRUE(response_->waitForReset()); +} + +TEST_P(ConnectUdpTerminationIntegrationTest, MaxStreamDuration) { + setUpstreamProtocol(upstreamProtocol()); + config_helper_.addConfigModifier([](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + ConfigHelper::HttpProtocolOptions protocol_options; + protocol_options.mutable_common_http_protocol_options() + ->mutable_max_stream_duration() + ->MergeFrom(ProtobufUtil::TimeUtil::MillisecondsToDuration(1000)); + ConfigHelper::setProtocolOptions(*bootstrap.mutable_static_resources()->mutable_clusters(0), + protocol_options); + }); + + initialize(); + setUpConnection(); + exchangeValidCapsules(); + + test_server_->waitForCounterGe("cluster.cluster_0.upstream_rq_max_duration_reached", 1); + + if (downstream_protocol_ == Http::CodecType::HTTP1) { + ASSERT_TRUE(codec_client_->waitForDisconnect()); + } else { + ASSERT_TRUE(response_->waitForReset()); + } +} + +TEST_P(ConnectUdpTerminationIntegrationTest, PathWithInvalidUriTemplate) { + initialize(); + connect_udp_headers_.setPath("/masque/udp/foo.lyft.com/80/"); + setUpConnection(); + EXPECT_EQ("404", response_->headers().getStatusValue()); +} + +TEST_P(ConnectUdpTerminationIntegrationTest, PathWithEmptyHost) { + initialize(); + connect_udp_headers_.setPath("/.well-known/masque/udp//80/"); + setUpConnection(); + EXPECT_EQ("404", response_->headers().getStatusValue()); +} + +TEST_P(ConnectUdpTerminationIntegrationTest, PathWithEmptyPort) { + initialize(); + connect_udp_headers_.setPath("/.well-known/masque/udp/foo.lyft.com//"); + setUpConnection(); + EXPECT_EQ("404", response_->headers().getStatusValue()); +} + +TEST_P(ConnectUdpTerminationIntegrationTest, DropUnknownCapsules) { + initialize(); + setUpConnection(); + Network::UdpRecvData request_datagram; + const std::string unknown_capsule_fragment = + absl::HexStringToBytes("01" // DATAGRAM Capsule Type + "08" // Capsule Length + "00" // Context ID + "a1a2a3a4a5a6a7" // UDP Proxying Payload + ); + codec_client_->sendData(*request_encoder_, unknown_capsule_fragment, false); + ASSERT_FALSE( + fake_upstreams_[0]->waitForUdpDatagram(request_datagram, std::chrono::milliseconds(1))); + + const std::string unknown_context_id = + absl::HexStringToBytes("00" // DATAGRAM Capsule Type + "08" // Capsule Length + "01" // Context ID + "a1a2a3a4a5a6a7" // UDP Proxying Payload + ); + codec_client_->sendData(*request_encoder_, unknown_context_id, false); + ASSERT_FALSE( + fake_upstreams_[0]->waitForUdpDatagram(request_datagram, std::chrono::milliseconds(1))); +} + +INSTANTIATE_TEST_SUITE_P(Protocols, ConnectUdpTerminationIntegrationTest, + testing::ValuesIn(HttpProtocolIntegrationTest::getProtocolTestParams( + {Http::CodecType::HTTP1, Http::CodecType::HTTP2, + Http::CodecType::HTTP3}, + {Http::CodecType::HTTP1})), + HttpProtocolIntegrationTest::protocolTestParamsToString); + +} // namespace +} // namespace Envoy diff --git a/test/integration/upstreams/per_host_upstream_config.h b/test/integration/upstreams/per_host_upstream_config.h index 829d4b871bae..c19569ac1c5f 100644 --- a/test/integration/upstreams/per_host_upstream_config.h +++ b/test/integration/upstreams/per_host_upstream_config.h @@ -70,11 +70,11 @@ class PerHostHttpUpstream : public Extensions::Upstreams::Http::Http::HttpUpstre class PerHostHttpConnPool : public Extensions::Upstreams::Http::Http::HttpConnPool { public: - PerHostHttpConnPool(Upstream::ThreadLocalCluster& thread_local_cluster, bool is_connect, + PerHostHttpConnPool(Upstream::ThreadLocalCluster& thread_local_cluster, const Router::RouteEntry& route_entry, absl::optional downstream_protocol, Upstream::LoadBalancerContext* ctx) - : HttpConnPool(thread_local_cluster, is_connect, route_entry, downstream_protocol, ctx) {} + : HttpConnPool(thread_local_cluster, route_entry, downstream_protocol, ctx) {} void onPoolReady(Envoy::Http::RequestEncoder& callbacks_encoder, Upstream::HostDescriptionConstSharedPtr host, StreamInfo::StreamInfo& info, @@ -95,16 +95,17 @@ class PerHostGenericConnPoolFactory : public Router::GenericConnPoolFactory { std::string name() const override { return "envoy.filters.connection_pools.http.per_host"; } std::string category() const override { return "envoy.upstreams"; } Router::GenericConnPoolPtr - createGenericConnPool(Upstream::ThreadLocalCluster& thread_local_cluster, bool is_connect, + createGenericConnPool(Upstream::ThreadLocalCluster& thread_local_cluster, + Router::GenericConnPoolFactory::UpstreamProtocol upstream_protocol, const Router::RouteEntry& route_entry, absl::optional downstream_protocol, Upstream::LoadBalancerContext* ctx) const override { - if (is_connect) { - // This example factory doesn't support terminating CONNECT stream. + if (upstream_protocol != UpstreamProtocol::HTTP) { + // This example factory doesn't support terminating CONNECT/CONNECT-UDP stream. return nullptr; } auto upstream_http_conn_pool = std::make_unique( - thread_local_cluster, is_connect, route_entry, downstream_protocol, ctx); + thread_local_cluster, route_entry, downstream_protocol, ctx); return (upstream_http_conn_pool->valid() ? std::move(upstream_http_conn_pool) : nullptr); } diff --git a/test/mocks/network/connection.h b/test/mocks/network/connection.h index 93213544a47c..99ecdd17a207 100644 --- a/test/mocks/network/connection.h +++ b/test/mocks/network/connection.h @@ -58,7 +58,7 @@ class MockConnectionBase { MOCK_METHOD(bool, isHalfCloseEnabled, (), (const)); \ MOCK_METHOD(void, close, (ConnectionCloseType type)); \ MOCK_METHOD(void, close, (ConnectionCloseType type, absl::string_view details)); \ - MOCK_METHOD(Event::Dispatcher&, dispatcher, ()); \ + MOCK_METHOD(Event::Dispatcher&, dispatcher, (), (const)); \ MOCK_METHOD(uint64_t, id, (), (const)); \ MOCK_METHOD(void, hashKey, (std::vector&), (const)); \ MOCK_METHOD(bool, initializeReadFilters, ()); \ diff --git a/test/per_file_coverage.sh b/test/per_file_coverage.sh index a9c1113fcc2f..da38e06f8c44 100755 --- a/test/per_file_coverage.sh +++ b/test/per_file_coverage.sh @@ -62,6 +62,7 @@ declare -a KNOWN_LOW_COVERAGE=( "source/extensions/transport_sockets/tls:95.0" "source/extensions/transport_sockets/tls/cert_validator:95.2" "source/extensions/transport_sockets/tls/private_key:88.9" +"source/extensions/upstreams/http/generic:85.0" # Braces in switch statements are considered uncovered "source/extensions/wasm_runtime/wamr:0.0" # Not enabled in coverage build "source/extensions/wasm_runtime/wasmtime:0.0" # Not enabled in coverage build "source/extensions/wasm_runtime/wavm:0.0" # Not enabled in coverage build