diff --git a/api/contrib/envoy/extensions/filters/network/postgres_proxy/v3alpha/postgres_proxy.proto b/api/contrib/envoy/extensions/filters/network/postgres_proxy/v3alpha/postgres_proxy.proto index 114e97ca7e487..c7be9af020f64 100644 --- a/api/contrib/envoy/extensions/filters/network/postgres_proxy/v3alpha/postgres_proxy.proto +++ b/api/contrib/envoy/extensions/filters/network/postgres_proxy/v3alpha/postgres_proxy.proto @@ -20,6 +20,16 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // [#extension: envoy.filters.network.postgres_proxy] message PostgresProxy { + // Upstream SSL operational modes. + enum SSLMode { + // Do not encrypt upstream connection to the server. + DISABLE = 0; + + // Establish upstream SSL connection to the server. If the server does not + // accept the request for SSL connection, the session is terminated. + REQUIRE = 1; + } + // The human readable prefix to use when emitting :ref:`statistics // `. string stat_prefix = 1 [(validate.rules).string = {min_len: 1}]; @@ -39,4 +49,12 @@ message PostgresProxy { // Refer to official documentation for details // `SSL Session Encryption Message Flow `_. bool terminate_ssl = 3; + + // Controls whether to establish upstream SSL connection to the server. + // Envoy will try to establish upstream SSL connection to the server only when + // Postgres filter is able to read Postgres payload in clear-text. It happens when + // a client established a clear-text connection to Envoy or when a client established + // SSL connection to Envoy and Postgres filter is configured to terminate SSL. + // Defaults to SSL_DISABLE. + SSLMode upstream_ssl = 4; } diff --git a/changelogs/current.yaml b/changelogs/current.yaml index 1b6bed936f34d..126ae4fd9346a 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -187,6 +187,9 @@ new_features: - area: matching change: | support filter chain selection based on the dynamic metadata and the filter state using :ref:`formatter actions `. +- area: postgres + change: | + added support for upstream SSL. - area: redis change: | extended :ref:`cluster support ` by adding a :ref:`dns_cache_config ` option that can be used to resolve hostnames returned by MOVED/ASK responses. diff --git a/contrib/postgres_proxy/filters/network/source/BUILD b/contrib/postgres_proxy/filters/network/source/BUILD index 4fd7bb97717a5..c4b5dce9470d4 100644 --- a/contrib/postgres_proxy/filters/network/source/BUILD +++ b/contrib/postgres_proxy/filters/network/source/BUILD @@ -37,6 +37,7 @@ envoy_cc_library( "//source/common/buffer:buffer_lib", "//source/common/network:filter_lib", "//source/extensions/filters/network:well_known_names", + "@envoy_api//contrib/envoy/extensions/filters/network/postgres_proxy/v3alpha:pkg_cc_proto", ], ) diff --git a/contrib/postgres_proxy/filters/network/source/config.cc b/contrib/postgres_proxy/filters/network/source/config.cc index ff19cbe37c0b2..e663c02f1ccef 100644 --- a/contrib/postgres_proxy/filters/network/source/config.cc +++ b/contrib/postgres_proxy/filters/network/source/config.cc @@ -19,6 +19,7 @@ NetworkFilters::PostgresProxy::PostgresConfigFactory::createFilterFactoryFromPro config_options.enable_sql_parsing_ = PROTOBUF_GET_WRAPPED_OR_DEFAULT(proto_config, enable_sql_parsing, true); config_options.terminate_ssl_ = proto_config.terminate_ssl(); + config_options.upstream_ssl_ = proto_config.upstream_ssl(); PostgresFilterConfigSharedPtr filter_config( std::make_shared(config_options, context.scope())); diff --git a/contrib/postgres_proxy/filters/network/source/postgres_decoder.cc b/contrib/postgres_proxy/filters/network/source/postgres_decoder.cc index b54935cf36f30..79951a7200934 100644 --- a/contrib/postgres_proxy/filters/network/source/postgres_decoder.cc +++ b/contrib/postgres_proxy/filters/network/source/postgres_decoder.cc @@ -191,6 +191,8 @@ Decoder::Result DecoderImpl::onData(Buffer::Instance& data, bool frontend) { return onDataIgnore(data, frontend); case State::InSyncState: return onDataInSync(data, frontend); + case State::NegotiatingUpstreamSSL: + return onDataInNegotiating(data, frontend); default: PANIC("not implemented"); } @@ -240,7 +242,6 @@ Decoder::Result DecoderImpl::onDataInit(Buffer::Instance& data, bool) { Decoder::Result result = Decoder::Result::ReadyForNext; uint32_t code = data.peekBEInt(4); - data.drain(4); // Startup message with 1234 in the most significant 16 bits // indicate request to encrypt. if (code >= 0x04d20000) { @@ -268,8 +269,24 @@ Decoder::Result DecoderImpl::onDataInit(Buffer::Instance& data, bool) { } } else { ENVOY_LOG(debug, "Detected version {}.{} of Postgres", code >> 16, code & 0x0000FFFF); - state_ = State::InSyncState; + if (callbacks_->shouldEncryptUpstream()) { + // Copy the received initial request. + temp_storage_.add(data.linearize(data.length()), data.length()); + // Send SSL request to upstream. + Buffer::OwnedImpl ssl_request; + uint32_t len = 8; + ssl_request.writeBEInt(len); + uint32_t ssl_code = 0x04d2162f; + ssl_request.writeBEInt(ssl_code); + + callbacks_->sendUpstream(ssl_request); + result = Decoder::Result::Stopped; + state_ = State::NegotiatingUpstreamSSL; + } else { + state_ = State::InSyncState; + } } + data.drain(4); processMessageBody(data, FRONTEND, message_len_ - 4, first_, msgParser); data.drain(message_len_); @@ -412,6 +429,43 @@ void DecoderImpl::decodeBackendStatements() { } } +Decoder::Result DecoderImpl::onDataInNegotiating(Buffer::Instance& data, bool frontend) { + if (frontend) { + // No data from downstream is allowed when negotiating upstream SSL + // with the server. + data.drain(data.length()); + state_ = State::OutOfSyncState; + return Decoder::Result::ReadyForNext; + } + + // This should be reply from the server indicating if it accepted + // request to use SSL. It is only one character long packet, where + // 'S' means use SSL, 'E' means do not use. + + // Indicate to the filter, the response and give the initial + // packet temporarily buffered to be sent upstream. + bool upstreamSSL = false; + state_ = State::InitState; + if (data.length() == 1) { + const char c = data.peekInt(0); + if (c == 'S') { + upstreamSSL = true; + } else { + if (c != 'E') { + state_ = State::OutOfSyncState; + } + } + } else { + state_ = State::OutOfSyncState; + } + + data.drain(data.length()); + + callbacks_->encryptUpstream(upstreamSSL, temp_storage_); + + return Decoder::Result::Stopped; +} + // Method is called when X (Terminate) message // is encountered by the decoder. void DecoderImpl::decodeFrontendTerminate() { diff --git a/contrib/postgres_proxy/filters/network/source/postgres_decoder.h b/contrib/postgres_proxy/filters/network/source/postgres_decoder.h index 2d4dffcb41e4c..d33c99697504d 100644 --- a/contrib/postgres_proxy/filters/network/source/postgres_decoder.h +++ b/contrib/postgres_proxy/filters/network/source/postgres_decoder.h @@ -44,6 +44,9 @@ class DecoderCallbacks { virtual void processQuery(const std::string&) PURE; virtual bool onSSLRequest() PURE; + virtual bool shouldEncryptUpstream() const PURE; + virtual void sendUpstream(Buffer::Instance&) PURE; + virtual void encryptUpstream(bool, Buffer::Instance&) PURE; }; // Postgres message decoder. @@ -88,7 +91,13 @@ class DecoderImpl : public Decoder, Logger::Loggable { bool encrypted() const { return encrypted_; } - enum class State { InitState, InSyncState, OutOfSyncState, EncryptedState }; + enum class State { + InitState, + InSyncState, + OutOfSyncState, + EncryptedState, + NegotiatingUpstreamSSL + }; State state() const { return state_; } void state(State state) { state_ = state; } @@ -98,6 +107,7 @@ class DecoderImpl : public Decoder, Logger::Loggable { Result onDataInit(Buffer::Instance& data, bool frontend); Result onDataInSync(Buffer::Instance& data, bool frontend); Result onDataIgnore(Buffer::Instance& data, bool frontend); + Result onDataInNegotiating(Buffer::Instance& data, bool frontend); // MsgAction defines the Decoder's method which will be invoked // when a specific message has been decoded. @@ -188,6 +198,11 @@ class DecoderImpl : public Decoder, Logger::Loggable { MsgParserDict BE_errors_; MsgParserDict BE_notices_; + // Buffer used to temporarily store a downstream postgres packet + // while sending other packets. Currently used only when negotiating + // upstream SSL. + Buffer::OwnedImpl temp_storage_; + // MAX_STARTUP_PACKET_LENGTH is defined in Postgres source code // as maximum size of initial packet. // https://github.com/postgres/postgres/search?q=MAX_STARTUP_PACKET_LENGTH&type=code diff --git a/contrib/postgres_proxy/filters/network/source/postgres_filter.cc b/contrib/postgres_proxy/filters/network/source/postgres_filter.cc index 6255c2ba5917a..2059ebaceeaa9 100644 --- a/contrib/postgres_proxy/filters/network/source/postgres_filter.cc +++ b/contrib/postgres_proxy/filters/network/source/postgres_filter.cc @@ -3,6 +3,7 @@ #include "envoy/buffer/buffer.h" #include "envoy/network/connection.h" +#include "source/common/common/assert.h" #include "source/extensions/filters/network/well_known_names.h" #include "contrib/postgres_proxy/filters/network/source/postgres_decoder.h" @@ -15,8 +16,8 @@ namespace PostgresProxy { PostgresFilterConfig::PostgresFilterConfig(const PostgresFilterConfigOptions& config_options, Stats::Scope& scope) : enable_sql_parsing_(config_options.enable_sql_parsing_), - terminate_ssl_(config_options.terminate_ssl_), scope_{scope}, - stats_{generateStats(config_options.stats_prefix_, scope)} {} + terminate_ssl_(config_options.terminate_ssl_), upstream_ssl_(config_options.upstream_ssl_), + scope_{scope}, stats_{generateStats(config_options.stats_prefix_, scope)} {} PostgresFilter::PostgresFilter(PostgresFilterConfigSharedPtr config) : config_{config} { if (!decoder_) { @@ -50,7 +51,12 @@ Network::FilterStatus PostgresFilter::onWrite(Buffer::Instance& data, bool) { // Backend Buffer backend_buffer_.add(data); - return doDecode(backend_buffer_, false); + Network::FilterStatus result = doDecode(backend_buffer_, false); + if (result == Network::FilterStatus::StopIteration) { + ASSERT(backend_buffer_.length() == 0); + data.drain(data.length()); + } + return result; } DecoderPtr PostgresFilter::createDecoder(DecoderCallbacks* callbacks) { @@ -205,8 +211,9 @@ bool PostgresFilter::onSSLRequest() { // Wait until 'S' has been transmitted. if (bytes >= 1) { if (!read_callbacks_->connection().startSecureTransport()) { - ENVOY_CONN_LOG(info, "postgres_proxy: cannot enable secure transport. Check configuration.", - read_callbacks_->connection()); + ENVOY_CONN_LOG( + info, "postgres_proxy: cannot enable downstream secure transport. Check configuration.", + read_callbacks_->connection()); read_callbacks_->connection().close(Network::ConnectionCloseType::NoFlush); } else { // Unsubscribe the callback. @@ -227,6 +234,45 @@ bool PostgresFilter::onSSLRequest() { return false; } +bool PostgresFilter::shouldEncryptUpstream() const { + return (config_->upstream_ssl_ == + envoy::extensions::filters::network::postgres_proxy::v3alpha::PostgresProxy::REQUIRE); +} + +void PostgresFilter::sendUpstream(Buffer::Instance& data) { + read_callbacks_->injectReadDataToFilterChain(data, false); +} + +void PostgresFilter::encryptUpstream(bool upstream_agreed, Buffer::Instance& data) { + RELEASE_ASSERT( + config_->upstream_ssl_ != + envoy::extensions::filters::network::postgres_proxy::v3alpha::PostgresProxy::DISABLE, + "encryptUpstream should not be called when upstream SSL is disabled."); + if (!upstream_agreed) { + ENVOY_CONN_LOG(info, + "postgres_proxy: upstream server rejected request to establish SSL connection. " + "Terminating.", + read_callbacks_->connection()); + read_callbacks_->connection().close(Network::ConnectionCloseType::NoFlush); + + config_->stats_.sessions_upstream_ssl_failed_.inc(); + } else { + // Try to switch upstream connection to use a secure channel. + if (read_callbacks_->startUpstreamSecureTransport()) { + config_->stats_.sessions_upstream_ssl_success_.inc(); + read_callbacks_->injectReadDataToFilterChain(data, false); + ENVOY_CONN_LOG(trace, "postgres_proxy: upstream SSL enabled.", read_callbacks_->connection()); + } else { + ENVOY_CONN_LOG(info, + "postgres_proxy: cannot enable upstream secure transport. Check " + "configuration. Terminating.", + read_callbacks_->connection()); + read_callbacks_->connection().close(Network::ConnectionCloseType::NoFlush); + config_->stats_.sessions_upstream_ssl_failed_.inc(); + } + } +} + Network::FilterStatus PostgresFilter::doDecode(Buffer::Instance& data, bool frontend) { // Keep processing data until buffer is empty or decoder says // that it cannot process data in the buffer. diff --git a/contrib/postgres_proxy/filters/network/source/postgres_filter.h b/contrib/postgres_proxy/filters/network/source/postgres_filter.h index dbc63686e8704..53f65640eabef 100644 --- a/contrib/postgres_proxy/filters/network/source/postgres_filter.h +++ b/contrib/postgres_proxy/filters/network/source/postgres_filter.h @@ -8,6 +8,7 @@ #include "source/common/buffer/buffer_impl.h" #include "source/common/common/logger.h" +#include "contrib/envoy/extensions/filters/network/postgres_proxy/v3alpha/postgres_proxy.pb.h" #include "contrib/postgres_proxy/filters/network/source/postgres_decoder.h" namespace Envoy { @@ -32,6 +33,8 @@ namespace PostgresProxy { COUNTER(sessions_encrypted) \ COUNTER(sessions_terminated_ssl) \ COUNTER(sessions_unencrypted) \ + COUNTER(sessions_upstream_ssl_success) \ + COUNTER(sessions_upstream_ssl_failed) \ COUNTER(statements) \ COUNTER(statements_insert) \ COUNTER(statements_delete) \ @@ -67,11 +70,16 @@ class PostgresFilterConfig { std::string stats_prefix_; bool enable_sql_parsing_; bool terminate_ssl_; + envoy::extensions::filters::network::postgres_proxy::v3alpha::PostgresProxy::SSLMode + upstream_ssl_; }; PostgresFilterConfig(const PostgresFilterConfigOptions& config_options, Stats::Scope& scope); bool enable_sql_parsing_{true}; bool terminate_ssl_{false}; + envoy::extensions::filters::network::postgres_proxy::v3alpha::PostgresProxy::SSLMode + upstream_ssl_{ + envoy::extensions::filters::network::postgres_proxy::v3alpha::PostgresProxy::DISABLE}; Stats::Scope& scope_; PostgresProxyStats stats_; @@ -112,6 +120,9 @@ class PostgresFilter : public Network::Filter, void incTransactionsRollback() override; void processQuery(const std::string&) override; bool onSSLRequest() override; + bool shouldEncryptUpstream() const override; + void sendUpstream(Buffer::Instance&) override; + void encryptUpstream(bool, Buffer::Instance&) override; Network::FilterStatus doDecode(Buffer::Instance& data, bool); DecoderPtr createDecoder(DecoderCallbacks* callbacks); diff --git a/contrib/postgres_proxy/filters/network/test/BUILD b/contrib/postgres_proxy/filters/network/test/BUILD index 496f8ec416e5b..fe10ba4ad970f 100644 --- a/contrib/postgres_proxy/filters/network/test/BUILD +++ b/contrib/postgres_proxy/filters/network/test/BUILD @@ -3,6 +3,7 @@ load( "envoy_cc_test", "envoy_cc_test_library", "envoy_contrib_package", + "envoy_proto_library", ) licenses(["notice"]) # Apache 2 @@ -54,22 +55,30 @@ envoy_cc_test( ], ) +envoy_proto_library( + name = "postgres_integration_proto", + srcs = [":postgres_integration_test.proto"], +) + envoy_cc_test( name = "postgres_integration_test", srcs = [ "postgres_integration_test.cc", ], data = [ - "postgres_test_config.yaml", + "postgres_test_config.yaml-template", "//test/config/integration/certs", ], deps = [ + ":postgres_integration_proto_cc_proto", + ":postgres_test_utils_lib", "//contrib/postgres_proxy/filters/network/source:config", "//contrib/postgres_proxy/filters/network/source:filter", "//source/common/tcp_proxy", "//source/extensions/filters/network/tcp_proxy:config", "//source/extensions/transport_sockets/starttls:config", "//test/integration:integration_lib", + "//test/test_common:registry_lib", "@envoy_api//contrib/envoy/extensions/filters/network/postgres_proxy/v3alpha:pkg_cc_proto", ], ) diff --git a/contrib/postgres_proxy/filters/network/test/postgres_decoder_test.cc b/contrib/postgres_proxy/filters/network/test/postgres_decoder_test.cc index dc273b8e55c68..5700839e915b2 100644 --- a/contrib/postgres_proxy/filters/network/test/postgres_decoder_test.cc +++ b/contrib/postgres_proxy/filters/network/test/postgres_decoder_test.cc @@ -24,6 +24,9 @@ class DecoderCallbacksMock : public DecoderCallbacks { MOCK_METHOD(void, incErrors, (ErrorType), (override)); MOCK_METHOD(void, processQuery, (const std::string&), (override)); MOCK_METHOD(bool, onSSLRequest, (), (override)); + MOCK_METHOD(bool, shouldEncryptUpstream, (), (const)); + MOCK_METHOD(void, sendUpstream, (Buffer::Instance&)); + MOCK_METHOD(void, encryptUpstream, (bool, Buffer::Instance&)); }; // Define fixture class with decoder and mock callbacks. @@ -121,25 +124,8 @@ TEST_F(PostgresProxyDecoderTest, StartupMessage) { TEST_F(PostgresProxyDecoderTest, StartupMessageNoAttr) { decoder_->state(DecoderImpl::State::InitState); - buf_[0] = '\0'; - // Startup message has the following structure: - // Length (4 bytes) - payload and length field - // version (4 bytes) - // Attributes: key/value pairs separated by '\0' - data_.writeBEInt(37); - // Add version code - data_.writeBEInt(0x00030000); - // user-postgres key-pair - data_.add("user"); // 4 bytes - data_.add(buf_, 1); - data_.add("postgres"); // 8 bytes - data_.add(buf_, 1); - // database-test-db key-pair - // Some other attribute - data_.add("attribute"); // 9 bytes - data_.add(buf_, 1); - data_.add("blah"); // 4 bytes - data_.add(buf_, 1); + createInitialPostgresRequest(data_); + ASSERT_THAT(decoder_->onData(data_, true), Decoder::Result::ReadyForNext); ASSERT_THAT(decoder_->state(), DecoderImpl::State::InSyncState); ASSERT_THAT(data_.length(), 0); @@ -635,6 +621,55 @@ TEST_F(PostgresProxyDecoderTest, TerminateSSL) { ASSERT_FALSE(decoder_->encrypted()); } +class PostgresProxyUpstreamSSLTest + : public PostgresProxyDecoderTestBase, + public ::testing::TestWithParam> {}; + +TEST_F(PostgresProxyDecoderTest, UpstreamSSLDisabled) { + // Set decoder to wait for initial message. + decoder_->state(DecoderImpl::State::InitState); + + createInitialPostgresRequest(data_); + + EXPECT_CALL(callbacks_, shouldEncryptUpstream).WillOnce(testing::Return(false)); + EXPECT_CALL(callbacks_, encryptUpstream(testing::_, testing::_)).Times(0); + ASSERT_THAT(decoder_->onData(data_, true), Decoder::Result::ReadyForNext); + ASSERT_THAT(decoder_->state(), DecoderImpl::State::InSyncState); +} + +TEST_P(PostgresProxyUpstreamSSLTest, UpstreamSSLEnabled) { + // Set decoder to wait for initial message. + decoder_->state(DecoderImpl::State::InitState); + + // Create initial message + createInitialPostgresRequest(data_); + + EXPECT_CALL(callbacks_, shouldEncryptUpstream).WillOnce(testing::Return(true)); + EXPECT_CALL(callbacks_, sendUpstream); + ASSERT_THAT(decoder_->onData(data_, true), Decoder::Result::Stopped); + ASSERT_THAT(decoder_->state(), DecoderImpl::State::NegotiatingUpstreamSSL); + + // Simulate various responses from the upstream server. + // Only "S" and "E" are valid responses. + data_.add(std::get<0>(GetParam())); + + EXPECT_CALL(callbacks_, encryptUpstream(std::get<1>(GetParam()), testing::_)); + // The reply from upstream should not be delivered to the client. + ASSERT_THAT(decoder_->onData(data_, false), Decoder::Result::Stopped); + ASSERT_THAT(decoder_->state(), std::get<2>(GetParam())); + ASSERT_TRUE(data_.length() == 0); +} + +INSTANTIATE_TEST_SUITE_P(BackendEncryptedMessagesTests, PostgresProxyUpstreamSSLTest, + ::testing::Values( + // Correct response from the server (encrypt). + std::make_tuple("S", true, DecoderImpl::State::InitState), + // Correct response from the server (do not encrypt). + std::make_tuple("E", false, DecoderImpl::State::InitState), + // Incorrect response from the server. Move to out-of-sync state. + std::make_tuple("W", false, DecoderImpl::State::OutOfSyncState), + std::make_tuple("WRONG", false, DecoderImpl::State::OutOfSyncState))); + class FakeBuffer : public Buffer::Instance { public: MOCK_METHOD(void, addDrainTracker, (std::function), (override)); diff --git a/contrib/postgres_proxy/filters/network/test/postgres_filter_test.cc b/contrib/postgres_proxy/filters/network/test/postgres_filter_test.cc index 3a081ce55b688..06f478e05d92b 100644 --- a/contrib/postgres_proxy/filters/network/test/postgres_filter_test.cc +++ b/contrib/postgres_proxy/filters/network/test/postgres_filter_test.cc @@ -33,7 +33,10 @@ class PostgresFilterTest public: PostgresFilterTest() { - PostgresFilterConfig::PostgresFilterConfigOptions config_options{stat_prefix_, true, false}; + PostgresFilterConfig::PostgresFilterConfigOptions config_options{ + stat_prefix_, true, false, + envoy::extensions::filters::network::postgres_proxy::v3alpha:: + PostgresProxy_SSLMode_DISABLE}; config_ = std::make_shared(config_options, scope_); filter_ = std::make_unique(config_); @@ -393,6 +396,40 @@ TEST_F(PostgresFilterTest, TerminateSSL) { ASSERT_THAT(filter_->getStats().sessions_unencrypted_.value(), 0); } +TEST_F(PostgresFilterTest, UpstreamSSL) { + EXPECT_CALL(filter_callbacks_, connection()).WillRepeatedly(ReturnRef(connection_)); + + // Configure upstream SSL to be disabled. encryptUpstream must not be called. + filter_->getConfig()->upstream_ssl_ = + envoy::extensions::filters::network::postgres_proxy::v3alpha::PostgresProxy::DISABLE; + ASSERT_FALSE(filter_->shouldEncryptUpstream()); + ASSERT_DEATH(filter_->encryptUpstream(true, data_), ".*"); + ASSERT_DEATH(filter_->encryptUpstream(false, data_), ".*"); + + // Configure upstream SSL to be required. If upstream server does not agree for SSL or + // converting upstream transport socket to secure mode fails, the filter should bump + // proper stats and close the connection to downstream client. + filter_->getConfig()->upstream_ssl_ = + envoy::extensions::filters::network::postgres_proxy::v3alpha::PostgresProxy::REQUIRE; + ASSERT_TRUE(filter_->shouldEncryptUpstream()); + // Simulate that upstream server agreed for SSL and conversion of upstream Transport socket was + // successful. + EXPECT_CALL(filter_callbacks_, startUpstreamSecureTransport()).WillOnce(testing::Return(true)); + filter_->encryptUpstream(true, data_); + ASSERT_EQ(1, filter_->getStats().sessions_upstream_ssl_success_.value()); + // Simulate that upstream server agreed for SSL but conversion of upstream Transport socket + // failed. + EXPECT_CALL(filter_callbacks_, startUpstreamSecureTransport()).WillOnce(testing::Return(false)); + filter_->encryptUpstream(true, data_); + ASSERT_EQ(1, filter_->getStats().sessions_upstream_ssl_failed_.value()); + // Simulate that upstream server does not agree for SSL. Filter should close the connection to + // downstream client. + EXPECT_CALL(filter_callbacks_, startUpstreamSecureTransport()).Times(0); + EXPECT_CALL(connection_, close(_)); + filter_->encryptUpstream(false, data_); + ASSERT_EQ(2, filter_->getStats().sessions_upstream_ssl_failed_.value()); +} + } // namespace PostgresProxy } // namespace NetworkFilters } // namespace Extensions diff --git a/contrib/postgres_proxy/filters/network/test/postgres_integration_test.cc b/contrib/postgres_proxy/filters/network/test/postgres_integration_test.cc index 58d1ddc779375..d06bb6d267a46 100644 --- a/contrib/postgres_proxy/filters/network/test/postgres_integration_test.cc +++ b/contrib/postgres_proxy/filters/network/test/postgres_integration_test.cc @@ -1,11 +1,20 @@ +#include "source/common/network/connection_impl.h" +#include "source/extensions/filters/network/common/factory_base.h" +#include "source/extensions/transport_sockets/tls/context_config_impl.h" +#include "source/extensions/transport_sockets/tls/ssl_socket.h" + #include "test/integration/fake_upstream.h" #include "test/integration/integration.h" #include "test/integration/utility.h" #include "test/mocks/network/mocks.h" #include "test/test_common/network_utility.h" +#include "test/test_common/registry.h" #include "contrib/envoy/extensions/filters/network/postgres_proxy/v3alpha/postgres_proxy.pb.h" #include "contrib/envoy/extensions/filters/network/postgres_proxy/v3alpha/postgres_proxy.pb.validate.h" +#include "contrib/postgres_proxy/filters/network/test/postgres_integration_test.pb.h" +#include "contrib/postgres_proxy/filters/network/test/postgres_integration_test.pb.validate.h" +#include "contrib/postgres_proxy/filters/network/test/postgres_test_utils.h" #include "gmock/gmock.h" #include "gtest/gtest.h" @@ -16,53 +25,49 @@ namespace PostgresProxy { class PostgresBaseIntegrationTest : public testing::TestWithParam, public BaseIntegrationTest { +public: + // Tuple to store upstream and downstream startTLS configuration. + // The first string contains string to enable/disable SSL. + // The second string contains transport socket configuration. + using SSLConfig = std::tuple; - std::string postgresConfig(bool terminate_ssl, bool add_start_tls_transport_socket) { + std::string postgresConfig(SSLConfig downstream_ssl_config, SSLConfig upstream_ssl_config, + std::string additional_filters) { std::string main_config = fmt::format( TestEnvironment::readFileToStringForTest(TestEnvironment::runfilesPath( - "contrib/postgres_proxy/filters/network/test/postgres_test_config.yaml")), + "contrib/postgres_proxy/filters/network/test/postgres_test_config.yaml-template")), Platform::null_device_path, Network::Test::getLoopbackAddressString(GetParam()), Network::Test::getLoopbackAddressString(GetParam()), - Network::Test::getAnyAddressString(GetParam()), terminate_ssl ? "true" : "false"); - - if (add_start_tls_transport_socket) { - main_config += - fmt::format(R"EOF( - transport_socket: - name: "starttls" - typed_config: - "@type": type.googleapis.com/envoy.extensions.transport_sockets.starttls.v3.StartTlsConfig - cleartext_socket_config: - tls_socket_config: - common_tls_context: - tls_certificates: - certificate_chain: - filename: {} - private_key: - filename: {} - )EOF", - TestEnvironment::runfilesPath("test/config/integration/certs/servercert.pem"), - TestEnvironment::runfilesPath("test/config/integration/certs/serverkey.pem")); - } + std::get<1>(upstream_ssl_config), // upstream SSL transport socket + Network::Test::getAnyAddressString(GetParam()), + std::get<0>(downstream_ssl_config), // downstream SSL termination + std::get<0>(upstream_ssl_config), // upstream_SSL option + additional_filters, // additional filters to insert after postgres + std::get<1>(downstream_ssl_config)); // downstream SSL transport socket return main_config; } -public: - PostgresBaseIntegrationTest(bool terminate_ssl, bool add_starttls_transport_socket) - : BaseIntegrationTest(GetParam(), - postgresConfig(terminate_ssl, add_starttls_transport_socket)) { + PostgresBaseIntegrationTest(SSLConfig downstream_ssl_config, SSLConfig upstream_ssl_config, + std::string additional_filters = "") + : BaseIntegrationTest(GetParam(), postgresConfig(downstream_ssl_config, upstream_ssl_config, + additional_filters)) { skip_tag_extraction_rule_check_ = true; }; void SetUp() override { BaseIntegrationTest::initialize(); } + + static constexpr absl::string_view empty_config_string_{""}; + static constexpr SSLConfig NoUpstreamSSL{empty_config_string_, empty_config_string_}; + static constexpr SSLConfig NoDownstreamSSL{empty_config_string_, empty_config_string_}; + FakeRawConnectionPtr fake_upstream_connection_; }; // Base class for tests with `terminate_ssl` disabled and without // `starttls` transport socket. class BasicPostgresIntegrationTest : public PostgresBaseIntegrationTest { public: - BasicPostgresIntegrationTest() : PostgresBaseIntegrationTest(false, false) {} + BasicPostgresIntegrationTest() : PostgresBaseIntegrationTest(NoDownstreamSSL, NoUpstreamSSL) {} }; // Test that the filter is properly chained and reacts to successful login @@ -107,19 +112,38 @@ TEST_P(BasicPostgresIntegrationTest, Login) { // Make sure that the successful login bumped up the number of sessions. test_server_->waitForCounterEq("postgres.postgres_stats.sessions", 1); } - INSTANTIATE_TEST_SUITE_P(IpVersions, BasicPostgresIntegrationTest, testing::ValuesIn(TestEnvironment::getIpVersionsForTest())); // Base class for tests with `terminate_ssl` enabled and `starttls` transport socket added. -class SSLPostgresIntegrationTest : public PostgresBaseIntegrationTest { +class DownstreamSSLPostgresIntegrationTest : public PostgresBaseIntegrationTest { public: - SSLPostgresIntegrationTest() : PostgresBaseIntegrationTest(true, true) {} + DownstreamSSLPostgresIntegrationTest() + : PostgresBaseIntegrationTest( + std::make_tuple( + "terminate_ssl: true", + fmt::format( + R"EOF(transport_socket: + name: "starttls" + typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.starttls.v3.StartTlsConfig + cleartext_socket_config: + tls_socket_config: + common_tls_context: + tls_certificates: + certificate_chain: + filename: {} + private_key: + filename: {} + )EOF", + TestEnvironment::runfilesPath("test/config/integration/certs/servercert.pem"), + TestEnvironment::runfilesPath("test/config/integration/certs/serverkey.pem"))), + NoUpstreamSSL) {} }; // Test verifies that Postgres filter replies with correct code upon // receiving request to terminate SSL. -TEST_P(SSLPostgresIntegrationTest, TerminateSSL) { +TEST_P(DownstreamSSLPostgresIntegrationTest, TerminateSSL) { Buffer::OwnedImpl data; IntegrationTcpClientPtr tcp_client = makeTcpConnection(lookupPort("listener_0")); @@ -147,17 +171,19 @@ TEST_P(SSLPostgresIntegrationTest, TerminateSSL) { test_server_->waitForCounterEq("postgres.postgres_stats.sessions_terminated_ssl", 1); } -INSTANTIATE_TEST_SUITE_P(IpVersions, SSLPostgresIntegrationTest, +INSTANTIATE_TEST_SUITE_P(IpVersions, DownstreamSSLPostgresIntegrationTest, testing::ValuesIn(TestEnvironment::getIpVersionsForTest())); -class SSLWrongConfigPostgresIntegrationTest : public PostgresBaseIntegrationTest { +class DownstreamSSLWrongConfigPostgresIntegrationTest : public PostgresBaseIntegrationTest { public: - SSLWrongConfigPostgresIntegrationTest() : PostgresBaseIntegrationTest(true, false) {} + DownstreamSSLWrongConfigPostgresIntegrationTest() + // Enable SSL termination but do not configure downstream transport socket. + : PostgresBaseIntegrationTest(std::make_tuple("terminate_ssl: true", ""), NoUpstreamSSL) {} }; // Test verifies that Postgres filter closes connection when it is configured to // terminate SSL, but underlying transport socket does not allow for such operation. -TEST_P(SSLWrongConfigPostgresIntegrationTest, TerminateSSLNoStartTlsTransportSocket) { +TEST_P(DownstreamSSLWrongConfigPostgresIntegrationTest, TerminateSSLNoStartTlsTransportSocket) { Buffer::OwnedImpl data; IntegrationTcpClientPtr tcp_client = makeTcpConnection(lookupPort("listener_0")); @@ -183,11 +209,278 @@ TEST_P(SSLWrongConfigPostgresIntegrationTest, TerminateSSLNoStartTlsTransportSoc tcp_client->waitForDisconnect(); ASSERT_TRUE(fake_upstream_connection->waitForDisconnect()); - // Make sure that the successful login bumped up the number of sessions. test_server_->waitForCounterEq("postgres.postgres_stats.sessions_terminated_ssl", 0); } -INSTANTIATE_TEST_SUITE_P(IpVersions, SSLWrongConfigPostgresIntegrationTest, +INSTANTIATE_TEST_SUITE_P(IpVersions, DownstreamSSLWrongConfigPostgresIntegrationTest, + testing::ValuesIn(TestEnvironment::getIpVersionsForTest())); + +// Upstream SSL integration tests. +// Tests do not use the real postgres server and concentrate only on initial exchange. +// The initial packet +// sent by the downstream client, SSL request sent to fake upstream and SSL response sent back by +// fake client are valid postgres payloads, because they must be parsed by postgres filter. + +class UpstreamSSLBaseIntegrationTest : public PostgresBaseIntegrationTest { +public: + UpstreamSSLBaseIntegrationTest(SSLConfig upstream_ssl_config) + // Disable downstream SSL and attach synchronization filter. + : PostgresBaseIntegrationTest(NoDownstreamSSL, upstream_ssl_config, R"EOF( + - name: sync + typed_config: + "@type": type.googleapis.com/test.integration.postgres.SyncWriteFilterConfig +)EOF") {} + + // Helper synchronization filter which is injected between postgres filter and tcp proxy. + // Its goal is to eliminate race conditions and synchronize operations between fake upstream and + // postgres filter. + struct SyncWriteFilter : public Network::WriteFilter { + SyncWriteFilter(absl::Notification& proceed_sync, absl::Notification& recv_sync) + : proceed_sync_(proceed_sync), recv_sync_(recv_sync) {} + + Network::FilterStatus onWrite(Buffer::Instance& data, bool) override { + if (data.length() > 0) { + // Notify fake upstream that payload has been received. + recv_sync_.Notify(); + // Wait for signal to continue. This is to give fake upstream + // some time to create and attach TLS transport socket. + proceed_sync_.WaitForNotification(); + } + return Network::FilterStatus::Continue; + } + + void initializeWriteFilterCallbacks(Network::WriteFilterCallbacks& callbacks) override { + read_callbacks_ = &callbacks; + } + + Network::WriteFilterCallbacks* read_callbacks_{}; + // Synchronization object used to stop Envoy processing to allow fake upstream to + // create and attach TLS transport socket. + absl::Notification& proceed_sync_; + // Synchronization object used to notify fake upstream that a message sent + // by fake upstream was received by Envoy. + absl::Notification& recv_sync_; + }; + + // Config factory for sync helper filter. + class SyncWriteFilterConfigFactory : public Extensions::NetworkFilters::Common::FactoryBase< + test::integration::postgres::SyncWriteFilterConfig> { + public: + explicit SyncWriteFilterConfigFactory(const std::string& name, + Network::ConnectionCallbacks& /* upstream_callbacks*/) + : FactoryBase(name) {} + + Network::FilterFactoryCb + createFilterFactoryFromProtoTyped(const test::integration::postgres::SyncWriteFilterConfig&, + Server::Configuration::FactoryContext&) override { + return [&](Network::FilterManager& filter_manager) -> void { + filter_manager.addWriteFilter(std::make_shared(proceed_sync_, recv_sync_)); + }; + } + + std::string name() const override { return name_; } + + // See SyncWriteFilter for purpose and description of the following sync objects. + absl::Notification proceed_sync_, recv_sync_; + + private: + const std::string name_; + }; + + // Method prepares TLS context to be injected to fake upstream. + // Method creates and attaches TLS transport socket to fake upstream. + void enableTLSOnFakeUpstream() { + // Setup factory and context for tls transport socket. + // The tls transport socket will be inserted into fake_upstream when + // Envoy's upstream starttls transport socket is converted to secure mode. + std::unique_ptr tls_context_manager = + std::make_unique(timeSystem()); + + envoy::extensions::transport_sockets::tls::v3::DownstreamTlsContext downstream_tls_context; + + std::string yaml_plain = R"EOF( + common_tls_context: + validation_context: + trusted_ca: + filename: "{{ test_rundir }}/test/config/integration/certs/upstreamcacert.pem" + tls_certificates: + certificate_chain: + filename: "{{ test_rundir }}/test/config/integration/certs/upstreamcert.pem" + private_key: + filename: "{{ test_rundir }}/test/config/integration/certs/upstreamkey.pem" +)EOF"; + + TestUtility::loadFromYaml(TestEnvironment::substitute(yaml_plain), downstream_tls_context); + + NiceMock mock_factory_ctx; + ON_CALL(mock_factory_ctx, api()).WillByDefault(testing::ReturnRef(*api_)); + auto cfg = std::make_unique( + downstream_tls_context, mock_factory_ctx); + static auto* client_stats_store = new Stats::TestIsolatedStoreImpl(); + Network::DownstreamTransportSocketFactoryPtr tls_context = + Network::DownstreamTransportSocketFactoryPtr{ + new Extensions::TransportSockets::Tls::ServerSslSocketFactory( + std::move(cfg), *tls_context_manager, *client_stats_store, {})}; + + Network::TransportSocketPtr ts = tls_context->createDownstreamTransportSocket(); + // Synchronization object used to suspend execution + // until dispatcher completes transport socket conversion. + absl::Notification notification; + + // Execute transport socket conversion to TLS on the same thread where received data + // is dispatched. Otherwise conversion may collide with data processing. + fake_upstreams_[0]->dispatcher()->post([&]() { + auto connection = + dynamic_cast(&fake_upstream_connection_->connection()); + connection->transportSocket() = std::move(ts); + connection->transportSocket()->setTransportSocketCallbacks(*connection); + notification.Notify(); + }); + + // Wait until the transport socket conversion completes. + notification.WaitForNotification(); + } + + NiceMock upstream_callbacks_; + SyncWriteFilterConfigFactory config_factory_{"sync", upstream_callbacks_}; + Registry::InjectFactory + registered_config_factory_{config_factory_}; +}; + +// Base class for tests with disabled upstream SSL. It should behave exactly +// as without any upstream configuration specified and pass +// messages in clear-text. +class UpstreamSSLDisabledPostgresIntegrationTest : public UpstreamSSLBaseIntegrationTest { +public: + // Disable downstream SSL and upstream SSL. + UpstreamSSLDisabledPostgresIntegrationTest() + : UpstreamSSLBaseIntegrationTest(std::make_tuple("upstream_ssl: DISABLE", "")) {} +}; + +// Verify that postgres filter does not send any additional messages when +// upstream SSL is disabled. Fake upstream should receive only the initial +// postgres message. +TEST_P(UpstreamSSLDisabledPostgresIntegrationTest, BasicConnectivityTest) { + IntegrationTcpClientPtr tcp_client = makeTcpConnection(lookupPort("listener_0")); + ASSERT_TRUE(fake_upstreams_[0]->waitForRawConnection(fake_upstream_connection_)); + + // Send the startup message. + Buffer::OwnedImpl data; + std::string rcvd; + createInitialPostgresRequest(data); + ASSERT_TRUE(tcp_client->write(data.toString())); + // Make sure that upstream receives startup message in clear-text (no SSL negotiation takes + // place). + ASSERT_TRUE(fake_upstream_connection_->waitForData(data.toString().length(), &rcvd)); + data.drain(data.length()); + + tcp_client->close(); + ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); + + test_server_->waitForCounterEq("postgres.postgres_stats.sessions_upstream_ssl_success", 0); + test_server_->waitForCounterEq("postgres.postgres_stats.sessions_upstream_ssl_failed", 0); +} + +INSTANTIATE_TEST_SUITE_P(IpVersions, UpstreamSSLDisabledPostgresIntegrationTest, + testing::ValuesIn(TestEnvironment::getIpVersionsForTest())); + +// Base class for parameterized tests with REQUIRE option for upstream SSL. +class UpstreamSSLRequirePostgresIntegrationTest : public UpstreamSSLBaseIntegrationTest { +public: + UpstreamSSLRequirePostgresIntegrationTest() + : UpstreamSSLBaseIntegrationTest(std::make_tuple("upstream_ssl: REQUIRE", + R"EOF(transport_socket: + name: "starttls" + typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.starttls.v3.UpstreamStartTlsConfig + tls_socket_config: + common_tls_context: {} +)EOF")) {} +}; + +// Test verifies that postgres filter starts upstream SSL negotiation with +// fake upstream upon receiving initial postgres packet. When server agrees +// to use SSL, TLS transport socket is attached to fake upstream and +// fake upstream receives initial postgres packet over encrypted connection. +TEST_P(UpstreamSSLRequirePostgresIntegrationTest, ServerAgreesForSSLTest) { + IntegrationTcpClientPtr tcp_client = makeTcpConnection(lookupPort("listener_0")); + ASSERT_TRUE(fake_upstreams_[0]->waitForRawConnection(fake_upstream_connection_)); + + // Send the startup message. + Buffer::OwnedImpl data; + Buffer::OwnedImpl upstream_data; + std::string rcvd; + createInitialPostgresRequest(data); + ASSERT_TRUE(tcp_client->write(data.toString())); + // Postgres filter should buffer the original message and negotiate SSL upstream. + // The first 4 bytes should be length on the message (8 bytes). + // The next 4 bytes should be SSL code. + ASSERT_TRUE(fake_upstream_connection_->waitForData(8, &rcvd)); + upstream_data.add(rcvd); + ASSERT_EQ(8, upstream_data.peekBEInt(0)); + ASSERT_EQ(80877103, upstream_data.peekBEInt(4)); + upstream_data.drain(upstream_data.length()); + fake_upstream_connection_->clearData(); + + // Reply to Envoy with 'S' and attach TLS socket to upstream. + upstream_data.add("S"); + ASSERT_TRUE(fake_upstream_connection_->write(upstream_data.toString())); + + config_factory_.recv_sync_.WaitForNotification(); + enableTLSOnFakeUpstream(); + config_factory_.proceed_sync_.Notify(); + + ASSERT_TRUE(fake_upstream_connection_->waitForData(data.length(), &rcvd)); + // Make sure that upstream received initial postgres request, which + // triggered upstream SSL negotiation and TLS handshake. + ASSERT_EQ(data.toString(), rcvd); + + data.drain(data.length()); + + tcp_client->close(); + ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); + + test_server_->waitForCounterEq("postgres.postgres_stats.sessions_upstream_ssl_success", 1); + test_server_->waitForCounterEq("postgres.postgres_stats.sessions_upstream_ssl_failed", 0); +} + +// Test verifies that postgres filter will not continue when upstream SSL +// is required and fake upstream does not agree for SSL. +TEST_P(UpstreamSSLRequirePostgresIntegrationTest, ServerDeniesSSLTest) { + IntegrationTcpClientPtr tcp_client = makeTcpConnection(lookupPort("listener_0")); + ASSERT_TRUE(fake_upstreams_[0]->waitForRawConnection(fake_upstream_connection_)); + + // Send the startup message. + Buffer::OwnedImpl data; + Buffer::OwnedImpl upstream_data; + std::string rcvd; + createInitialPostgresRequest(data); + ASSERT_TRUE(tcp_client->write(data.toString())); + // Postgres filter should buffer the original message and negotiate SSL upstream. + // The first 4 bytes should be length on the message (8 bytes). + // The next 4 bytes should be SSL code. + ASSERT_TRUE(fake_upstream_connection_->waitForData(8, &rcvd)); + upstream_data.add(rcvd); + ASSERT_EQ(8, upstream_data.peekBEInt(0)); + ASSERT_EQ(80877103, upstream_data.peekBEInt(4)); + upstream_data.drain(upstream_data.length()); + + // Reply to Envoy with 'E' (SSL not allowed). + upstream_data.add("E"); + ASSERT_TRUE(fake_upstream_connection_->write(upstream_data.toString())); + config_factory_.proceed_sync_.Notify(); + + data.drain(data.length()); + + // Connection to client should be closed. + tcp_client->waitForDisconnect(); + ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); + + test_server_->waitForCounterEq("postgres.postgres_stats.sessions_upstream_ssl_success", 0); + test_server_->waitForCounterEq("postgres.postgres_stats.sessions_upstream_ssl_failed", 1); +} + +INSTANTIATE_TEST_SUITE_P(IpVersions, UpstreamSSLRequirePostgresIntegrationTest, testing::ValuesIn(TestEnvironment::getIpVersionsForTest())); } // namespace PostgresProxy diff --git a/contrib/postgres_proxy/filters/network/test/postgres_integration_test.proto b/contrib/postgres_proxy/filters/network/test/postgres_integration_test.proto new file mode 100644 index 0000000000000..6ab902a04e6df --- /dev/null +++ b/contrib/postgres_proxy/filters/network/test/postgres_integration_test.proto @@ -0,0 +1,6 @@ +syntax = "proto3"; + +package test.integration.postgres; + +message SyncWriteFilterConfig { +} diff --git a/contrib/postgres_proxy/filters/network/test/postgres_test_config.yaml b/contrib/postgres_proxy/filters/network/test/postgres_test_config.yaml-template similarity index 82% rename from contrib/postgres_proxy/filters/network/test/postgres_test_config.yaml rename to contrib/postgres_proxy/filters/network/test/postgres_test_config.yaml-template index a60abfb26adc2..e1063292769e6 100644 --- a/contrib/postgres_proxy/filters/network/test/postgres_test_config.yaml +++ b/contrib/postgres_proxy/filters/network/test/postgres_test_config.yaml-template @@ -21,6 +21,8 @@ static_resources: socket_address: address: "{}" port_value: 0 + #downstream startTLS transport socket: + {} listeners: name: listener_0 address: @@ -33,9 +35,16 @@ static_resources: typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.postgres_proxy.v3alpha.PostgresProxy stat_prefix: postgres_stats - terminate_ssl: {} + # downstream SSL option: + {} + # upstream SSL option: + {} + # additional filters + {} - name: tcp typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy stat_prefix: tcp_stats cluster: cluster_0 + # upstream startTLS transport socket + {} diff --git a/contrib/postgres_proxy/filters/network/test/postgres_test_utils.cc b/contrib/postgres_proxy/filters/network/test/postgres_test_utils.cc index 8793acf3b8151..34347dd3fbf34 100644 --- a/contrib/postgres_proxy/filters/network/test/postgres_test_utils.cc +++ b/contrib/postgres_proxy/filters/network/test/postgres_test_utils.cc @@ -17,6 +17,28 @@ void createPostgresMsg(Buffer::Instance& data, std::string type, std::string pay } } +// Helper function to create an initial postgres message. +void createInitialPostgresRequest(Buffer::Instance& data) { + // Startup message has the following structure: + // Length (4 bytes) - payload and length field + // version (4 bytes) + // Attributes: key/value pairs separated by '\0' + data.writeBEInt(37); + // Add version code + data.writeBEInt(0x00030000); + // user-postgres key-pair + data.add("user"); // 4 bytes + data.writeBEInt(0); + data.add("postgres"); // 8 bytes + data.writeBEInt(0); + // database-test-db key-pair + // Some other attribute + data.add("attribute"); // 9 bytes + data.writeBEInt(0); + data.add("blah"); // 4 bytes + data.writeBEInt(0); +} + } // namespace PostgresProxy } // namespace NetworkFilters } // namespace Extensions diff --git a/contrib/postgres_proxy/filters/network/test/postgres_test_utils.h b/contrib/postgres_proxy/filters/network/test/postgres_test_utils.h index 0ee1614ec180f..24f9121b1e4d0 100644 --- a/contrib/postgres_proxy/filters/network/test/postgres_test_utils.h +++ b/contrib/postgres_proxy/filters/network/test/postgres_test_utils.h @@ -8,6 +8,7 @@ namespace NetworkFilters { namespace PostgresProxy { void createPostgresMsg(Buffer::Instance& data, std::string type, std::string payload = ""); +void createInitialPostgresRequest(Buffer::Instance& data); } // namespace PostgresProxy } // namespace NetworkFilters diff --git a/docs/root/configuration/listeners/network_filters/postgres_proxy_filter.rst b/docs/root/configuration/listeners/network_filters/postgres_proxy_filter.rst index 5e4834c17662e..58f2e29a7616b 100644 --- a/docs/root/configuration/listeners/network_filters/postgres_proxy_filter.rst +++ b/docs/root/configuration/listeners/network_filters/postgres_proxy_filter.rst @@ -75,6 +75,8 @@ Every configured Postgres proxy filter has statistics rooted at postgres.