diff --git a/.gitignore b/.gitignore index f316cf83b8b61..242768544b418 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ BROWSE .deps *.pyc SOURCE_VERSION +.cache diff --git a/source/common/config/BUILD b/source/common/config/BUILD index 43eddcb8de6a7..3569b1277c98c 100644 --- a/source/common/config/BUILD +++ b/source/common/config/BUILD @@ -85,6 +85,16 @@ envoy_cc_library( ], ) +envoy_cc_library( + name = "datasource_lib", + srcs = ["datasource.cc"], + hdrs = ["datasource.h"], + deps = [ + "//source/common/filesystem:filesystem_lib", + "@envoy_api//envoy/api/v2/core:base_cc", + ], +) + envoy_cc_library( name = "filter_json_lib", srcs = ["filter_json.cc"], diff --git a/source/common/config/datasource.cc b/source/common/config/datasource.cc new file mode 100644 index 0000000000000..7d8d58375733b --- /dev/null +++ b/source/common/config/datasource.cc @@ -0,0 +1,35 @@ +#include "common/config/datasource.h" + +#include "common/filesystem/filesystem_impl.h" + +#include "fmt/format.h" + +namespace Envoy { +namespace Config { +namespace DataSource { + +std::string read(const envoy::api::v2::core::DataSource& source, bool allow_empty) { + switch (source.specifier_case()) { + case envoy::api::v2::core::DataSource::kFilename: + return Filesystem::fileReadToEnd(source.filename()); + case envoy::api::v2::core::DataSource::kInlineBytes: + return source.inline_bytes(); + case envoy::api::v2::core::DataSource::kInlineString: + return source.inline_string(); + default: + if (!allow_empty) { + throw EnvoyException( + fmt::format("Unexpected DataSource::specifier_case(): {}", source.specifier_case())); + } + return ""; + } +} + +std::string getPath(const envoy::api::v2::core::DataSource& source) { + return source.specifier_case() == envoy::api::v2::core::DataSource::kFilename ? source.filename() + : ""; +} + +} // namespace DataSource +} // namespace Config +} // namespace Envoy diff --git a/source/common/config/datasource.h b/source/common/config/datasource.h new file mode 100644 index 0000000000000..e5c74063155b9 --- /dev/null +++ b/source/common/config/datasource.h @@ -0,0 +1,26 @@ +#pragma once + +#include "envoy/api/v2/core/base.pb.h" + +namespace Envoy { +namespace Config { +namespace DataSource { + +/** + * Read contents of the DataSource. + * @param source data source. + * @param allow_empty return an empty string if no DataSource case is specified. + * @return std::string with DataSource contents. + * @throw EnvoyException if no DataSource case is specified and !allow_empty. + */ +std::string read(const envoy::api::v2::core::DataSource& source, bool allow_empty); + +/** + * @param source data source. + * @return std::string path to DataSource if a filename, otherwise an empty string. + */ +std::string getPath(const envoy::api::v2::core::DataSource& source); + +} // namespace DataSource +} // namespace Config +} // namespace Envoy diff --git a/source/common/grpc/BUILD b/source/common/grpc/BUILD index bbc466cf38f6b..e611ed5ae5a2b 100644 --- a/source/common/grpc/BUILD +++ b/source/common/grpc/BUILD @@ -87,6 +87,7 @@ envoy_cc_library( "//source/common/common:linked_object", "//source/common/common:thread_annotations", "//source/common/common:thread_lib", + "//source/common/config:datasource_lib", "//source/common/tracing:http_tracer_lib", ], ) diff --git a/source/common/grpc/async_client_manager_impl.cc b/source/common/grpc/async_client_manager_impl.cc index d55207ded517f..61913dc577518 100644 --- a/source/common/grpc/async_client_manager_impl.cc +++ b/source/common/grpc/async_client_manager_impl.cc @@ -43,6 +43,7 @@ GoogleAsyncClientFactoryImpl::GoogleAsyncClientFactoryImpl( scope_(scope.createScope(fmt::format("grpc.{}.", config.stat_prefix()))), config_(config) { #ifndef ENVOY_GOOGLE_GRPC UNREFERENCED_PARAMETER(tls_); + UNREFERENCED_PARAMETER(google_tls_slot_); UNREFERENCED_PARAMETER(scope_); UNREFERENCED_PARAMETER(config_); throw EnvoyException("Google C++ gRPC client is not linked"); diff --git a/source/common/grpc/google_async_client_impl.cc b/source/common/grpc/google_async_client_impl.cc index 2ed5c52c6c1ed..4d00b6c794bf7 100644 --- a/source/common/grpc/google_async_client_impl.cc +++ b/source/common/grpc/google_async_client_impl.cc @@ -75,6 +75,7 @@ GoogleAsyncClientImpl::GoogleAsyncClientImpl( } GoogleAsyncClientImpl::~GoogleAsyncClientImpl() { + ENVOY_LOG(debug, "Client teardown, resetting streams"); while (!active_streams_.empty()) { active_streams_.front()->resetStream(); } diff --git a/source/common/grpc/google_async_client_site.cc b/source/common/grpc/google_async_client_site.cc index 026356ef0dc8e..c9fd2d0d9da77 100644 --- a/source/common/grpc/google_async_client_site.cc +++ b/source/common/grpc/google_async_client_site.cc @@ -1,5 +1,6 @@ // Site-specific customizations for Google gRPC client implementation. +#include "common/config/datasource.h" #include "common/grpc/google_async_client_impl.h" #include "grpc++/channel.h" @@ -11,8 +12,18 @@ namespace Grpc { std::shared_ptr GoogleAsyncClientImpl::createChannel(const envoy::api::v2::core::GrpcService::GoogleGrpc& config) { - // TODO(htuch): add support for SSL, OAuth2, GCP, etc. credentials. - std::shared_ptr creds = grpc::InsecureChannelCredentials(); + // TODO(htuch): add support for OAuth2, GCP, etc. credentials. + std::shared_ptr creds; + if (config.has_ssl_credentials()) { + const grpc::SslCredentialsOptions ssl_creds = { + .pem_root_certs = Config::DataSource::read(config.ssl_credentials().root_certs(), true), + .pem_private_key = Config::DataSource::read(config.ssl_credentials().private_key(), true), + .pem_cert_chain = Config::DataSource::read(config.ssl_credentials().cert_chain(), true), + }; + creds = grpc::SslCredentials(ssl_creds); + } else { + creds = grpc::InsecureChannelCredentials(); + } return CreateChannel(config.target_uri(), creds); } diff --git a/source/common/ssl/BUILD b/source/common/ssl/BUILD index 5415f55fee431..8d4c9b2f53ada 100644 --- a/source/common/ssl/BUILD +++ b/source/common/ssl/BUILD @@ -34,6 +34,7 @@ envoy_cc_library( deps = [ "//include/envoy/ssl:context_config_interface", "//source/common/common:assert_lib", + "//source/common/config:datasource_lib", "//source/common/config:tls_context_json_lib", "//source/common/json:json_loader_lib", "//source/common/protobuf:utility_lib", diff --git a/source/common/ssl/context_config_impl.cc b/source/common/ssl/context_config_impl.cc index 3954e93670584..1c1563c4367ef 100644 --- a/source/common/ssl/context_config_impl.cc +++ b/source/common/ssl/context_config_impl.cc @@ -3,8 +3,8 @@ #include #include "common/common/assert.h" +#include "common/config/datasource.h" #include "common/config/tls_context_json.h" -#include "common/filesystem/filesystem_impl.h" #include "common/protobuf/utility.h" #include "openssl/ssl.h" @@ -41,22 +41,28 @@ ContextConfigImpl::ContextConfigImpl(const envoy::api::v2::auth::CommonTlsContex RepeatedPtrUtil::join(config.tls_params().cipher_suites(), ":"), DEFAULT_CIPHER_SUITES)), ecdh_curves_(StringUtil::nonEmptyStringOrDefault( RepeatedPtrUtil::join(config.tls_params().ecdh_curves(), ":"), DEFAULT_ECDH_CURVES)), - ca_cert_(readDataSource(config.validation_context().trusted_ca(), true)), - ca_cert_path_(getDataSourcePath(config.validation_context().trusted_ca())), - certificate_revocation_list_(readDataSource(config.validation_context().crl(), true)), - certificate_revocation_list_path_(getDataSourcePath(config.validation_context().crl())), - cert_chain_(config.tls_certificates().empty() - ? "" - : readDataSource(config.tls_certificates()[0].certificate_chain(), true)), - cert_chain_path_(config.tls_certificates().empty() - ? "" - : getDataSourcePath(config.tls_certificates()[0].certificate_chain())), - private_key_(config.tls_certificates().empty() - ? "" - : readDataSource(config.tls_certificates()[0].private_key(), true)), - private_key_path_(config.tls_certificates().empty() - ? "" - : getDataSourcePath(config.tls_certificates()[0].private_key())), + ca_cert_(Config::DataSource::read(config.validation_context().trusted_ca(), true)), + ca_cert_path_(Config::DataSource::getPath(config.validation_context().trusted_ca())), + certificate_revocation_list_( + Config::DataSource::read(config.validation_context().crl(), true)), + certificate_revocation_list_path_( + Config::DataSource::getPath(config.validation_context().crl())), + cert_chain_( + config.tls_certificates().empty() + ? "" + : Config::DataSource::read(config.tls_certificates()[0].certificate_chain(), true)), + cert_chain_path_( + config.tls_certificates().empty() + ? "" + : Config::DataSource::getPath(config.tls_certificates()[0].certificate_chain())), + private_key_( + config.tls_certificates().empty() + ? "" + : Config::DataSource::read(config.tls_certificates()[0].private_key(), true)), + private_key_path_( + config.tls_certificates().empty() + ? "" + : Config::DataSource::getPath(config.tls_certificates()[0].private_key())), verify_subject_alt_name_list_(config.validation_context().verify_subject_alt_name().begin(), config.validation_context().verify_subject_alt_name().end()), verify_certificate_hash_(config.validation_context().verify_certificate_hash().empty() @@ -74,30 +80,6 @@ ContextConfigImpl::ContextConfigImpl(const envoy::api::v2::auth::CommonTlsContex } } -const std::string ContextConfigImpl::readDataSource(const envoy::api::v2::core::DataSource& source, - bool allow_empty) { - switch (source.specifier_case()) { - case envoy::api::v2::core::DataSource::kFilename: - return Filesystem::fileReadToEnd(source.filename()); - case envoy::api::v2::core::DataSource::kInlineBytes: - return source.inline_bytes(); - case envoy::api::v2::core::DataSource::kInlineString: - return source.inline_string(); - default: - if (!allow_empty) { - throw EnvoyException( - fmt::format("Unexpected DataSource::specifier_case(): {}", source.specifier_case())); - } - return ""; - } -} - -const std::string -ContextConfigImpl::getDataSourcePath(const envoy::api::v2::core::DataSource& source) { - return source.specifier_case() == envoy::api::v2::core::DataSource::kFilename ? source.filename() - : ""; -} - unsigned ContextConfigImpl::tlsVersionFromProto( const envoy::api::v2::auth::TlsParameters_TlsProtocol& version, unsigned default_version) { switch (version) { @@ -143,7 +125,7 @@ ServerContextConfigImpl::ServerContextConfigImpl( switch (config.session_ticket_keys_type_case()) { case envoy::api::v2::auth::DownstreamTlsContext::kSessionTicketKeys: for (const auto& datasource : config.session_ticket_keys().keys()) { - validateAndAppendKey(ret, readDataSource(datasource, false)); + validateAndAppendKey(ret, Config::DataSource::read(datasource, false)); } break; case envoy::api::v2::auth::DownstreamTlsContext::kSessionTicketKeysSdsSecretConfig: diff --git a/source/common/ssl/context_config_impl.h b/source/common/ssl/context_config_impl.h index 13171d60cf034..b075668186c4f 100644 --- a/source/common/ssl/context_config_impl.h +++ b/source/common/ssl/context_config_impl.h @@ -50,10 +50,6 @@ class ContextConfigImpl : public virtual Ssl::ContextConfig { protected: ContextConfigImpl(const envoy::api::v2::auth::CommonTlsContext& config); - static const std::string readDataSource(const envoy::api::v2::core::DataSource& source, - bool allow_empty); - static const std::string getDataSourcePath(const envoy::api::v2::core::DataSource& source); - private: static unsigned tlsVersionFromProto(const envoy::api::v2::auth::TlsParameters_TlsProtocol& version, diff --git a/test/common/grpc/BUILD b/test/common/grpc/BUILD index 62ed8918880f6..820d08bab942c 100644 --- a/test/common/grpc/BUILD +++ b/test/common/grpc/BUILD @@ -81,6 +81,7 @@ envoy_cc_test_library( envoy_cc_test( name = "grpc_client_integration_test", srcs = ["grpc_client_integration_test.cc"], + data = ["//test/config/integration/certs"], deps = [ ":grpc_client_integration_lib", "//source/common/common:enum_to_int", @@ -89,6 +90,9 @@ envoy_cc_test( "//source/common/http/http2:conn_pool_lib", "//source/common/network:connection_lib", "//source/common/network:raw_buffer_socket_lib", + "//source/common/ssl:context_lib", + "//source/common/ssl:context_config_lib", + "//source/common/ssl:ssl_socket_lib", "//source/common/stats:stats_lib", "//test/integration:integration_lib", "//test/mocks/grpc:grpc_mocks", diff --git a/test/common/grpc/grpc_client_integration_test.cc b/test/common/grpc/grpc_client_integration_test.cc index f2350647ebd9d..9c7da9f1e1cd6 100644 --- a/test/common/grpc/grpc_client_integration_test.cc +++ b/test/common/grpc/grpc_client_integration_test.cc @@ -9,6 +9,9 @@ #include "common/http/http2/conn_pool.h" #include "common/network/connection_impl.h" #include "common/network/raw_buffer_socket.h" +#include "common/ssl/context_config_impl.h" +#include "common/ssl/context_manager_impl.h" +#include "common/ssl/ssl_socket.h" #include "common/stats/stats_impl.h" #include "test/common/grpc/grpc_client_integration.h" @@ -205,8 +208,13 @@ class HelloworldRequest : public MockAsyncRequestCallbacksFindMethodByName("SayHello")), - fake_upstream_(new FakeUpstream(0, FakeHttpConnection::Type::HTTP2, ipVersion())) { + : method_descriptor_(helloworld::Greeter::descriptor()->FindMethodByName("SayHello")) {} + + virtual void initialize() { + if (fake_upstream_ == nullptr) { + fake_upstream_ = + std::make_unique(0, FakeHttpConnection::Type::HTTP2, ipVersion()); + } switch (clientType()) { case ClientType::EnvoyGrpc: grpc_client_ = createAsyncClientImpl(); @@ -237,7 +245,9 @@ class GrpcClientIntegrationTest : public GrpcClientIntegrationParamTest { AsyncClientPtr createAsyncClientImpl() { client_connection_ = std::make_unique( dispatcher_, fake_upstream_->localAddress(), nullptr, - std::make_unique(), nullptr); + std::move(async_client_transport_socket_), nullptr); + ON_CALL(*mock_cluster_info_, connectTimeout()) + .WillByDefault(Return(std::chrono::milliseconds(1000))); EXPECT_CALL(*mock_cluster_info_, name()).WillRepeatedly(ReturnRef(fake_cluster_name_)); EXPECT_CALL(cm_, get(_)).WillRepeatedly(Return(&thread_local_cluster_)); EXPECT_CALL(thread_local_cluster_, info()).WillRepeatedly(Return(cluster_info_ptr_)); @@ -258,15 +268,19 @@ class GrpcClientIntegrationTest : public GrpcClientIntegrationParamTest { return std::make_unique(cm_, fake_cluster_name_); } - AsyncClientPtr createGoogleAsyncClientImpl() { -#ifdef ENVOY_GOOGLE_GRPC + virtual envoy::api::v2::core::GrpcService::GoogleGrpc createGoogleGrpcConfig() { envoy::api::v2::core::GrpcService::GoogleGrpc config; config.set_target_uri(fake_upstream_->localAddress()->asString()); config.set_stat_prefix("fake_cluster"); + return config; + } + + AsyncClientPtr createGoogleAsyncClientImpl() { +#ifdef ENVOY_GOOGLE_GRPC google_tls_ = std::make_unique(); GoogleGenericStubFactory stub_factory; return std::make_unique(dispatcher_, *google_tls_, stub_factory, - stats_store_, config); + stats_store_, createGoogleGrpcConfig()); #else NOT_REACHED; #endif @@ -360,6 +374,7 @@ class GrpcClientIntegrationTest : public GrpcClientIntegrationParamTest { const TestMetadata empty_metadata_; // Fake/mock infrastructure for Grpc::AsyncClientImpl upstream. + Network::TransportSocketPtr async_client_transport_socket_{new Network::RawBufferSocket()}; const std::string fake_cluster_name_{"fake_cluster"}; Upstream::MockClusterManager cm_; Upstream::MockClusterInfo* mock_cluster_info_ = new NiceMock(); @@ -387,6 +402,7 @@ INSTANTIATE_TEST_CASE_P(IpVersionsClientType, GrpcClientIntegrationTest, // Validate that a simple request-reply stream works. TEST_P(GrpcClientIntegrationTest, BasicStream) { + initialize(); auto stream = createStream(empty_metadata_); stream->sendRequest(); stream->sendServerInitialMetadata(empty_metadata_); @@ -397,6 +413,7 @@ TEST_P(GrpcClientIntegrationTest, BasicStream) { // Validate that a client destruction with open streams cleans up appropriately. TEST_P(GrpcClientIntegrationTest, ClientDestruct) { + initialize(); auto stream = createStream(empty_metadata_); stream->sendRequest(); grpc_client_.reset(); @@ -404,6 +421,7 @@ TEST_P(GrpcClientIntegrationTest, ClientDestruct) { // Validate that a simple request-reply unary RPC works. TEST_P(GrpcClientIntegrationTest, BasicRequest) { + initialize(); auto request = createRequest(empty_metadata_); request->sendReply(); dispatcher_helper_.runDispatcher(); @@ -411,6 +429,7 @@ TEST_P(GrpcClientIntegrationTest, BasicRequest) { // Validate that multiple streams work. TEST_P(GrpcClientIntegrationTest, MultiStream) { + initialize(); auto stream_0 = createStream(empty_metadata_); auto stream_1 = createStream(empty_metadata_); stream_0->sendRequest(); @@ -424,6 +443,7 @@ TEST_P(GrpcClientIntegrationTest, MultiStream) { // Validate that multiple request-reply unary RPCs works. TEST_P(GrpcClientIntegrationTest, MultiRequest) { + initialize(); auto request_0 = createRequest(empty_metadata_); auto request_1 = createRequest(empty_metadata_); request_1->sendReply(); @@ -433,6 +453,7 @@ TEST_P(GrpcClientIntegrationTest, MultiRequest) { // Validate that a non-200 HTTP status results in the expected gRPC error. TEST_P(GrpcClientIntegrationTest, HttpNon200Status) { + initialize(); for (const auto http_response_status : {400, 401, 403, 404, 429, 431}) { auto stream = createStream(empty_metadata_); const Http::TestHeaderMapImpl reply_headers{{":status", std::to_string(http_response_status)}}; @@ -450,6 +471,7 @@ TEST_P(GrpcClientIntegrationTest, HttpNon200Status) { // Validate that a non-200 HTTP status results in fallback to grpc-status. TEST_P(GrpcClientIntegrationTest, GrpcStatusFallback) { + initialize(); auto stream = createStream(empty_metadata_); const Http::TestHeaderMapImpl reply_headers{ {":status", "404"}, @@ -464,6 +486,7 @@ TEST_P(GrpcClientIntegrationTest, GrpcStatusFallback) { // Validate that a HTTP-level reset is handled as an INTERNAL gRPC error. TEST_P(GrpcClientIntegrationTest, HttpReset) { + initialize(); auto stream = createStream(empty_metadata_); stream->sendServerInitialMetadata(empty_metadata_); dispatcher_helper_.runDispatcher(); @@ -476,6 +499,7 @@ TEST_P(GrpcClientIntegrationTest, HttpReset) { // Validate that a reply with bad gRPC framing (compressed frames with Envoy // client) is handled as an INTERNAL gRPC error. TEST_P(GrpcClientIntegrationTest, BadReplyGrpcFraming) { + initialize(); // Only testing behavior of Envoy client, since Google client handles // compressed frames. SKIP_IF_GRPC_CLIENT(ClientType::GoogleGrpc); @@ -491,6 +515,7 @@ TEST_P(GrpcClientIntegrationTest, BadReplyGrpcFraming) { // Validate that a reply with bad protobuf is handled as an INTERNAL gRPC error. TEST_P(GrpcClientIntegrationTest, BadReplyProtobuf) { + initialize(); auto stream = createStream(empty_metadata_); stream->sendRequest(); stream->sendServerInitialMetadata(empty_metadata_); @@ -504,6 +529,7 @@ TEST_P(GrpcClientIntegrationTest, BadReplyProtobuf) { // Validate that an out-of-range gRPC status is handled as an INVALID_CODE gRPC // error. TEST_P(GrpcClientIntegrationTest, OutOfRangeGrpcStatus) { + initialize(); // TODO(htuch): there is an UBSAN issue with Google gRPC client library // handling of out-of-range status codes, see // https://circleci.com/gh/envoyproxy/envoy/20234?utm_campaign=vcs-integration-link&utm_medium=referral&utm_source=github-build-link @@ -522,6 +548,7 @@ TEST_P(GrpcClientIntegrationTest, OutOfRangeGrpcStatus) { // Validate that a missing gRPC status is handled as an UNKNOWN gRPC error. TEST_P(GrpcClientIntegrationTest, MissingGrpcStatus) { + initialize(); auto stream = createStream(empty_metadata_); stream->sendServerInitialMetadata(empty_metadata_); stream->sendReply(); @@ -535,6 +562,7 @@ TEST_P(GrpcClientIntegrationTest, MissingGrpcStatus) { // Validate that a reply terminated without trailers is handled as a gRPC error. TEST_P(GrpcClientIntegrationTest, ReplyNoTrailers) { + initialize(); auto stream = createStream(empty_metadata_); stream->sendRequest(); stream->sendServerInitialMetadata(empty_metadata_); @@ -552,6 +580,7 @@ TEST_P(GrpcClientIntegrationTest, ReplyNoTrailers) { // Validate that send client initial metadata works. TEST_P(GrpcClientIntegrationTest, StreamClientInitialMetadata) { + initialize(); const TestMetadata initial_metadata = { {Http::LowerCaseString("foo"), "bar"}, {Http::LowerCaseString("baz"), "blah"}, @@ -563,6 +592,7 @@ TEST_P(GrpcClientIntegrationTest, StreamClientInitialMetadata) { // Validate that send client initial metadata works. TEST_P(GrpcClientIntegrationTest, RequestClientInitialMetadata) { + initialize(); const TestMetadata initial_metadata = { {Http::LowerCaseString("foo"), "bar"}, {Http::LowerCaseString("baz"), "blah"}, @@ -574,6 +604,7 @@ TEST_P(GrpcClientIntegrationTest, RequestClientInitialMetadata) { // Validate that receiving server initial metadata works. TEST_P(GrpcClientIntegrationTest, ServerInitialMetadata) { + initialize(); auto stream = createStream(empty_metadata_); stream->sendRequest(); const TestMetadata initial_metadata = { @@ -588,6 +619,7 @@ TEST_P(GrpcClientIntegrationTest, ServerInitialMetadata) { // Validate that receiving server trailing metadata works. TEST_P(GrpcClientIntegrationTest, ServerTrailingMetadata) { + initialize(); auto stream = createStream(empty_metadata_); stream->sendRequest(); stream->sendServerInitialMetadata(empty_metadata_); @@ -602,6 +634,7 @@ TEST_P(GrpcClientIntegrationTest, ServerTrailingMetadata) { // Validate that a trailers-only response is handled for streams. TEST_P(GrpcClientIntegrationTest, StreamTrailersOnly) { + initialize(); auto stream = createStream(empty_metadata_); stream->sendServerTrailers(Status::GrpcStatus::Ok, "", empty_metadata_, true); dispatcher_helper_.runDispatcher(); @@ -610,6 +643,7 @@ TEST_P(GrpcClientIntegrationTest, StreamTrailersOnly) { // Validate that a trailers-only response is handled for requests, where it is // an error. TEST_P(GrpcClientIntegrationTest, RequestTrailersOnly) { + initialize(); auto request = createRequest(empty_metadata_); const Http::TestHeaderMapImpl reply_headers{{":status", "200"}, {"grpc-status", "0"}}; EXPECT_CALL(*request->child_span_, setTag(Tracing::Tags::get().GRPC_STATUS_CODE, "0")); @@ -623,6 +657,7 @@ TEST_P(GrpcClientIntegrationTest, RequestTrailersOnly) { // Validate that a trailers RESOURCE_EXHAUSTED reply is handled. TEST_P(GrpcClientIntegrationTest, ResourceExhaustedError) { + initialize(); auto stream = createStream(empty_metadata_); stream->sendServerInitialMetadata(empty_metadata_); stream->sendReply(); @@ -634,6 +669,7 @@ TEST_P(GrpcClientIntegrationTest, ResourceExhaustedError) { // Validate that we can continue to receive after a local close. TEST_P(GrpcClientIntegrationTest, ReceiveAfterLocalClose) { + initialize(); auto stream = createStream(empty_metadata_); stream->sendRequest(true); stream->sendServerInitialMetadata(empty_metadata_); @@ -644,6 +680,7 @@ TEST_P(GrpcClientIntegrationTest, ReceiveAfterLocalClose) { // Validate that reset() doesn't explode on a half-closed stream (local). TEST_P(GrpcClientIntegrationTest, ResetAfterCloseLocal) { + initialize(); auto stream = createStream(empty_metadata_); stream->grpc_stream_->closeStream(); stream->fake_stream_->waitForEndStream(dispatcher_helper_.dispatcher_); @@ -654,6 +691,7 @@ TEST_P(GrpcClientIntegrationTest, ResetAfterCloseLocal) { // Validate that request cancel() works. TEST_P(GrpcClientIntegrationTest, CancelRequest) { + initialize(); auto request = createRequest(empty_metadata_); EXPECT_CALL(*request->child_span_, setTag(Tracing::Tags::get().STATUS, Tracing::Tags::get().CANCELED)); @@ -663,6 +701,106 @@ TEST_P(GrpcClientIntegrationTest, CancelRequest) { request->fake_stream_->waitForReset(); } +// SSL connection credential validation tests. +class GrpcSslClientIntegrationTest : public GrpcClientIntegrationTest { +public: + void TearDown() override { + // Reset some state in the superclass before we destruct context_manager_ in our destructor, it + // doesn't like dangling contexts at destruction. + fake_upstream_.reset(); + client_connection_.reset(); + mock_cluster_info_->transport_socket_factory_.reset(); + } + + virtual envoy::api::v2::core::GrpcService::GoogleGrpc createGoogleGrpcConfig() override { + auto config = GrpcClientIntegrationTest::createGoogleGrpcConfig(); + auto* ssl_creds = config.mutable_ssl_credentials(); + ssl_creds->mutable_root_certs()->set_filename( + TestEnvironment::runfilesPath("test/config/integration/certs/upstreamcacert.pem")); + if (use_client_cert_) { + ssl_creds->mutable_private_key()->set_filename( + TestEnvironment::runfilesPath("test/config/integration/certs/clientkey.pem")); + ssl_creds->mutable_cert_chain()->set_filename( + TestEnvironment::runfilesPath("test/config/integration/certs/clientcert.pem")); + } + return config; + } + + void initialize() override { + envoy::api::v2::auth::UpstreamTlsContext tls_context; + auto* common_tls_context = tls_context.mutable_common_tls_context(); + auto* validation_context = common_tls_context->mutable_validation_context(); + validation_context->mutable_trusted_ca()->set_filename( + TestEnvironment::runfilesPath("test/config/integration/certs/upstreamcacert.pem")); + if (use_client_cert_) { + auto* tls_cert = common_tls_context->add_tls_certificates(); + tls_cert->mutable_certificate_chain()->set_filename( + TestEnvironment::runfilesPath("test/config/integration/certs/clientcert.pem")); + tls_cert->mutable_private_key()->set_filename( + TestEnvironment::runfilesPath("test/config/integration/certs/clientkey.pem")); + } + Ssl::ClientContextConfigImpl cfg(tls_context); + + mock_cluster_info_->transport_socket_factory_ = + std::make_unique(cfg, context_manager_, stats_store_); + ON_CALL(*mock_cluster_info_, transportSocketFactory()) + .WillByDefault(ReturnRef(*mock_cluster_info_->transport_socket_factory_)); + async_client_transport_socket_ = + mock_cluster_info_->transport_socket_factory_->createTransportSocket(); + fake_upstream_ = std::make_unique(createUpstreamSslContext(), 0, + FakeHttpConnection::Type::HTTP2, ipVersion()); + + GrpcClientIntegrationTest::initialize(); + } + + Network::TransportSocketFactoryPtr createUpstreamSslContext() { + envoy::api::v2::auth::DownstreamTlsContext tls_context; + auto* common_tls_context = tls_context.mutable_common_tls_context(); + common_tls_context->add_alpn_protocols("h2"); + auto* tls_cert = common_tls_context->add_tls_certificates(); + tls_cert->mutable_certificate_chain()->set_filename( + TestEnvironment::runfilesPath("test/config/integration/certs/upstreamcert.pem")); + tls_cert->mutable_private_key()->set_filename( + TestEnvironment::runfilesPath("test/config/integration/certs/upstreamkey.pem")); + if (use_client_cert_) { + tls_context.mutable_require_client_certificate()->set_value(true); + auto* validation_context = common_tls_context->mutable_validation_context(); + validation_context->mutable_trusted_ca()->set_filename( + TestEnvironment::runfilesPath("test/config/integration/certs/cacert.pem")); + } + Ssl::ServerContextConfigImpl cfg(tls_context); + + static Stats::Scope* upstream_stats_store = new Stats::IsolatedStoreImpl(); + return std::make_unique(cfg, EMPTY_STRING, + std::vector{}, true, + context_manager_, *upstream_stats_store); + } + + bool use_client_cert_{}; + Ssl::ContextManagerImpl context_manager_{runtime_}; +}; + +// Parameterize the loopback test server socket address and gRPC client type. +INSTANTIATE_TEST_CASE_P(SslIpVersionsClientType, GrpcSslClientIntegrationTest, + GRPC_CLIENT_INTEGRATION_PARAMS); + +// Validate that a simple request-reply unary RPC works with SSL. +TEST_P(GrpcSslClientIntegrationTest, BasicSslRequest) { + initialize(); + auto request = createRequest(empty_metadata_); + request->sendReply(); + dispatcher_helper_.runDispatcher(); +} + +// Validate that a simple request-reply unary RPC works with SSL + client certs. +TEST_P(GrpcSslClientIntegrationTest, BasicSslRequestWithClientCert) { + use_client_cert_ = true; + initialize(); + auto request = createRequest(empty_metadata_); + request->sendReply(); + dispatcher_helper_.runDispatcher(); +} + } // namespace } // namespace Grpc } // namespace Envoy diff --git a/test/config/integration/certs/upstreamcert.cfg b/test/config/integration/certs/upstreamcert.cfg index 5ce155873d4d3..4c6c2985ac486 100644 --- a/test/config/integration/certs/upstreamcert.cfg +++ b/test/config/integration/certs/upstreamcert.cfg @@ -34,3 +34,5 @@ authorityKeyIdentifier = keyid:always [alt_names] DNS.1 = *.lyft.com +IP.1 = 0.0.0.0 +IP.2 = :: diff --git a/test/config/integration/certs/upstreamcert.pem b/test/config/integration/certs/upstreamcert.pem index a92bec5e00c0c..1e624ee67f582 100644 --- a/test/config/integration/certs/upstreamcert.pem +++ b/test/config/integration/certs/upstreamcert.pem @@ -1,19 +1,20 @@ -----BEGIN CERTIFICATE----- -MIIDFjCCAn+gAwIBAgIJAIEoeL+knpI0MA0GCSqGSIb3DQEBCwUAMH8xCzAJBgNV +MIIDLjCCApegAwIBAgIJAMgAHjgWLVToMA0GCSqGSIb3DQEBCwUAMH8xCzAJBgNV BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1TYW4gRnJhbmNp c2NvMQ0wCwYDVQQKEwRMeWZ0MRkwFwYDVQQLExBMeWZ0IEVuZ2luZWVyaW5nMRkw -FwYDVQQDExBUZXN0IFVwc3RyZWFtIENBMB4XDTE3MDcwOTAxMzkzMloXDTE5MDcw -OTAxMzkzMlowgYMxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYw -FAYDVQQHEw1TYW4gRnJhbmNpc2NvMQ0wCwYDVQQKEwRMeWZ0MRkwFwYDVQQLExBM -eWZ0IEVuZ2luZWVyaW5nMR0wGwYDVQQDExRUZXN0IFVwc3RyZWFtIFNlcnZlcjCB +FwYDVQQDExBUZXN0IFVwc3RyZWFtIENBMB4XDTE4MDIxMzE4NDY0N1oXDTIwMDIx +MzE4NDY0N1owgYMxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYw +FAYDVQQHDA1TYW4gRnJhbmNpc2NvMQ0wCwYDVQQKDARMeWZ0MRkwFwYDVQQLDBBM +eWZ0IEVuZ2luZWVyaW5nMR0wGwYDVQQDDBRUZXN0IFVwc3RyZWFtIFNlcnZlcjCB nzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAu7BZ855UTUGLBbSIG7NbmOHnit0k qhVaS7qf56wuGybRyoS82vUKKF+HG84DJZGjPCCzr9sjgPuDI2ebWf28YScS/K/v VVVk4mdJcpL8LSKF9j2wWjD1fk/soLAfeMgs3GvXBXk1GsJ+PzCr6jKJ+EqghKpV -3snf6/leRi0gBrMCAwEAAaOBlDCBkTAMBgNVHRMBAf8EAjAAMAsGA1UdDwQEAwIF -4DAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwEwFQYDVR0RBA4wDIIKKi5s -eWZ0LmNvbTAdBgNVHQ4EFgQUL4L/GnkQef+2KrADZK0zecZe84MwHwYDVR0jBBgw -FoAUN3V9rh1fRnJU2AhMHtL1DVgUMGkwDQYJKoZIhvcNAQELBQADgYEAvE6Hoguo -ImLP3WPBzW5WuvWwheG2QOlCEo9gP3V/C4ptAr92znG4Jrsp72S86LFdFYR+ZKgK -4QRwtoPyVIb6cGK+N70TE/21Z3jWuA5gvezKT3XnGy+RhPSapLAjjafuvYsVUPv9 -aGPmBCsMiipz9dWjLxVTF9nUNZeRLnxbLSk= +3snf6/leRi0gBrMCAwEAAaOBrDCBqTAMBgNVHRMBAf8EAjAAMAsGA1UdDwQEAwIF +4DAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwEwLQYDVR0RBCYwJIIKKi5s +eWZ0LmNvbYcEAAAAAIcQAAAAAAAAAAAAAAAAAAAAADAdBgNVHQ4EFgQUL4L/GnkQ +ef+2KrADZK0zecZe84MwHwYDVR0jBBgwFoAUN3V9rh1fRnJU2AhMHtL1DVgUMGkw +DQYJKoZIhvcNAQELBQADgYEApWzNKgGEY5RZzzXx8n4dvP05Co61fFE1pBlBS4xp +lEh/7umKpJzV1YpRIUlwwYO/PZqP5Eh99/iC2ozxFeVZwjGht/mvyxN2pzWADiuR +4wD+XsjcUh7j6OxPDMsz/s332oZ2Jn3kpjgOv44xKexPp86td9rl49KwETkrl49X +Q5I= -----END CERTIFICATE----- diff --git a/test/integration/BUILD b/test/integration/BUILD index 314d1f152cf00..81a3911d88a2b 100644 --- a/test/integration/BUILD +++ b/test/integration/BUILD @@ -26,13 +26,20 @@ envoy_cc_test( envoy_cc_test( name = "ads_integration_test", srcs = ["ads_integration_test.cc"], - data = ["//test/config/integration:server_ads.yaml"], + data = [ + "//test/config/integration:server_ads.yaml", + "//test/config/integration/certs", + ], deps = [ ":http_integration_lib", "//source/common/config:protobuf_link_hacks", "//source/common/config:resources_lib", "//source/common/protobuf:utility_lib", + "//source/common/ssl:context_config_lib", + "//source/common/ssl:context_lib", + "//source/common/ssl:ssl_socket_lib", "//test/common/grpc:grpc_client_integration_lib", + "//test/mocks/runtime:runtime_mocks", "//test/test_common:network_utility_lib", "//test/test_common:utility_lib", "@envoy_api//envoy/api/v2:cds_cc", diff --git a/test/integration/ads_integration_test.cc b/test/integration/ads_integration_test.cc index 5095c584a5d20..9699bcb86da35 100644 --- a/test/integration/ads_integration_test.cc +++ b/test/integration/ads_integration_test.cc @@ -9,10 +9,14 @@ #include "common/config/protobuf_link_hacks.h" #include "common/config/resources.h" #include "common/protobuf/utility.h" +#include "common/ssl/context_config_impl.h" +#include "common/ssl/context_manager_impl.h" +#include "common/ssl/ssl_socket.h" #include "test/common/grpc/grpc_client_integration.h" #include "test/integration/http_integration.h" #include "test/integration/utility.h" +#include "test/mocks/runtime/mocks.h" #include "test/test_common/network_utility.h" #include "test/test_common/utility.h" @@ -33,7 +37,7 @@ const std::string config = R"EOF( api_type: GRPC static_resources: clusters: - name: ads_cluster + name: dummy_cluster connect_timeout: { seconds: 5 } type: STATIC hosts: @@ -59,6 +63,29 @@ class AdsIntegrationTest : public HttpIntegrationTest, public Grpc::GrpcClientIn fake_upstreams_.clear(); } + void createUpstreams() override { + HttpIntegrationTest::createUpstreams(); + fake_upstreams_.emplace_back( + new FakeUpstream(createUpstreamSslContext(), 0, FakeHttpConnection::Type::HTTP2, version_)); + } + + Network::TransportSocketFactoryPtr createUpstreamSslContext() { + envoy::api::v2::auth::DownstreamTlsContext tls_context; + auto* common_tls_context = tls_context.mutable_common_tls_context(); + common_tls_context->add_alpn_protocols("h2"); + auto* tls_cert = common_tls_context->add_tls_certificates(); + tls_cert->mutable_certificate_chain()->set_filename( + TestEnvironment::runfilesPath("test/config/integration/certs/upstreamcert.pem")); + tls_cert->mutable_private_key()->set_filename( + TestEnvironment::runfilesPath("test/config/integration/certs/upstreamkey.pem")); + Ssl::ServerContextConfigImpl cfg(tls_context); + + static Stats::Scope* upstream_stats_store = new Stats::TestIsolatedStoreImpl(); + return std::make_unique(cfg, EMPTY_STRING, + std::vector{}, true, + context_manager_, *upstream_stats_store); + } + AssertionResult compareDiscoveryRequest(const std::string& expected_type_url, const std::string& expected_version, const std::vector& expected_resource_names) { @@ -172,17 +199,34 @@ class AdsIntegrationTest : public HttpIntegrationTest, public Grpc::GrpcClientIn void initialize() override { config_helper_.addConfigModifier([this](envoy::config::bootstrap::v2::Bootstrap& bootstrap) { - setGrpcService( - *bootstrap.mutable_dynamic_resources()->mutable_ads_config()->add_grpc_services(), - "ads_cluster", fake_upstreams_.back()->localAddress()); + auto* grpc_service = + bootstrap.mutable_dynamic_resources()->mutable_ads_config()->add_grpc_services(); + setGrpcService(*grpc_service, "ads_cluster", fake_upstreams_.back()->localAddress()); + auto* ads_cluster = bootstrap.mutable_static_resources()->add_clusters(); + ads_cluster->MergeFrom(bootstrap.static_resources().clusters()[0]); + ads_cluster->set_name("ads_cluster"); + auto* context = ads_cluster->mutable_tls_context(); + auto* validation_context = + context->mutable_common_tls_context()->mutable_validation_context(); + validation_context->mutable_trusted_ca()->set_filename( + TestEnvironment::runfilesPath("test/config/integration/certs/upstreamcacert.pem")); + validation_context->add_verify_subject_alt_name("foo.lyft.com"); + if (clientType() == Grpc::ClientType::GoogleGrpc) { + auto* google_grpc = grpc_service->mutable_google_grpc(); + auto* ssl_creds = google_grpc->mutable_ssl_credentials(); + ssl_creds->mutable_root_certs()->set_filename( + TestEnvironment::runfilesPath("test/config/integration/certs/upstreamcacert.pem")); + } }); setUpstreamProtocol(FakeHttpConnection::Type::HTTP2); HttpIntegrationTest::initialize(); - ads_connection_ = fake_upstreams_[0]->waitForHttpConnection(*dispatcher_); + ads_connection_ = fake_upstreams_[1]->waitForHttpConnection(*dispatcher_); ads_stream_ = ads_connection_->waitForNewStream(*dispatcher_); ads_stream_->startGrpcStream(); } + Runtime::MockLoader runtime_; + Ssl::ContextManagerImpl context_manager_{runtime_}; FakeHttpConnectionPtr ads_connection_; FakeStreamPtr ads_stream_; }; diff --git a/test/integration/integration.cc b/test/integration/integration.cc index 6ad7fcc3dab47..0939084bb1bfa 100644 --- a/test/integration/integration.cc +++ b/test/integration/integration.cc @@ -273,7 +273,7 @@ void BaseIntegrationTest::setUpstreamProtocol(FakeHttpConnection::Type protocol) if (upstream_protocol_ == FakeHttpConnection::Type::HTTP2) { config_helper_.addConfigModifier( [&](envoy::config::bootstrap::v2::Bootstrap& bootstrap) -> void { - RELEASE_ASSERT(bootstrap.mutable_static_resources()->clusters_size() == 1); + RELEASE_ASSERT(bootstrap.mutable_static_resources()->clusters_size() >= 1); auto* cluster = bootstrap.mutable_static_resources()->mutable_clusters(0); cluster->mutable_http2_protocol_options(); });