diff --git a/api/envoy/service/health/v3/BUILD b/api/envoy/service/health/v3/BUILD index ad01f3f340298..e3e214b25d3f1 100644 --- a/api/envoy/service/health/v3/BUILD +++ b/api/envoy/service/health/v3/BUILD @@ -7,6 +7,7 @@ licenses(["notice"]) # Apache 2 api_proto_package( has_services = True, deps = [ + "//envoy/config/cluster/v3:pkg", "//envoy/config/core/v3:pkg", "//envoy/config/endpoint/v3:pkg", "//envoy/service/discovery/v2:pkg", diff --git a/api/envoy/service/health/v3/hds.proto b/api/envoy/service/health/v3/hds.proto index 24fa7e9b5de8d..d73757f7a4fb0 100644 --- a/api/envoy/service/health/v3/hds.proto +++ b/api/envoy/service/health/v3/hds.proto @@ -2,6 +2,7 @@ syntax = "proto3"; package envoy.service.health.v3; +import "envoy/config/cluster/v3/cluster.proto"; import "envoy/config/core/v3/base.proto"; import "envoy/config/core/v3/health_check.proto"; import "envoy/config/endpoint/v3/endpoint_components.proto"; @@ -168,6 +169,11 @@ message ClusterHealthCheck { repeated config.core.v3.HealthCheck health_checks = 2; repeated LocalityEndpoints locality_endpoints = 3; + + // Optional map that gets filtered by :ref:`health_checks.transport_socket_match_criteria ` + // on connection when health checking. For more details, see + // :ref:`config.cluster.v3.Cluster.transport_socket_matches `. + repeated config.cluster.v3.Cluster.TransportSocketMatch transport_socket_matches = 4; } message HealthCheckSpecifier { diff --git a/api/envoy/service/health/v4alpha/BUILD b/api/envoy/service/health/v4alpha/BUILD index 448d869e456a2..60bd19511855e 100644 --- a/api/envoy/service/health/v4alpha/BUILD +++ b/api/envoy/service/health/v4alpha/BUILD @@ -7,6 +7,7 @@ licenses(["notice"]) # Apache 2 api_proto_package( has_services = True, deps = [ + "//envoy/config/cluster/v4alpha:pkg", "//envoy/config/core/v4alpha:pkg", "//envoy/config/endpoint/v3:pkg", "//envoy/service/health/v3:pkg", diff --git a/api/envoy/service/health/v4alpha/hds.proto b/api/envoy/service/health/v4alpha/hds.proto index a14e4c9327bac..537d20b58cbb3 100644 --- a/api/envoy/service/health/v4alpha/hds.proto +++ b/api/envoy/service/health/v4alpha/hds.proto @@ -2,6 +2,7 @@ syntax = "proto3"; package envoy.service.health.v4alpha; +import "envoy/config/cluster/v4alpha/cluster.proto"; import "envoy/config/core/v4alpha/base.proto"; import "envoy/config/core/v4alpha/health_check.proto"; import "envoy/config/endpoint/v3/endpoint_components.proto"; @@ -173,6 +174,11 @@ message ClusterHealthCheck { repeated config.core.v4alpha.HealthCheck health_checks = 2; repeated LocalityEndpoints locality_endpoints = 3; + + // Optional map that gets filtered by :ref:`health_checks.transport_socket_match_criteria ` + // on connection when health checking. For more details, see + // :ref:`config.cluster.v3.Cluster.transport_socket_matches `. + repeated config.cluster.v4alpha.Cluster.TransportSocketMatch transport_socket_matches = 4; } message HealthCheckSpecifier { diff --git a/docs/root/version_history/current.rst b/docs/root/version_history/current.rst index db723d7bd4016..056b2656b7b0b 100644 --- a/docs/root/version_history/current.rst +++ b/docs/root/version_history/current.rst @@ -72,6 +72,7 @@ New Features The emitted dynamic metadata is set by :ref:`dynamic metadata ` field in a returned :ref:`CheckResponse `. * grpc-json: support specifying `response_body` field in for `google.api.HttpBody` message. * hds: added :ref:`cluster_endpoints_health ` to HDS responses, keeping endpoints in the same groupings as they were configured in the HDS specifier by cluster and locality instead of as a flat list. +* hds: added :ref:`transport_socket_matches ` to HDS cluster health check specifier, so the existing match filter :ref:`transport_socket_match_criteria ` in the repeated field :ref:`health_checks ` has context to match against. This unblocks support for health checks over HTTPS and HTTP/2. * http: added support for :ref:`%DOWNSTREAM_PEER_FINGERPRINT_1% ` as custom header. * http: added :ref:`allow_chunked_length ` configuration option for HTTP/1 codec to allow processing requests/responses with both Content-Length and Transfer-Encoding: chunked headers. If such message is served and option is enabled - per RFC Content-Length is ignored and removed. * http: introduced new HTTP/1 and HTTP/2 codec implementations that will remove the use of exceptions for control flow due to high risk factors and instead use error statuses. The old behavior is used by default, but the new codecs can be enabled for testing by setting the runtime feature `envoy.reloadable_features.new_codec_behavior` to true. The new codecs will be in development for one month, and then enabled by default while the old codecs are deprecated. diff --git a/generated_api_shadow/envoy/service/health/v3/BUILD b/generated_api_shadow/envoy/service/health/v3/BUILD index ad01f3f340298..e3e214b25d3f1 100644 --- a/generated_api_shadow/envoy/service/health/v3/BUILD +++ b/generated_api_shadow/envoy/service/health/v3/BUILD @@ -7,6 +7,7 @@ licenses(["notice"]) # Apache 2 api_proto_package( has_services = True, deps = [ + "//envoy/config/cluster/v3:pkg", "//envoy/config/core/v3:pkg", "//envoy/config/endpoint/v3:pkg", "//envoy/service/discovery/v2:pkg", diff --git a/generated_api_shadow/envoy/service/health/v3/hds.proto b/generated_api_shadow/envoy/service/health/v3/hds.proto index 24fa7e9b5de8d..d73757f7a4fb0 100644 --- a/generated_api_shadow/envoy/service/health/v3/hds.proto +++ b/generated_api_shadow/envoy/service/health/v3/hds.proto @@ -2,6 +2,7 @@ syntax = "proto3"; package envoy.service.health.v3; +import "envoy/config/cluster/v3/cluster.proto"; import "envoy/config/core/v3/base.proto"; import "envoy/config/core/v3/health_check.proto"; import "envoy/config/endpoint/v3/endpoint_components.proto"; @@ -168,6 +169,11 @@ message ClusterHealthCheck { repeated config.core.v3.HealthCheck health_checks = 2; repeated LocalityEndpoints locality_endpoints = 3; + + // Optional map that gets filtered by :ref:`health_checks.transport_socket_match_criteria ` + // on connection when health checking. For more details, see + // :ref:`config.cluster.v3.Cluster.transport_socket_matches `. + repeated config.cluster.v3.Cluster.TransportSocketMatch transport_socket_matches = 4; } message HealthCheckSpecifier { diff --git a/generated_api_shadow/envoy/service/health/v4alpha/BUILD b/generated_api_shadow/envoy/service/health/v4alpha/BUILD index 448d869e456a2..60bd19511855e 100644 --- a/generated_api_shadow/envoy/service/health/v4alpha/BUILD +++ b/generated_api_shadow/envoy/service/health/v4alpha/BUILD @@ -7,6 +7,7 @@ licenses(["notice"]) # Apache 2 api_proto_package( has_services = True, deps = [ + "//envoy/config/cluster/v4alpha:pkg", "//envoy/config/core/v4alpha:pkg", "//envoy/config/endpoint/v3:pkg", "//envoy/service/health/v3:pkg", diff --git a/generated_api_shadow/envoy/service/health/v4alpha/hds.proto b/generated_api_shadow/envoy/service/health/v4alpha/hds.proto index 1a1b9ed300e8d..8fb671161fd6c 100644 --- a/generated_api_shadow/envoy/service/health/v4alpha/hds.proto +++ b/generated_api_shadow/envoy/service/health/v4alpha/hds.proto @@ -2,6 +2,7 @@ syntax = "proto3"; package envoy.service.health.v4alpha; +import "envoy/config/cluster/v4alpha/cluster.proto"; import "envoy/config/core/v4alpha/base.proto"; import "envoy/config/core/v4alpha/health_check.proto"; import "envoy/config/endpoint/v3/endpoint_components.proto"; @@ -172,6 +173,11 @@ message ClusterHealthCheck { repeated config.core.v4alpha.HealthCheck health_checks = 2; repeated LocalityEndpoints locality_endpoints = 3; + + // Optional map that gets filtered by :ref:`health_checks.transport_socket_match_criteria ` + // on connection when health checking. For more details, see + // :ref:`config.cluster.v3.Cluster.transport_socket_matches `. + repeated config.cluster.v4alpha.Cluster.TransportSocketMatch transport_socket_matches = 4; } message HealthCheckSpecifier { diff --git a/source/common/upstream/health_discovery_service.cc b/source/common/upstream/health_discovery_service.cc index 5cffaff8e9951..c43de71f93ee3 100644 --- a/source/common/upstream/health_discovery_service.cc +++ b/source/common/upstream/health_discovery_service.cc @@ -203,6 +203,10 @@ void HdsDelegate::processMessage( cluster_config.add_health_checks()->MergeFrom(health_check); } + // Add transport_socket_match to cluster for use in host connections. + cluster_config.mutable_transport_socket_matches()->MergeFrom( + cluster_health_check.transport_socket_matches()); + ENVOY_LOG(debug, "New HdsCluster config {} ", cluster_config.DebugString()); // Create HdsCluster diff --git a/test/common/upstream/BUILD b/test/common/upstream/BUILD index e3b6a2c1ca916..7a65b1341f942 100644 --- a/test/common/upstream/BUILD +++ b/test/common/upstream/BUILD @@ -283,6 +283,8 @@ envoy_cc_test( srcs = ["hds_test.cc"], deps = [ "//source/common/upstream:health_discovery_service_lib", + "//source/common/upstream:transport_socket_match_lib", + "//source/extensions/transport_sockets/raw_buffer:config", "//source/extensions/transport_sockets/tls:context_lib", "//test/mocks/access_log:access_log_mocks", "//test/mocks/event:event_mocks", @@ -295,6 +297,8 @@ envoy_cc_test( "//test/mocks/upstream:cluster_info_factory_mocks", "//test/mocks/upstream:cluster_info_mocks", "//test/mocks/upstream:cluster_manager_mocks", + "//test/mocks/upstream:upstream_mocks", + "//test/test_common:environment_lib", "//test/test_common:simulated_time_system_lib", "//test/test_common:utility_lib", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", diff --git a/test/common/upstream/hds_test.cc b/test/common/upstream/hds_test.cc index 35e690c99acd8..5bc411ff44256 100644 --- a/test/common/upstream/hds_test.cc +++ b/test/common/upstream/hds_test.cc @@ -5,9 +5,12 @@ #include "envoy/service/health/v3/hds.pb.h" #include "envoy/type/v3/http.pb.h" +#include "common/protobuf/protobuf.h" #include "common/singleton/manager_impl.h" #include "common/upstream/health_discovery_service.h" +#include "common/upstream/transport_socket_match_impl.h" +#include "extensions/transport_sockets/raw_buffer/config.h" #include "extensions/transport_sockets/tls/context_manager_impl.h" #include "test/mocks/access_log/mocks.h" @@ -21,9 +24,12 @@ #include "test/mocks/upstream/cluster_info.h" #include "test/mocks/upstream/cluster_info_factory.h" #include "test/mocks/upstream/cluster_manager.h" +#include "test/mocks/upstream/mocks.h" +#include "test/test_common/environment.h" #include "test/test_common/simulated_time_system.h" #include "test/test_common/utility.h" +#include "absl/strings/str_format.h" #include "gmock/gmock.h" #include "gtest/gtest.h" @@ -480,6 +486,90 @@ TEST_F(HdsTest, TestMinimalOnReceiveMessage) { hds_delegate_->onReceiveMessage(std::move(message)); } +// Test that a transport_socket_matches and transport_socket_match_criteria filter as expected to +// build the correct TransportSocketFactory based on these fields. +TEST_F(HdsTest, TestSocketContext) { + EXPECT_CALL(*async_client_, startRaw(_, _, _, _)).WillOnce(Return(&async_stream_)); + EXPECT_CALL(async_stream_, sendMessageRaw_(_, _)); + createHdsDelegate(); + + // Create Message. + message.reset(createSimpleMessage()); + + // Add transport socket matches to message. + const std::string match_yaml = absl::StrFormat( + R"EOF( +transport_socket_matches: +- name: "test_socket" + match: + test_match: "true" + transport_socket: + name: "envoy.transport_sockets.raw_buffer" +)EOF"); + auto* cluster_health_check = message->mutable_cluster_health_checks(0); + cluster_health_check->MergeFrom( + TestUtility::parseYaml(match_yaml)); + + // Add transport socket match criteria to our health check, for filtering matches. + const std::string criteria_yaml = absl::StrFormat( + R"EOF( +transport_socket_match_criteria: + test_match: "true" +)EOF"); + cluster_health_check->mutable_health_checks(0)->MergeFrom( + TestUtility::parseYaml(criteria_yaml)); + + Network::MockClientConnection* connection = new NiceMock(); + EXPECT_CALL(dispatcher_, createClientConnection_(_, _, _, _)).WillRepeatedly(Return(connection)); + + // Pull out socket_matcher object normally internal to createClusterInfo, to test that a matcher + // would match the expected socket. + std::unique_ptr socket_matcher; + EXPECT_CALL(test_factory_, createClusterInfo(_)) + .WillRepeatedly(Invoke([&](const ClusterInfoFactory::CreateClusterInfoParams& params) { + // Build scope, factory_context as does ProdClusterInfoFactory. + Envoy::Stats::ScopePtr scope = + params.stats_.createScope(fmt::format("cluster.{}.", params.cluster_.name())); + Envoy::Server::Configuration::TransportSocketFactoryContextImpl factory_context( + params.admin_, params.ssl_context_manager_, *scope, params.cm_, params.local_info_, + params.dispatcher_, params.random_, params.stats_, params.singleton_manager_, + params.tls_, params.validation_visitor_, params.api_); + + // Create a mock socket_factory for the scope of this unit test. + std::unique_ptr socket_factory = + std::make_unique(); + + // set socket_matcher object in test scope. + socket_matcher = std::make_unique( + params.cluster_.transport_socket_matches(), factory_context, socket_factory, *scope); + + // But still use the fake cluster_info_. + return cluster_info_; + })); + + EXPECT_CALL(*connection, setBufferLimits(_)); + EXPECT_CALL(dispatcher_, deferredDelete_(_)); + + // Process message. + EXPECT_CALL(*server_response_timer_, enableTimer(_, _)).Times(AtLeast(1)); + hds_delegate_->onReceiveMessage(std::move(message)); + + // pretend our endpoint was connected to. + connection->raiseEvent(Network::ConnectionEvent::Connected); + + // Get our health checker to match against. + const auto clusters = hds_delegate_->hdsClusters(); + ASSERT_EQ(clusters.size(), 1); + const auto hcs = clusters[0]->healthCheckers(); + ASSERT_EQ(hcs.size(), 1); + + // Check that our match hits. + HealthCheckerImplBase* health_checker_base = dynamic_cast(hcs[0].get()); + const auto match = + socket_matcher->resolve(health_checker_base->transportSocketMatchMetadata().get()); + EXPECT_EQ(match.name_, "test_socket"); +} + // Tests OnReceiveMessage given a HealthCheckSpecifier message without interval field TEST_F(HdsTest, TestDefaultIntervalOnReceiveMessage) { EXPECT_CALL(*async_client_, startRaw(_, _, _, _)).WillOnce(Return(&async_stream_)); diff --git a/test/integration/BUILD b/test/integration/BUILD index f2081b42f6d1c..d817f368cfc35 100644 --- a/test/integration/BUILD +++ b/test/integration/BUILD @@ -373,12 +373,10 @@ envoy_cc_test( ], deps = [ ":http_integration_lib", - "//source/extensions/transport_sockets/tls:context_lib", "//test/common/upstream:utility_lib", "@envoy_api//envoy/config/bootstrap/v3:pkg_cc_proto", "@envoy_api//envoy/config/route/v3:pkg_cc_proto", "@envoy_api//envoy/extensions/filters/network/http_connection_manager/v3:pkg_cc_proto", - "@envoy_api//envoy/extensions/transport_sockets/tls/v3:pkg_cc_proto", ], ) @@ -494,6 +492,9 @@ envoy_cc_test_library( hdrs = [ "http_integration.h", ], + data = [ + "//test/config/integration/certs", + ], deps = [ ":integration_lib", ":test_host_predicate_lib", @@ -502,6 +503,7 @@ envoy_cc_test_library( "//source/extensions/filters/http/on_demand:config", "//source/extensions/filters/http/router:config", "//source/extensions/filters/network/http_connection_manager:config", + "//source/extensions/transport_sockets/tls:context_lib", "//test/common/upstream:utility_lib", "//test/integration/filters:add_body_filter_config_lib", "//test/integration/filters:add_trailers_filter_config_lib", @@ -980,6 +982,9 @@ envoy_cc_test( envoy_cc_test( name = "hds_integration_test", srcs = ["hds_integration_test.cc"], + data = [ + "//test/config/integration/certs", + ], shard_count = 2, # Alternately timing out and failing in CI on windows; observed to pass locally tags = ["flaky_on_windows"], @@ -999,6 +1004,7 @@ envoy_cc_test( "@envoy_api//envoy/config/bootstrap/v3:pkg_cc_proto", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", "@envoy_api//envoy/service/health/v3:pkg_cc_proto", + "@envoy_api//envoy/type/v3:pkg_cc_proto", ], ) diff --git a/test/integration/hds_integration_test.cc b/test/integration/hds_integration_test.cc index 3b9020fd99819..74b36ff94b490 100644 --- a/test/integration/hds_integration_test.cc +++ b/test/integration/hds_integration_test.cc @@ -4,6 +4,7 @@ #include "envoy/config/core/v3/config_source.pb.h" #include "envoy/config/core/v3/health_check.pb.h" #include "envoy/service/health/v3/hds.pb.h" +#include "envoy/type/v3/http.pb.h" #include "envoy/upstream/upstream.h" #include "common/config/metadata.h" @@ -37,7 +38,6 @@ class HdsIntegrationTest : public Grpc::VersionedGrpcClientIntegrationParamTest, hds_upstream_ = fake_upstreams_.back().get(); HttpIntegrationTest::createUpstreams(); } - void initialize() override { setUpstreamCount(upstream_endpoints_); config_helper_.addConfigModifier([this](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { @@ -58,10 +58,17 @@ class HdsIntegrationTest : public Grpc::VersionedGrpcClientIntegrationParamTest, HttpIntegrationTest::initialize(); // Endpoint connections - host_upstream_ = - std::make_unique(0, FakeHttpConnection::Type::HTTP1, version_, timeSystem()); - host2_upstream_ = - std::make_unique(0, FakeHttpConnection::Type::HTTP1, version_, timeSystem()); + if (tls_hosts_) { + host_upstream_ = + std::make_unique(HttpIntegrationTest::createUpstreamTlsContext(), 0, + http_conn_type_, version_, timeSystem()); + host2_upstream_ = + std::make_unique(HttpIntegrationTest::createUpstreamTlsContext(), 0, + http_conn_type_, version_, timeSystem()); + } else { + host_upstream_ = std::make_unique(0, http_conn_type_, version_, timeSystem()); + host2_upstream_ = std::make_unique(0, http_conn_type_, version_, timeSystem()); + } } // Sets up a connection between Envoy and the management server. @@ -122,30 +129,62 @@ class HdsIntegrationTest : public Grpc::VersionedGrpcClientIntegrationParamTest, // Creates a basic HealthCheckSpecifier message containing one endpoint and // one HTTP health_check - envoy::service::health::v3::HealthCheckSpecifier makeHttpHealthCheckSpecifier() { + envoy::service::health::v3::HealthCheckSpecifier + makeHttpHealthCheckSpecifier(envoy::type::v3::CodecClientType codec_type, bool use_tls) { envoy::service::health::v3::HealthCheckSpecifier server_health_check_specifier_; server_health_check_specifier_.mutable_interval()->set_nanos(100000000); // 0.1 seconds - auto* health_check = server_health_check_specifier_.add_cluster_health_checks(); + auto* cluster_health_check = server_health_check_specifier_.add_cluster_health_checks(); - health_check->set_cluster_name("anna"); + cluster_health_check->set_cluster_name("anna"); Network::Utility::addressToProtobufAddress( *host_upstream_->localAddress(), - *health_check->add_locality_endpoints()->add_endpoints()->mutable_address()); - health_check->mutable_locality_endpoints(0)->mutable_locality()->set_region("middle_earth"); - health_check->mutable_locality_endpoints(0)->mutable_locality()->set_zone("shire"); - health_check->mutable_locality_endpoints(0)->mutable_locality()->set_sub_zone("hobbiton"); - - health_check->add_health_checks()->mutable_timeout()->set_seconds(MaxTimeout); - health_check->mutable_health_checks(0)->mutable_interval()->set_seconds(MaxTimeout); - health_check->mutable_health_checks(0)->mutable_unhealthy_threshold()->set_value(2); - health_check->mutable_health_checks(0)->mutable_healthy_threshold()->set_value(2); - health_check->mutable_health_checks(0)->mutable_grpc_health_check(); - health_check->mutable_health_checks(0) - ->mutable_http_health_check() - ->set_hidden_envoy_deprecated_use_http2(false); - health_check->mutable_health_checks(0)->mutable_http_health_check()->set_path("/healthcheck"); - + *cluster_health_check->add_locality_endpoints()->add_endpoints()->mutable_address()); + cluster_health_check->mutable_locality_endpoints(0)->mutable_locality()->set_region( + "middle_earth"); + cluster_health_check->mutable_locality_endpoints(0)->mutable_locality()->set_zone("shire"); + cluster_health_check->mutable_locality_endpoints(0)->mutable_locality()->set_sub_zone( + "hobbiton"); + auto* health_check = cluster_health_check->add_health_checks(); + health_check->mutable_timeout()->set_seconds(MaxTimeout); + health_check->mutable_interval()->set_seconds(MaxTimeout); + health_check->mutable_unhealthy_threshold()->set_value(2); + health_check->mutable_healthy_threshold()->set_value(2); + health_check->mutable_grpc_health_check(); + auto* http_health_check = health_check->mutable_http_health_check(); + http_health_check->set_path("/healthcheck"); + http_health_check->set_codec_client_type(codec_type); + if (use_tls) { + // Map our transport socket matches with our matcher. + const std::string criteria_yaml = absl::StrFormat( + R"EOF( +transport_socket_match_criteria: + good_match: "true" +)EOF"); + health_check->MergeFrom( + TestUtility::parseYaml(criteria_yaml)); + + // Create the list of all possible matches. + const std::string match_yaml = absl::StrFormat( + R"EOF( +transport_socket_matches: +- name: "tls_socket" + match: + good_match: "true" + transport_socket: + name: tls + typed_config: + "@type": type.googleapis.com/envoy.api.v2.auth.UpstreamTlsContext + common_tls_context: + tls_certificates: + - certificate_chain: { filename: "%s" } + private_key: { filename: "%s" } + )EOF", + TestEnvironment::runfilesPath("test/config/integration/certs/clientcert.pem"), + TestEnvironment::runfilesPath("test/config/integration/certs/clientkey.pem")); + cluster_health_check->MergeFrom( + TestUtility::parseYaml(match_yaml)); + } return server_health_check_specifier_; } @@ -299,6 +338,8 @@ class HdsIntegrationTest : public Grpc::VersionedGrpcClientIntegrationParamTest, FakeHttpConnectionPtr host_fake_connection_; FakeHttpConnectionPtr host2_fake_connection_; FakeRawConnectionPtr host_fake_raw_connection_; + FakeHttpConnection::Type http_conn_type_{FakeHttpConnection::Type::HTTP1}; + bool tls_hosts_{false}; static constexpr int MaxTimeout = 100; envoy::service::health::v3::HealthCheckRequestOrEndpointHealthResponse envoy_msg_; @@ -321,7 +362,8 @@ TEST_P(HdsIntegrationTest, SingleEndpointHealthyHttp) { envoy::service::health::v3::Capability::HTTP); // Server asks for health checking - server_health_check_specifier_ = makeHttpHealthCheckSpecifier(); + server_health_check_specifier_ = + makeHttpHealthCheckSpecifier(envoy::type::v3::CodecClientType::HTTP1, false); hds_stream_->startGrpcStream(); hds_stream_->sendGrpcMessage(server_health_check_specifier_); test_server_->waitForCounterGe("hds_delegate.requests", ++hds_requests_); @@ -347,7 +389,8 @@ TEST_P(HdsIntegrationTest, SingleEndpointHealthyHttp) { // that it is unhealthy to the server. TEST_P(HdsIntegrationTest, SingleEndpointTimeoutHttp) { initialize(); - server_health_check_specifier_ = makeHttpHealthCheckSpecifier(); + server_health_check_specifier_ = + makeHttpHealthCheckSpecifier(envoy::type::v3::CodecClientType::HTTP1, false); server_health_check_specifier_.mutable_cluster_health_checks(0) ->mutable_health_checks(0) @@ -387,7 +430,8 @@ TEST_P(HdsIntegrationTest, SingleEndpointTimeoutHttp) { // indeed unhealthy to the server. TEST_P(HdsIntegrationTest, SingleEndpointUnhealthyHttp) { initialize(); - server_health_check_specifier_ = makeHttpHealthCheckSpecifier(); + server_health_check_specifier_ = + makeHttpHealthCheckSpecifier(envoy::type::v3::CodecClientType::HTTP1, false); // Server <--> Envoy waitForHdsStream(); @@ -524,7 +568,8 @@ TEST_P(HdsIntegrationTest, SingleEndpointUnhealthyTcp) { TEST_P(HdsIntegrationTest, TwoEndpointsSameLocality) { initialize(); - server_health_check_specifier_ = makeHttpHealthCheckSpecifier(); + server_health_check_specifier_ = + makeHttpHealthCheckSpecifier(envoy::type::v3::CodecClientType::HTTP1, false); Network::Utility::addressToProtobufAddress( *host2_upstream_->localAddress(), *server_health_check_specifier_.mutable_cluster_health_checks(0) @@ -581,7 +626,8 @@ TEST_P(HdsIntegrationTest, TwoEndpointsSameLocality) { // different localities and report back the correct health statuses. TEST_P(HdsIntegrationTest, TwoEndpointsDifferentLocality) { initialize(); - server_health_check_specifier_ = makeHttpHealthCheckSpecifier(); + server_health_check_specifier_ = + makeHttpHealthCheckSpecifier(envoy::type::v3::CodecClientType::HTTP1, false); // Add endpoint auto* health_check = server_health_check_specifier_.mutable_cluster_health_checks(0); @@ -648,7 +694,8 @@ TEST_P(HdsIntegrationTest, TwoEndpointsDifferentLocality) { // report back the correct health statuses. TEST_P(HdsIntegrationTest, TwoEndpointsDifferentClusters) { initialize(); - server_health_check_specifier_ = makeHttpHealthCheckSpecifier(); + server_health_check_specifier_ = + makeHttpHealthCheckSpecifier(envoy::type::v3::CodecClientType::HTTP1, false); // Add endpoint auto* health_check = server_health_check_specifier_.add_cluster_health_checks(); @@ -739,7 +786,8 @@ TEST_P(HdsIntegrationTest, TestUpdateMessage) { ASSERT_TRUE(hds_stream_->waitForGrpcMessage(*dispatcher_, envoy_msg_)); // Server asks for health checking - server_health_check_specifier_ = makeHttpHealthCheckSpecifier(); + server_health_check_specifier_ = + makeHttpHealthCheckSpecifier(envoy::type::v3::CodecClientType::HTTP1, false); hds_stream_->startGrpcStream(); hds_stream_->sendGrpcMessage(server_health_check_specifier_); test_server_->waitForCounterGe("hds_delegate.requests", ++hds_requests_); @@ -819,7 +867,8 @@ TEST_P(HdsIntegrationTest, TestUpdateChangesTimer) { ASSERT_TRUE(hds_stream_->waitForGrpcMessage(*dispatcher_, envoy_msg_)); // Server asks for health checking - server_health_check_specifier_ = makeHttpHealthCheckSpecifier(); + server_health_check_specifier_ = + makeHttpHealthCheckSpecifier(envoy::type::v3::CodecClientType::HTTP1, false); hds_stream_->startGrpcStream(); hds_stream_->sendGrpcMessage(server_health_check_specifier_); test_server_->waitForCounterGe("hds_delegate.requests", ++hds_requests_); @@ -858,7 +907,8 @@ TEST_P(HdsIntegrationTest, TestDefaultTimer) { ASSERT_TRUE(hds_stream_->waitForGrpcMessage(*dispatcher_, envoy_msg_)); // Server asks for health checking - server_health_check_specifier_ = makeHttpHealthCheckSpecifier(); + server_health_check_specifier_ = + makeHttpHealthCheckSpecifier(envoy::type::v3::CodecClientType::HTTP1, false); server_health_check_specifier_.clear_interval(); hds_stream_->startGrpcStream(); hds_stream_->sendGrpcMessage(server_health_check_specifier_); @@ -875,5 +925,123 @@ TEST_P(HdsIntegrationTest, TestDefaultTimer) { cleanupHdsConnection(); } +// Health checks a single endpoint over TLS with HTTP/2 +TEST_P(HdsIntegrationTest, SingleEndpointHealthyTlsHttp2) { + // Change member variable to specify host streams to have tls transport socket. + tls_hosts_ = true; + + // Change hosts to operate over HTTP/2 instead of default HTTP. + http_conn_type_ = FakeHttpConnection::Type::HTTP2; + + initialize(); + + // Server <--> Envoy + waitForHdsStream(); + ASSERT_TRUE(hds_stream_->waitForGrpcMessage(*dispatcher_, envoy_msg_)); + EXPECT_EQ(envoy_msg_.health_check_request().capability().health_check_protocols(0), + envoy::service::health::v3::Capability::HTTP); + + // Server asks for health checking + server_health_check_specifier_ = + makeHttpHealthCheckSpecifier(envoy::type::v3::CodecClientType::HTTP2, true); + hds_stream_->startGrpcStream(); + hds_stream_->sendGrpcMessage(server_health_check_specifier_); + test_server_->waitForCounterGe("hds_delegate.requests", ++hds_requests_); + + // Envoy sends a health check message to an endpoint + healthcheckEndpoints(); + + // Endpoint responds to the health check + host_stream_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, false); + host_stream_->encodeData(1024, true); + + // Receive updates until the one we expect arrives + waitForEndpointHealthResponse(envoy::config::core::v3::HEALTHY); + + checkCounters(1, 2, 1, 0); + + // Clean up connections + cleanupHostConnections(); + cleanupHdsConnection(); +} + +// Health checks a single endpoint over TLS with HTTP/1 +TEST_P(HdsIntegrationTest, SingleEndpointHealthyTlsHttp1) { + // Change member variable to specify host streams to have tls transport socket. + tls_hosts_ = true; + + initialize(); + + // Server <--> Envoy + waitForHdsStream(); + ASSERT_TRUE(hds_stream_->waitForGrpcMessage(*dispatcher_, envoy_msg_)); + EXPECT_EQ(envoy_msg_.health_check_request().capability().health_check_protocols(0), + envoy::service::health::v3::Capability::HTTP); + + // Server asks for health checking + server_health_check_specifier_ = + makeHttpHealthCheckSpecifier(envoy::type::v3::CodecClientType::HTTP1, true); + hds_stream_->startGrpcStream(); + hds_stream_->sendGrpcMessage(server_health_check_specifier_); + test_server_->waitForCounterGe("hds_delegate.requests", ++hds_requests_); + + // Envoy sends a health check message to an endpoint + healthcheckEndpoints(); + + // Endpoint responds to the health check + host_stream_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, false); + host_stream_->encodeData(1024, true); + + // Receive updates until the one we expect arrives + waitForEndpointHealthResponse(envoy::config::core::v3::HEALTHY); + + checkCounters(1, 2, 1, 0); + + // Clean up connections + cleanupHostConnections(); + cleanupHdsConnection(); +} + +// Attempts to health check a TLS endpoint over plaintext, which should fail. +TEST_P(HdsIntegrationTest, SingleEndpointUnhealthyTlsMissingSocketMatch) { + // Make the endpoints expect communication over TLS. + tls_hosts_ = true; + + initialize(); + + // Server <--> Envoy + waitForHdsStream(); + ASSERT_TRUE(hds_stream_->waitForGrpcMessage(*dispatcher_, envoy_msg_)); + EXPECT_EQ(envoy_msg_.health_check_request().capability().health_check_protocols(0), + envoy::service::health::v3::Capability::HTTP); + + // Make the specifier not have the TLS socket matches, so it will try to connect over plaintext. + server_health_check_specifier_ = + makeHttpHealthCheckSpecifier(envoy::type::v3::CodecClientType::HTTP1, false); + + hds_stream_->startGrpcStream(); + hds_stream_->sendGrpcMessage(server_health_check_specifier_); + test_server_->waitForCounterGe("hds_delegate.requests", ++hds_requests_); + + // Envoy sends a health check message to an endpoint + ASSERT_TRUE(host_upstream_->waitForRawConnection(host_fake_raw_connection_)); + + // Endpoint doesn't respond to the health check + ASSERT_TRUE(host_fake_raw_connection_->waitForDisconnect()); + + // Receive updates until the one we expect arrives. This should be UNHEALTHY and not TIMEOUT, + // because TIMEOUT occurs in the situation where there is no response from the endpoint. In this + // case, the endpoint does respond but it is over TLS, and HDS is trying to parse it as plaintext. + // It does not recognize the malformed plaintext, so it is considered a failure and UNHEALTHY is + // set. + waitForEndpointHealthResponse(envoy::config::core::v3::UNHEALTHY); + + checkCounters(1, 2, 0, 1); + + // Clean up connections + cleanupHostConnections(); + cleanupHdsConnection(); +} + } // namespace } // namespace Envoy diff --git a/test/integration/http_integration.cc b/test/integration/http_integration.cc index b52cd98c1ce18..8036703a355e2 100644 --- a/test/integration/http_integration.cc +++ b/test/integration/http_integration.cc @@ -25,6 +25,10 @@ #include "common/runtime/runtime_impl.h" #include "common/upstream/upstream_impl.h" +#include "extensions/transport_sockets/tls/context_config_impl.h" +#include "extensions/transport_sockets/tls/context_impl.h" +#include "extensions/transport_sockets/tls/ssl_socket.h" + #include "test/common/upstream/utility.h" #include "test/integration/autonomous_upstream.h" #include "test/integration/test_host_predicate_config.h" @@ -226,6 +230,28 @@ IntegrationCodecClientPtr HttpIntegrationTest::makeRawHttpConnection( downstream_protocol_); } +Network::TransportSocketFactoryPtr HttpIntegrationTest::createUpstreamTlsContext() { + envoy::extensions::transport_sockets::tls::v3::DownstreamTlsContext tls_context; + const std::string yaml = absl::StrFormat( + R"EOF( +common_tls_context: + tls_certificates: + - certificate_chain: { filename: "%s" } + private_key: { filename: "%s" } + validation_context: + trusted_ca: { filename: "%s" } +require_client_certificate: true +)EOF", + TestEnvironment::runfilesPath("test/config/integration/certs/upstreamcert.pem"), + TestEnvironment::runfilesPath("test/config/integration/certs/upstreamkey.pem"), + TestEnvironment::runfilesPath("test/config/integration/certs/cacert.pem")); + TestUtility::loadFromYaml(yaml, tls_context); + auto cfg = std::make_unique( + tls_context, factory_context_); + static Stats::Scope* upstream_stats_store = new Stats::IsolatedStoreImpl(); + return std::make_unique( + std::move(cfg), context_manager_, *upstream_stats_store, std::vector{}); +} IntegrationCodecClientPtr HttpIntegrationTest::makeHttpConnection(Network::ClientConnectionPtr&& conn) { auto codec = makeRawHttpConnection(std::move(conn), absl::nullopt); diff --git a/test/integration/http_integration.h b/test/integration/http_integration.h index 30e898936f724..cb197cadb5e41 100644 --- a/test/integration/http_integration.h +++ b/test/integration/http_integration.h @@ -104,6 +104,7 @@ class HttpIntegrationTest : public BaseIntegrationTest { protected: void useAccessLog(absl::string_view format = ""); + Network::TransportSocketFactoryPtr createUpstreamTlsContext(); IntegrationCodecClientPtr makeHttpConnection(uint32_t port); // Makes a http connection object without checking its connected state. virtual IntegrationCodecClientPtr makeRawHttpConnection( diff --git a/test/integration/transport_socket_match_integration_test.cc b/test/integration/transport_socket_match_integration_test.cc index 2456921be3e16..75d101beb7023 100644 --- a/test/integration/transport_socket_match_integration_test.cc +++ b/test/integration/transport_socket_match_integration_test.cc @@ -1,11 +1,6 @@ #include "envoy/config/bootstrap/v3/bootstrap.pb.h" #include "envoy/config/route/v3/route_components.pb.h" #include "envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.pb.h" -#include "envoy/extensions/transport_sockets/tls/v3/cert.pb.h" - -#include "extensions/transport_sockets/tls/context_config_impl.h" -#include "extensions/transport_sockets/tls/context_impl.h" -#include "extensions/transport_sockets/tls/ssl_socket.h" #include "test/integration/autonomous_upstream.h" #include "test/integration/http_integration.h" @@ -117,29 +112,6 @@ name: "tls_socket" .set_string_value(host_type); }; - Network::TransportSocketFactoryPtr createUpstreamSslContext() { - envoy::extensions::transport_sockets::tls::v3::DownstreamTlsContext tls_context; - const std::string yaml = absl::StrFormat( - R"EOF( -common_tls_context: - tls_certificates: - - certificate_chain: { filename: "%s" } - private_key: { filename: "%s" } - validation_context: - trusted_ca: { filename: "%s" } -require_client_certificate: true -)EOF", - TestEnvironment::runfilesPath("test/config/integration/certs/upstreamcert.pem"), - TestEnvironment::runfilesPath("test/config/integration/certs/upstreamkey.pem"), - TestEnvironment::runfilesPath("test/config/integration/certs/cacert.pem")); - TestUtility::loadFromYaml(yaml, tls_context); - auto cfg = std::make_unique( - tls_context, factory_context_); - static Stats::Scope* upstream_stats_store = new Stats::IsolatedStoreImpl(); - return std::make_unique( - std::move(cfg), context_manager_, *upstream_stats_store, std::vector{}); - } - bool isTLSUpstream(int index) { return index % 2 == 0; } void createUpstreams() override { @@ -147,8 +119,8 @@ require_client_certificate: true auto endpoint = upstream_address_fn_(i); if (isTLSUpstream(i)) { fake_upstreams_.emplace_back(new AutonomousUpstream( - createUpstreamSslContext(), endpoint->ip()->port(), FakeHttpConnection::Type::HTTP1, - endpoint->ip()->version(), timeSystem(), false)); + HttpIntegrationTest::createUpstreamTlsContext(), endpoint->ip()->port(), + FakeHttpConnection::Type::HTTP1, endpoint->ip()->version(), timeSystem(), false)); } else { fake_upstreams_.emplace_back(new AutonomousUpstream( Network::Test::createRawBufferSocketFactory(), endpoint->ip()->port(),