diff --git a/api/envoy/extensions/filters/network/tcp_proxy/v3/tcp_proxy.proto b/api/envoy/extensions/filters/network/tcp_proxy/v3/tcp_proxy.proto index 5be0bcd853da..f3c509fe11b4 100644 --- a/api/envoy/extensions/filters/network/tcp_proxy/v3/tcp_proxy.proto +++ b/api/envoy/extensions/filters/network/tcp_proxy/v3/tcp_proxy.proto @@ -69,6 +69,22 @@ message TcpProxy { "envoy.config.filter.network.tcp_proxy.v2.TcpProxy.TunnelingConfig"; // The hostname to send in the synthesized CONNECT headers to the upstream proxy. + // This field evaluates command operators if set, otherwise returns hostname as is. + // + // Example: dynamically set hostname using downstream SNI + // + // .. code-block:: yaml + // + // tunneling_config: + // hostname: "%REQUESTED_SERVER_NAME%:443" + // + // Example: dynamically set hostname using dynamic metadata + // + // .. code-block: yaml + // + // tunneling_config: + // hostname: "%DYNAMIC_METADATA(tunnel:address)%" + // string hostname = 1 [(validate.rules).string = {min_len: 1}]; // Use POST method instead of CONNECT method to tunnel the TCP stream. diff --git a/changelogs/current.yaml b/changelogs/current.yaml index e2567e8e9424..37ce7815218b 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -32,6 +32,9 @@ behavior_changes: Fixed metric tag extraction so that :ref:`stat_prefix <envoy_v3_api_field_extensions.filters.network.redis_proxy.v3.RedisProxy.stat_prefix>` is properly extracted. This changes the Prometheus name from envoy_redis_myprefix_command_pttl_latency_sum{} to envoy_redis_command_pttl_latency_sum{envoy_redis_prefix="myprefix"}. +- area: tcp_proxy + change: | + added support for command operators in :ref:`TunnelingConfig hostname <envoy_v3_api_field_extensions.filters.network.tcp_proxy.v3.TcpProxy.TunnelingConfig.hostname>` to dynamically set upstream hostname. minor_behavior_changes: - area: thrift diff --git a/envoy/tcp/upstream.h b/envoy/tcp/upstream.h index 80c385e96aa5..be6170c8ae7d 100644 --- a/envoy/tcp/upstream.h +++ b/envoy/tcp/upstream.h @@ -25,8 +25,10 @@ class GenericUpstream; class TunnelingConfigHelper { public: virtual ~TunnelingConfigHelper() = default; + // The host name of the tunneling upstream HTTP request. - virtual const std::string& hostname() const PURE; + // This function evaluates command operators if specified. Otherwise it returns host name as is. + virtual std::string host(const StreamInfo::StreamInfo& stream_info) const PURE; // The method of the upstream HTTP request. True if using POST method, CONNECT otherwise. virtual bool usePost() const PURE; diff --git a/source/common/tcp_proxy/BUILD b/source/common/tcp_proxy/BUILD index bd2a496c4eef..01743ff3082b 100644 --- a/source/common/tcp_proxy/BUILD +++ b/source/common/tcp_proxy/BUILD @@ -60,6 +60,7 @@ envoy_cc_library( "//source/common/common:empty_string", "//source/common/common:macros", "//source/common/common:minimal_logger_lib", + "//source/common/formatter:substitution_format_string_lib", "//source/common/http:codec_client_lib", "//source/common/network:application_protocol_lib", "//source/common/network:cidr_range_lib", @@ -71,6 +72,7 @@ envoy_cc_library( "//source/common/network:upstream_server_name_lib", "//source/common/network:upstream_socket_options_filter_state_lib", "//source/common/network:utility_lib", + "//source/common/protobuf:utility_lib", "//source/common/router:metadatamatchcriteria_lib", "//source/common/stream_info:stream_info_lib", "//source/common/upstream:load_balancer_lib", diff --git a/source/common/tcp_proxy/tcp_proxy.cc b/source/common/tcp_proxy/tcp_proxy.cc index b388c17c2096..9bcff5c13123 100644 --- a/source/common/tcp_proxy/tcp_proxy.cc +++ b/source/common/tcp_proxy/tcp_proxy.cc @@ -9,6 +9,7 @@ #include "envoy/event/dispatcher.h" #include "envoy/event/timer.h" #include "envoy/extensions/filters/network/tcp_proxy/v3/tcp_proxy.pb.h" +#include "envoy/extensions/filters/network/tcp_proxy/v3/tcp_proxy.pb.validate.h" #include "envoy/stats/scope.h" #include "envoy/upstream/cluster_manager.h" #include "envoy/upstream/upstream.h" @@ -78,7 +79,7 @@ Config::SharedConfig::SharedConfig( } if (config.has_tunneling_config()) { tunneling_config_helper_ = - std::make_unique<TunnelingConfigHelperImpl>(config.tunneling_config()); + std::make_unique<TunnelingConfigHelperImpl>(config.tunneling_config(), context); } if (config.has_max_downstream_connection_duration()) { const uint64_t connection_duration = @@ -539,6 +540,26 @@ const Router::MetadataMatchCriteria* Filter::metadataMatchCriteria() { } } +TunnelingConfigHelperImpl::TunnelingConfigHelperImpl( + const envoy::extensions::filters::network::tcp_proxy::v3::TcpProxy_TunnelingConfig& + config_message, + Server::Configuration::FactoryContext& context) + : use_post_(config_message.use_post()), + header_parser_(Envoy::Router::HeaderParser::configure(config_message.headers_to_add())) { + envoy::config::core::v3::SubstitutionFormatString substitution_format_config; + substitution_format_config.mutable_text_format_source()->set_inline_string( + config_message.hostname()); + hostname_fmt_ = Formatter::SubstitutionFormatStringUtils::fromProtoConfig( + substitution_format_config, context); +} + +std::string TunnelingConfigHelperImpl::host(const StreamInfo::StreamInfo& stream_info) const { + return hostname_fmt_->format(*Http::StaticEmptyHeaders::get().request_headers, + *Http::StaticEmptyHeaders::get().response_headers, + *Http::StaticEmptyHeaders::get().response_trailers, stream_info, + absl::string_view()); +} + void Filter::onConnectTimeout() { ENVOY_CONN_LOG(debug, "connect timeout", read_callbacks_->connection()); read_callbacks_->upstreamHost()->outlierDetector().putResult( diff --git a/source/common/tcp_proxy/tcp_proxy.h b/source/common/tcp_proxy/tcp_proxy.h index 74406a610c09..48d4bc55ae0f 100644 --- a/source/common/tcp_proxy/tcp_proxy.h +++ b/source/common/tcp_proxy/tcp_proxy.h @@ -22,6 +22,8 @@ #include "envoy/upstream/upstream.h" #include "source/common/common/logger.h" +#include "source/common/formatter/substitution_format_string.h" +#include "source/common/http/header_map_impl.h" #include "source/common/network/cidr_range.h" #include "source/common/network/filter_impl.h" #include "source/common/network/hash_policy.h" @@ -114,17 +116,16 @@ class TunnelingConfigHelperImpl : public TunnelingConfigHelper { public: TunnelingConfigHelperImpl( const envoy::extensions::filters::network::tcp_proxy::v3::TcpProxy_TunnelingConfig& - config_message) - : hostname_(config_message.hostname()), use_post_(config_message.use_post()), - header_parser_(Envoy::Router::HeaderParser::configure(config_message.headers_to_add())) {} - const std::string& hostname() const override { return hostname_; } + config_message, + Server::Configuration::FactoryContext& context); + std::string host(const StreamInfo::StreamInfo& stream_info) const override; bool usePost() const override { return use_post_; } Envoy::Http::HeaderEvaluator& headerEvaluator() const override { return *header_parser_; } private: - const std::string hostname_; const bool use_post_; std::unique_ptr<Envoy::Router::HeaderParser> header_parser_; + Formatter::FormatterPtr hostname_fmt_; }; /** diff --git a/source/common/tcp_proxy/upstream.cc b/source/common/tcp_proxy/upstream.cc index fe9579234ae2..53dbff2a2e59 100644 --- a/source/common/tcp_proxy/upstream.cc +++ b/source/common/tcp_proxy/upstream.cc @@ -277,7 +277,7 @@ void Http2Upstream::setRequestEncoder(Http::RequestEncoder& request_encoder, boo is_ssl ? Http::Headers::get().SchemeValues.Https : Http::Headers::get().SchemeValues.Http; auto headers = Http::createHeaderMap<Http::RequestHeaderMapImpl>({ {Http::Headers::get().Method, config_.usePost() ? "POST" : "CONNECT"}, - {Http::Headers::get().Host, config_.hostname()}, + {Http::Headers::get().Host, config_.host(downstream_info_)}, }); if (config_.usePost()) { @@ -309,7 +309,7 @@ void Http1Upstream::setRequestEncoder(Http::RequestEncoder& request_encoder, boo auto headers = Http::createHeaderMap<Http::RequestHeaderMapImpl>({ {Http::Headers::get().Method, config_.usePost() ? "POST" : "CONNECT"}, - {Http::Headers::get().Host, config_.hostname()}, + {Http::Headers::get().Host, config_.host(downstream_info_)}, }); if (config_.usePost()) { diff --git a/test/common/tcp_proxy/BUILD b/test/common/tcp_proxy/BUILD index 45be486491e1..0630b106fd5c 100644 --- a/test/common/tcp_proxy/BUILD +++ b/test/common/tcp_proxy/BUILD @@ -79,6 +79,7 @@ envoy_cc_test( deps = [ "//source/common/tcp_proxy", "//test/mocks/http:http_mocks", + "//test/mocks/server:factory_context_mocks", "//test/mocks/tcp:tcp_mocks", "//test/mocks/upstream:cluster_manager_mocks", "//test/test_common:test_runtime_lib", diff --git a/test/common/tcp_proxy/upstream_test.cc b/test/common/tcp_proxy/upstream_test.cc index b42a43cb5cf1..fa3e5f4a956a 100644 --- a/test/common/tcp_proxy/upstream_test.cc +++ b/test/common/tcp_proxy/upstream_test.cc @@ -6,6 +6,7 @@ #include "test/mocks/buffer/mocks.h" #include "test/mocks/http/mocks.h" #include "test/mocks/http/stream_encoder.h" +#include "test/mocks/server/factory_context.h" #include "test/mocks/tcp/mocks.h" #include "test/test_common/environment.h" #include "test/test_common/network_utility.h" @@ -40,10 +41,11 @@ template <typename T> class HttpUpstreamTest : public testing::Test { } void setupUpstream() { - config_ = std::make_unique<TunnelingConfigHelperImpl>(config_message_); + config_ = std::make_unique<TunnelingConfigHelperImpl>(config_message_, context_); upstream_ = std::make_unique<T>(callbacks_, *this->config_, downstream_stream_info_); upstream_->setRequestEncoder(encoder_, true); } + NiceMock<StreamInfo::MockStreamInfo> downstream_stream_info_; Http::MockRequestEncoder encoder_; Http::MockHttp1StreamEncoderOptions stream_encoder_options_; @@ -51,6 +53,7 @@ template <typename T> class HttpUpstreamTest : public testing::Test { TcpProxy_TunnelingConfig config_message_; std::unique_ptr<TunnelingConfigHelper> config_; std::unique_ptr<HttpUpstream> upstream_; + NiceMock<Server::Configuration::MockFactoryContext> context_; }; using testing::Types; @@ -207,14 +210,23 @@ template <typename T> class HttpUpstreamRequestEncoderTest : public testing::Tes } void setupUpstream() { - config_ = std::make_unique<TunnelingConfigHelperImpl>(config_message_); + config_ = std::make_unique<TunnelingConfigHelperImpl>(config_message_, context_); upstream_ = std::make_unique<T>(callbacks_, *this->config_, this->downstream_stream_info_); } + void populateMetadata(envoy::config::core::v3::Metadata& metadata, const std::string& ns, + const std::string& key, const std::string& value) { + ProtobufWkt::Struct struct_obj; + auto& fields_map = *struct_obj.mutable_fields(); + fields_map[key] = ValueUtil::stringValue(value); + (*metadata.mutable_filter_metadata())[ns] = struct_obj; + } + NiceMock<StreamInfo::MockStreamInfo> downstream_stream_info_; Http::MockRequestEncoder encoder_; Http::MockHttp1StreamEncoderOptions stream_encoder_options_; NiceMock<Tcp::ConnectionPool::MockUpstreamCallbacks> callbacks_; + NiceMock<Server::Configuration::MockFactoryContext> context_; std::unique_ptr<HttpUpstream> upstream_; TcpProxy_TunnelingConfig config_message_; @@ -232,7 +244,7 @@ TYPED_TEST(HttpUpstreamRequestEncoderTest, RequestEncoderOld) { std::unique_ptr<Http::RequestHeaderMapImpl> expected_headers; expected_headers = Http::createHeaderMap<Http::RequestHeaderMapImpl>({ {Http::Headers::get().Method, "CONNECT"}, - {Http::Headers::get().Host, this->config_->hostname()}, + {Http::Headers::get().Host, this->config_->host(this->downstream_stream_info_)}, }); if (this->is_http2_) { @@ -252,7 +264,7 @@ TYPED_TEST(HttpUpstreamRequestEncoderTest, RequestEncoder) { std::unique_ptr<Http::RequestHeaderMapImpl> expected_headers; expected_headers = Http::createHeaderMap<Http::RequestHeaderMapImpl>({ {Http::Headers::get().Method, "CONNECT"}, - {Http::Headers::get().Host, this->config_->hostname()}, + {Http::Headers::get().Host, this->config_->host(this->downstream_stream_info_)}, }); EXPECT_CALL(this->encoder_, encodeHeaders(HeaderMapEqualRef(expected_headers.get()), false)); @@ -265,7 +277,7 @@ TYPED_TEST(HttpUpstreamRequestEncoderTest, RequestEncoderUsePost) { std::unique_ptr<Http::RequestHeaderMapImpl> expected_headers; expected_headers = Http::createHeaderMap<Http::RequestHeaderMapImpl>({ {Http::Headers::get().Method, "POST"}, - {Http::Headers::get().Host, this->config_->hostname()}, + {Http::Headers::get().Host, this->config_->host(this->downstream_stream_info_)}, {Http::Headers::get().Path, "/"}, }); @@ -300,7 +312,7 @@ TYPED_TEST(HttpUpstreamRequestEncoderTest, RequestEncoderHeaders) { std::unique_ptr<Http::RequestHeaderMapImpl> expected_headers; expected_headers = Http::createHeaderMap<Http::RequestHeaderMapImpl>({ {Http::Headers::get().Method, "CONNECT"}, - {Http::Headers::get().Host, this->config_->hostname()}, + {Http::Headers::get().Host, this->config_->host(this->downstream_stream_info_)}, }); expected_headers->setCopy(Http::LowerCaseString("header0"), "value0"); @@ -328,7 +340,7 @@ TYPED_TEST(HttpUpstreamRequestEncoderTest, ConfigReuse) { std::unique_ptr<Http::RequestHeaderMapImpl> expected_headers; expected_headers = Http::createHeaderMap<Http::RequestHeaderMapImpl>({ {Http::Headers::get().Method, "CONNECT"}, - {Http::Headers::get().Host, this->config_->hostname()}, + {Http::Headers::get().Host, this->config_->host(this->downstream_stream_info_)}, }); expected_headers->setCopy(Http::LowerCaseString("key"), "value1"); @@ -371,7 +383,7 @@ TYPED_TEST(HttpUpstreamRequestEncoderTest, RequestEncoderHeadersWithDownstreamIn std::unique_ptr<Http::RequestHeaderMapImpl> expected_headers; expected_headers = Http::createHeaderMap<Http::RequestHeaderMapImpl>({ {Http::Headers::get().Method, "CONNECT"}, - {Http::Headers::get().Host, this->config_->hostname()}, + {Http::Headers::get().Host, this->config_->host(this->downstream_stream_info_)}, }); expected_headers->setCopy(Http::LowerCaseString("header0"), "value0"); @@ -383,7 +395,55 @@ TYPED_TEST(HttpUpstreamRequestEncoderTest, RequestEncoderHeadersWithDownstreamIn *Network::Test::getCanonicalLoopbackAddress(ip_versions[0]), 80); Network::ConnectionInfoSetterImpl connection_info(ip_port, ip_port); EXPECT_CALL(this->downstream_stream_info_, downstreamAddressProvider) - .WillOnce(testing::ReturnRef(connection_info)); + .WillRepeatedly(testing::ReturnRef(connection_info)); + EXPECT_CALL(this->encoder_, encodeHeaders(HeaderMapEqualRef(expected_headers.get()), false)); + this->upstream_->setRequestEncoder(this->encoder_, false); +} + +TYPED_TEST(HttpUpstreamRequestEncoderTest, + RequestEncoderHostnameWithDownstreamInfoRequestedServerName) { + this->config_message_.set_hostname("%REQUESTED_SERVER_NAME%:443"); + this->setupUpstream(); + + std::unique_ptr<Http::RequestHeaderMapImpl> expected_headers; + expected_headers = Http::createHeaderMap<Http::RequestHeaderMapImpl>({ + {Http::Headers::get().Method, "CONNECT"}, + {Http::Headers::get().Host, "www.google.com:443"}, + }); + + auto ip_versions = TestEnvironment::getIpVersionsForTest(); + ASSERT_FALSE(ip_versions.empty()); + + auto ip_port = Network::Utility::getAddressWithPort( + *Network::Test::getCanonicalLoopbackAddress(ip_versions[0]), 80); + Network::ConnectionInfoSetterImpl connection_info(ip_port, ip_port); + connection_info.setRequestedServerName("www.google.com"); + EXPECT_CALL(this->downstream_stream_info_, downstreamAddressProvider) + .Times(2) + .WillRepeatedly(testing::ReturnRef(connection_info)); + EXPECT_CALL(this->encoder_, encodeHeaders(HeaderMapEqualRef(expected_headers.get()), false)); + this->upstream_->setRequestEncoder(this->encoder_, false); +} + +TYPED_TEST(HttpUpstreamRequestEncoderTest, + RequestEncoderHostnameWithDownstreamInfoDynamicMetadata) { + this->config_message_.set_hostname("%DYNAMIC_METADATA(tunnel:address)%:443"); + this->setupUpstream(); + + std::unique_ptr<Http::RequestHeaderMapImpl> expected_headers; + expected_headers = Http::createHeaderMap<Http::RequestHeaderMapImpl>({ + {Http::Headers::get().Method, "CONNECT"}, + {Http::Headers::get().Host, "www.google.com:443"}, + }); + + auto ip_versions = TestEnvironment::getIpVersionsForTest(); + ASSERT_FALSE(ip_versions.empty()); + + envoy::config::core::v3::Metadata metadata; + this->populateMetadata(metadata, "tunnel", "address", "www.google.com"); + + EXPECT_CALL(testing::Const(this->downstream_stream_info_), dynamicMetadata()) + .WillRepeatedly(testing::ReturnRef(metadata)); EXPECT_CALL(this->encoder_, encodeHeaders(HeaderMapEqualRef(expected_headers.get()), false)); this->upstream_->setRequestEncoder(this->encoder_, false); } diff --git a/test/extensions/upstreams/tcp/generic/BUILD b/test/extensions/upstreams/tcp/generic/BUILD index f4a37a797d2e..46881ead2768 100644 --- a/test/extensions/upstreams/tcp/generic/BUILD +++ b/test/extensions/upstreams/tcp/generic/BUILD @@ -14,6 +14,7 @@ envoy_cc_test( deps = [ "//source/common/tcp_proxy", "//source/extensions/upstreams/tcp/generic:config", + "//test/mocks/server:factory_context_mocks", "//test/mocks/upstream:upstream_mocks", ], ) diff --git a/test/extensions/upstreams/tcp/generic/config_test.cc b/test/extensions/upstreams/tcp/generic/config_test.cc index 5bbbbe5fd862..864f898b5bb8 100644 --- a/test/extensions/upstreams/tcp/generic/config_test.cc +++ b/test/extensions/upstreams/tcp/generic/config_test.cc @@ -1,6 +1,7 @@ #include "source/common/tcp_proxy/tcp_proxy.h" #include "source/extensions/upstreams/tcp/generic/config.h" +#include "test/mocks/server/factory_context.h" #include "test/mocks/tcp/mocks.h" #include "test/mocks/upstream/cluster_manager.h" #include "test/mocks/upstream/load_balancer_context.h" @@ -31,12 +32,13 @@ class TcpConnPoolTest : public ::testing::Test { NiceMock<StreamInfo::MockStreamInfo> downstream_stream_info_; NiceMock<Network::MockConnection> connection_; Upstream::MockLoadBalancerContext lb_context_; + NiceMock<Server::Configuration::MockFactoryContext> context_; }; TEST_F(TcpConnPoolTest, TestNoConnPool) { envoy::extensions::filters::network::tcp_proxy::v3::TcpProxy_TunnelingConfig config_proto; config_proto.set_hostname("host"); - const TcpProxy::TunnelingConfigHelperImpl config(config_proto); + const TcpProxy::TunnelingConfigHelperImpl config(config_proto, context_); EXPECT_CALL(thread_local_cluster_, httpConnPool(_, _, _)).WillOnce(Return(absl::nullopt)); EXPECT_EQ(nullptr, factory_.createGenericConnPool( thread_local_cluster_, TcpProxy::TunnelingConfigHelperOptConstRef(config), @@ -49,7 +51,7 @@ TEST_F(TcpConnPoolTest, Http2Config) { EXPECT_CALL(thread_local_cluster_, info).WillOnce(Return(info)); envoy::extensions::filters::network::tcp_proxy::v3::TcpProxy_TunnelingConfig config_proto; config_proto.set_hostname("host"); - const TcpProxy::TunnelingConfigHelperImpl config(config_proto); + const TcpProxy::TunnelingConfigHelperImpl config(config_proto, context_); EXPECT_CALL(thread_local_cluster_, httpConnPool(_, _, _)).WillOnce(Return(absl::nullopt)); EXPECT_EQ(nullptr, factory_.createGenericConnPool( @@ -65,7 +67,7 @@ TEST_F(TcpConnPoolTest, Http3Config) { EXPECT_CALL(thread_local_cluster_, info).Times(AnyNumber()).WillRepeatedly(Return(info)); envoy::extensions::filters::network::tcp_proxy::v3::TcpProxy_TunnelingConfig config_proto; config_proto.set_hostname("host"); - const TcpProxy::TunnelingConfigHelperImpl config(config_proto); + const TcpProxy::TunnelingConfigHelperImpl config(config_proto, context_); EXPECT_CALL(thread_local_cluster_, httpConnPool(_, _, _)).WillOnce(Return(absl::nullopt)); EXPECT_EQ(nullptr, factory_.createGenericConnPool( thread_local_cluster_, TcpProxy::TunnelingConfigHelperOptConstRef(config),