diff --git a/api/envoy/extensions/filters/listener/proxy_protocol/v3/proxy_protocol.proto b/api/envoy/extensions/filters/listener/proxy_protocol/v3/proxy_protocol.proto index 4431d5463932..31ca8a6950e4 100644 --- a/api/envoy/extensions/filters/listener/proxy_protocol/v3/proxy_protocol.proto +++ b/api/envoy/extensions/filters/listener/proxy_protocol/v3/proxy_protocol.proto @@ -70,4 +70,19 @@ message ProxyProtocol { // :ref:`core.v3.ProxyProtocolConfig.pass_through_tlvs `, // which controls pass-through for the upstream. config.core.v3.ProxyProtocolPassThroughTLVs pass_through_tlvs = 3; + + // The PROXY protocol versions that won't be matched. Useful to limit the scope and attack surface of the filter. + // + // When the filter receives PROXY protocol data that is disallowed, it will reject the connection. + // By default, the filter will match all PROXY protocol versions. + // See https://www.haproxy.org/download/2.1/doc/proxy-protocol.txt for details. + // + // .. attention:: + // + // When used in conjunction with the :ref:`allow_requests_without_proxy_protocol `, + // the filter will not attempt to match signatures for the disallowed versions. + // For example, when ``disallowed_versions=V2``, ``allow_requests_without_proxy_protocol=true``, + // and an incoming request matches the V2 signature, the filter will allow the request through without any modification. + // The filter treats this request as if it did not have any PROXY protocol information. + repeated config.core.v3.ProxyProtocolConfig.Version disallowed_versions = 4; } diff --git a/changelogs/current.yaml b/changelogs/current.yaml index 82a3b50d2227..5ed152d6c31b 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -388,6 +388,13 @@ new_features: change: | Added :ref:`formatters ` to Fluentd access logger to allow adding extension commands when formatter access logs. +- area: proxy_protocol + change: | + Added :ref:`disallowed_versions ` + to enforce the filter only matches specific PROXY protocol versions. +- area: proxy_protocol + change: | + Added new statistics to the proxy protocol filter to track connections found/disallowed/errored by PROXY protocol version. - area: rbac change: | Added :ref:`rules_stat_prefix ` diff --git a/docs/root/configuration/listeners/listener_filters/proxy_protocol.rst b/docs/root/configuration/listeners/listener_filters/proxy_protocol.rst index 90eb7743acb7..e96d457e7725 100644 --- a/docs/root/configuration/listeners/listener_filters/proxy_protocol.rst +++ b/docs/root/configuration/listeners/listener_filters/proxy_protocol.rst @@ -16,9 +16,13 @@ the TLV will be emitted as dynamic metadata with user-specified key. This implementation supports both version 1 and version 2, it automatically determines on a per-connection basis which of the two -versions is present. Note: if the filter is enabled, the Proxy Protocol -must be present on the connection (either version 1 or version 2), -the standard does not allow parsing to determine if it is present or not. +versions is present. + +.. note:: + If the filter is enabled, the Proxy Protocol must be present on the connection (either version 1 or version 2). + The standard does not allow parsing to determine if it is present or not. However, the filter can be configured + to allow the connection to be accepted without the Proxy Protocol header (against the standard). + See :ref:`allow_requests_without_proxy_protocol `. If there is a protocol error or an unsupported address family (e.g. AF_UNIX) the connection will be closed and an error thrown. @@ -29,10 +33,33 @@ If there is a protocol error or an unsupported address family Statistics ---------- -This filter emits the following statistics: +This filter emits the following general statistics, rooted at *downstream_proxy_proto* + +.. csv-table:: + :header: Name, Type, Description + :widths: 4, 1, 8 + + not_found_disallowed, Counter, "Total number of connections that don't contain the PROXY protocol header and are rejected." + not_found_allowed, Counter, "Total number of connections that don't contain the PROXY protocol header, but are allowed due to :ref:`allow_requests_without_proxy_protocol `." + +The filter also emits the statistics rooted at *downstream_proxy_proto.versions.* +for each matched PROXY protocol version. Proxy protocol versions include ``v1`` and ``v2``. .. csv-table:: :header: Name, Type, Description - :widths: 1, 1, 2 + :widths: 4, 1, 8 + + found, Counter, "Total number of connections where the PROXY protocol header was found and parsed correctly." + disallowed, Counter, "Total number of ``found`` connections that are rejected due to :ref:`disallowed_versions `." + error, Counter, "Total number of connections where the PROXY protocol header was malformed (and the connection was rejected)." + +The filter also emits the following legacy statistics, rooted at its own scope: + +.. csv-table:: + :header: Name, Type, Description + :widths: 4, 1, 8 + + downstream_cx_proxy_proto_error, Counter, "Total number of connections with proxy protocol errors, i.e. ``v1.error``, ``v2.error``, and ``not_found_disallowed``." - downstream_cx_proxy_proto_error, Counter, Total proxy protocol errors +.. attention:: + Prefer using the more-detailed non-legacy statistics above. diff --git a/source/common/config/well_known_names.cc b/source/common/config/well_known_names.cc index 63d3861982b2..cb1a8c7d3eb1 100644 --- a/source/common/config/well_known_names.cc +++ b/source/common/config/well_known_names.cc @@ -211,6 +211,9 @@ TagNameValues::TagNameValues() { // http..rbac.(.)* addTokenized(RBAC_HTTP_PREFIX, "http.*.rbac.$.**"); + + // proxy_proto.(versions.v.)** + addRe2(PROXY_PROTOCOL_VERSION, R"(^proxy_proto\.(versions\.v(\d)\.)\w+)", "proxy_proto.versions"); } void TagNameValues::addRe2(const std::string& name, const std::string& regex, diff --git a/source/common/config/well_known_names.h b/source/common/config/well_known_names.h index 8cf5fbd14715..9af3ca60ce53 100644 --- a/source/common/config/well_known_names.h +++ b/source/common/config/well_known_names.h @@ -161,6 +161,8 @@ class TagNameValues { const std::string THRIFT_PREFIX = "envoy.thrift_prefix"; // Stats prefix for the Redis Proxy network filter const std::string REDIS_PREFIX = "envoy.redis_prefix"; + // Proxy Protocol version for a connection (Proxy Protocol listener filter). + const std::string PROXY_PROTOCOL_VERSION = "envoy.proxy_protocol_version"; // Mapping from the names above to their respective regex strings. const std::vector> name_regex_pairs_; diff --git a/source/extensions/filters/listener/proxy_protocol/proxy_protocol.cc b/source/extensions/filters/listener/proxy_protocol/proxy_protocol.cc index 265eb570a0b3..953e38561e1e 100644 --- a/source/extensions/filters/listener/proxy_protocol/proxy_protocol.cc +++ b/source/extensions/filters/listener/proxy_protocol/proxy_protocol.cc @@ -13,6 +13,7 @@ #include "envoy/event/dispatcher.h" #include "envoy/network/listen_socket.h" #include "envoy/stats/scope.h" +#include "envoy/stats/stats_macros.h" #include "source/common/api/os_sys_calls_impl.h" #include "source/common/common/assert.h" @@ -27,6 +28,7 @@ #include "source/common/protobuf/utility.h" #include "source/extensions/common/proxy_protocol/proxy_protocol_header.h" +using envoy::config::core::v3::ProxyProtocolConfig; using envoy::config::core::v3::ProxyProtocolPassThroughTLVs; using Envoy::Extensions::Common::ProxyProtocol::PROXY_PROTO_V1_SIGNATURE; using Envoy::Extensions::Common::ProxyProtocol::PROXY_PROTO_V1_SIGNATURE_LEN; @@ -48,10 +50,60 @@ namespace Extensions { namespace ListenerFilters { namespace ProxyProtocol { +constexpr absl::string_view kProxyProtoStatsPrefix = "proxy_proto."; +constexpr absl::string_view kVersionStatsPrefix = "versions."; + +ProxyProtocolStats ProxyProtocolStats::create(Stats::Scope& scope) { + return { + /*legacy_=*/{LEGACY_PROXY_PROTOCOL_STATS(POOL_COUNTER(scope))}, + /*general_=*/ + {GENERAL_PROXY_PROTOCOL_STATS(POOL_COUNTER_PREFIX(scope, kProxyProtoStatsPrefix))}, + /*v1_=*/ + {VERSIONED_PROXY_PROTOCOL_STATS(POOL_COUNTER_PREFIX( + scope, absl::StrCat(kProxyProtoStatsPrefix, kVersionStatsPrefix, "v1.")))}, + /*v2_=*/ + {VERSIONED_PROXY_PROTOCOL_STATS(POOL_COUNTER_PREFIX( + scope, absl::StrCat(kProxyProtoStatsPrefix, kVersionStatsPrefix, "v2.")))}, + }; +} + +void GeneralProxyProtocolStats::increment(ReadOrParseState decision) { + switch (decision) { + case ReadOrParseState::Done: + not_found_allowed_.inc(); + break; + case ReadOrParseState::TryAgainLater: + break; // Do nothing. + case ReadOrParseState::Error: + not_found_disallowed_.inc(); + break; + case ReadOrParseState::Denied: + IS_ENVOY_BUG("ReadOrParseState can never be Denied when proxy protocol is not found"); + break; + } +} + +void VersionedProxyProtocolStats::increment(ReadOrParseState decision) { + switch (decision) { + case ReadOrParseState::Done: + found_.inc(); + break; + case ReadOrParseState::TryAgainLater: + break; // Do nothing. + case ReadOrParseState::Error: + error_.inc(); + break; + case ReadOrParseState::Denied: + found_.inc(); + disallowed_.inc(); + break; + } +} + Config::Config( Stats::Scope& scope, const envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol& proto_config) - : stats_{ALL_PROXY_PROTOCOL_STATS(POOL_COUNTER(scope))}, + : stats_(ProxyProtocolStats::create(scope)), allow_requests_without_proxy_protocol_(proto_config.allow_requests_without_proxy_protocol()), pass_all_tlvs_(proto_config.has_pass_through_tlvs() ? proto_config.pass_through_tlvs().match_type() == @@ -67,6 +119,24 @@ Config::Config( pass_through_tlvs_.insert(0xFF & tlv_type); } } + + for (const auto& version : proto_config.disallowed_versions()) { + switch (version) { + PANIC_ON_PROTO_ENUM_SENTINEL_VALUES; + case ProxyProtocolConfig::V1: + allow_v1_ = false; + break; + case ProxyProtocolConfig::V2: + allow_v2_ = false; + break; + } + } + + // Remove this check if PROXY protocol v3 is ever introduced. + if (!allow_v1_ && !allow_v2_) { + throw ProtoValidationException( + "Proxy Protocol filter is misconfigured: all proxy protocol versions are disallowed."); + } } const KeyValuePair* Config::isTlvTypeNeeded(uint8_t type) const { @@ -91,6 +161,10 @@ bool Config::allowRequestsWithoutProxyProtocol() const { return allow_requests_without_proxy_protocol_; } +bool Config::isVersionV1Allowed() const { return allow_v1_; } + +bool Config::isVersionV2Allowed() const { return allow_v2_; } + Network::FilterStatus Filter::onAccept(Network::ListenerFilterCallbacks& cb) { ENVOY_LOG(debug, "proxy_protocol: New connection accepted"); cb_ = &cb; @@ -99,16 +173,31 @@ Network::FilterStatus Filter::onAccept(Network::ListenerFilterCallbacks& cb) { } Network::FilterStatus Filter::onData(Network::ListenerFilterBuffer& buffer) { - const ReadOrParseState read_state = parseBuffer(buffer); + const ReadOrParseState read_state = parseBuffer(buffer); // Implicitly updates header_version_ + + switch (header_version_) { + case ProxyProtocolVersion::V1: + config_->stats_.v1_.increment(read_state); + break; + case ProxyProtocolVersion::V2: + config_->stats_.v2_.increment(read_state); + break; + case ProxyProtocolVersion::NotFound: + config_->stats_.general_.increment(read_state); + break; + } + switch (read_state) { + case ReadOrParseState::Denied: + cb_->socket().ioHandle().close(); + return Network::FilterStatus::StopIteration; case ReadOrParseState::Error: - config_->stats_.downstream_cx_proxy_proto_error_.inc(); + config_->stats_.legacy_.downstream_cx_proxy_proto_error_ + .inc(); // Keep for backwards-compatibility cb_->socket().ioHandle().close(); return Network::FilterStatus::StopIteration; case ReadOrParseState::TryAgainLater: return Network::FilterStatus::StopIteration; - case ReadOrParseState::SkipFilter: - return Network::FilterStatus::Continue; case ReadOrParseState::Done: return Network::FilterStatus::Continue; } @@ -125,6 +214,10 @@ ReadOrParseState Filter::parseBuffer(Network::ListenerFilterBuffer& buffer) { if (read_header_state != ReadOrParseState::Done) { return read_header_state; } + if (header_version_ == ProxyProtocolVersion::NotFound) { + // Filter is skipped and request is allowed through. + return ReadOrParseState::Done; + } } // After parse the header, the extensions size is discovered. Then extend the buffer @@ -497,23 +590,25 @@ ReadOrParseState Filter::readProxyHeader(Network::ListenerFilterBuffer& buffer) auto raw_slice = buffer.rawSlice(); const char* buf = static_cast(raw_slice.mem_); - if (config_.get()->allowRequestsWithoutProxyProtocol()) { - auto matchv2 = !memcmp(buf, PROXY_PROTO_V2_SIGNATURE, + if (config_->allowRequestsWithoutProxyProtocol()) { + auto matchv2 = config_->isVersionV2Allowed() && + !memcmp(buf, PROXY_PROTO_V2_SIGNATURE, std::min(PROXY_PROTO_V2_SIGNATURE_LEN, raw_slice.len_)); - auto matchv1 = !memcmp(buf, PROXY_PROTO_V1_SIGNATURE, + auto matchv1 = config_->isVersionV1Allowed() && + !memcmp(buf, PROXY_PROTO_V1_SIGNATURE, std::min(PROXY_PROTO_V1_SIGNATURE_LEN, raw_slice.len_)); if (!matchv2 && !matchv1) { // The bytes we have seen so far do not match v1 or v2 proxy protocol, so we can safely // short-circuit ENVOY_LOG(trace, "request does not use v1 or v2 proxy protocol, forwarding as is"); - return ReadOrParseState::SkipFilter; + return ReadOrParseState::Done; } } if (raw_slice.len_ >= PROXY_PROTO_V2_HEADER_LEN) { const char* sig = PROXY_PROTO_V2_SIGNATURE; if (!memcmp(buf, sig, PROXY_PROTO_V2_SIGNATURE_LEN)) { - header_version_ = V2; + header_version_ = ProxyProtocolVersion::V2; } else if (memcmp(buf, PROXY_PROTO_V1_SIGNATURE, PROXY_PROTO_V1_SIGNATURE_LEN)) { // It is not v2, and can't be v1, so no sense hanging around: it is invalid ENVOY_LOG(debug, "failed to read proxy protocol (exceed max v1 header len)"); @@ -521,7 +616,11 @@ ReadOrParseState Filter::readProxyHeader(Network::ListenerFilterBuffer& buffer) } } - if (header_version_ == V2) { + if (header_version_ == ProxyProtocolVersion::V2) { + if (!config_->isVersionV2Allowed()) { + ENVOY_LOG(trace, "Filter is not configured to allow v2 proxy protocol requests"); + return ReadOrParseState::Denied; + } const int ver_cmd = buf[PROXY_PROTO_V2_SIGNATURE_LEN]; if (((ver_cmd & 0xf0) >> 4) != PROXY_PROTO_V2_VERSION) { ENVOY_LOG(debug, "Unsupported V2 proxy protocol version"); @@ -560,7 +659,7 @@ ReadOrParseState Filter::readProxyHeader(Network::ListenerFilterBuffer& buffer) // for more data. break; } else { - header_version_ = V1; + header_version_ = ProxyProtocolVersion::V1; search_index_++; } break; @@ -571,7 +670,11 @@ ReadOrParseState Filter::readProxyHeader(Network::ListenerFilterBuffer& buffer) return ReadOrParseState::Error; } - if (header_version_ == V1) { + if (header_version_ == ProxyProtocolVersion::V1) { + if (!config_->isVersionV1Allowed()) { + ENVOY_LOG(trace, "Filter is not configured to allow v1 proxy protocol requests"); + return ReadOrParseState::Denied; + } if (parseV1Header(buf, search_index_)) { return ReadOrParseState::Done; } else { diff --git a/source/extensions/filters/listener/proxy_protocol/proxy_protocol.h b/source/extensions/filters/listener/proxy_protocol/proxy_protocol.h index 228dc6f366fb..b3507e1a6071 100644 --- a/source/extensions/filters/listener/proxy_protocol/proxy_protocol.h +++ b/source/extensions/filters/listener/proxy_protocol/proxy_protocol.h @@ -23,19 +23,80 @@ namespace ProxyProtocol { using KeyValuePair = envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol::KeyValuePair; +enum class ProxyProtocolVersion { NotFound = 0, V1 = 1, V2 = 2 }; + +enum class ReadOrParseState { Done, TryAgainLater, Error, Denied }; + /** - * All stats for the proxy protocol. @see stats_macros.h + * Legacy stats that are under the root scope, not under the filter's scope. + * Kept for backwards compatibility. + * @deprecated Use GeneralProxyProtocolStats instead. + * @see stats_macros.h */ // clang-format off -#define ALL_PROXY_PROTOCOL_STATS(COUNTER) \ +#define LEGACY_PROXY_PROTOCOL_STATS(COUNTER) \ COUNTER(downstream_cx_proxy_proto_error) // clang-format on +struct LegacyProxyProtocolStats { + LEGACY_PROXY_PROTOCOL_STATS(GENERATE_COUNTER_STRUCT) +}; + +/** + * Stats reported for the filter, rooted under the filter's scope. + * @see stats_macros.h + */ +// clang-format off +#define GENERAL_PROXY_PROTOCOL_STATS(COUNTER) \ + COUNTER(not_found_allowed) \ + COUNTER(not_found_disallowed) +// clang-format on + +struct GeneralProxyProtocolStats { + GENERAL_PROXY_PROTOCOL_STATS(GENERATE_COUNTER_STRUCT) + + /** + * Increment the stats for the given filter decision. + */ + void increment(ReadOrParseState decision); +}; + +/** + * Stats reported for each version of the proxy protocol. + * @see stats_macros.h + */ +// clang-format off +#define VERSIONED_PROXY_PROTOCOL_STATS(COUNTER) \ + COUNTER(found) \ + COUNTER(disallowed) \ + COUNTER(error) +// clang-format on + +struct VersionedProxyProtocolStats { + VERSIONED_PROXY_PROTOCOL_STATS(GENERATE_COUNTER_STRUCT) + + /** + * Increment the stats for the given filter decision. + */ + void increment(ReadOrParseState decision); +}; + /** * Definition of all stats for the proxy protocol. @see stats_macros.h */ struct ProxyProtocolStats { - ALL_PROXY_PROTOCOL_STATS(GENERATE_COUNTER_STRUCT) + LegacyProxyProtocolStats legacy_; + GeneralProxyProtocolStats general_; + VersionedProxyProtocolStats v1_; + VersionedProxyProtocolStats v2_; + + /** + * Create an instance of the stats struct with all stats for the filter. + * + * For backwards compatibility, the legacy stats are rooted under their own scope. + * The general and versioned stats are correctly rooted at this filter's own scope. + */ + static ProxyProtocolStats create(Stats::Scope& scope); }; /** @@ -71,19 +132,23 @@ class Config : public Logger::Loggable { */ bool allowRequestsWithoutProxyProtocol() const; + /** + * Filter configuration that determines if a version is disallowed. + */ + bool isVersionV1Allowed() const; + bool isVersionV2Allowed() const; + private: absl::flat_hash_map tlv_types_; const bool allow_requests_without_proxy_protocol_; const bool pass_all_tlvs_; absl::flat_hash_set pass_through_tlvs_{}; + bool allow_v1_{true}; + bool allow_v2_{true}; }; using ConfigSharedPtr = std::shared_ptr; -enum ProxyProtocolVersion { Unknown = 0, V1 = 1, V2 = 2 }; - -enum class ReadOrParseState { Done, TryAgainLater, Error, SkipFilter }; - /** * Implementation the PROXY Protocol listener filter * (https://github.com/haproxy/haproxy/blob/master/doc/proxy-protocol.txt) @@ -134,7 +199,7 @@ class Filter : public Network::ListenerFilter, Logger::Loggablemutable_on_tlv_present()->set_key("PP2TypeAuthority"); auto* listener = bootstrap.mutable_static_resources()->mutable_listeners(0); + listener->set_stat_prefix("test_listener"); auto* ppv_filter = listener->add_listener_filters(); - ppv_filter->set_name("envoy.listener.proxy_protocol"); - ppv_filter->mutable_typed_config()->PackFrom(proxy_protocol); + ppv_filter->set_name(kProxyProtoFilterName); + ASSERT_TRUE(ppv_filter->mutable_typed_config()->PackFrom(proxy_protocol)); } ProxyProtoIntegrationTest::ProxyProtoIntegrationTest() @@ -109,6 +114,14 @@ TEST_P(ProxyProtoIntegrationTest, V2RouterRequestAndResponseWithBodyNoBufferV6) }; testRouterRequestAndResponseWithBody(1024, 512, false, false, &creator); + + // Verify stats (with tags for proxy protocol version). + const auto found_counter = test_server_->counter("proxy_proto.versions.v2.found"); + EXPECT_EQ(found_counter->value(), 1UL); + EXPECT_EQ(found_counter->tagExtractedName(), "proxy_proto.found"); + EXPECT_THAT(found_counter->tags(), IsSupersetOf(Stats::TagVector{ + {"envoy.proxy_protocol_version", "2"}, + })); } TEST_P(ProxyProtoIntegrationTest, RouterProxyUnknownRequestAndResponseWithBodyNoBuffer) { @@ -355,4 +368,74 @@ TEST_P(ProxyProtoFilterChainMatchIntegrationTest, MoreSpecificDirectSource) { absl::StrCat("- ", StreamInfo::ResponseCodeDetails::get().FilterChainNotFound))); } +ProxyProtoDisallowedVersionsIntegrationTest::ProxyProtoDisallowedVersionsIntegrationTest() { + config_helper_.skipPortUsageValidation(); + config_helper_.addConfigModifier([&](envoy::config::bootstrap::v3::Bootstrap& bootstrap) -> void { + // This test doesn't need to deal with upstream connections at all, so make sure none occur. + bootstrap.mutable_static_resources()->mutable_clusters(0)->clear_load_assignment(); + + // V1 is disallowed. + ::envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol proxy_protocol; + proxy_protocol.add_disallowed_versions(::envoy::config::core::v3::ProxyProtocolConfig::V1); + + auto* listener = bootstrap.mutable_static_resources()->mutable_listeners(0); + auto* ppv_filter = listener->mutable_listener_filters(0); + ASSERT_EQ(ppv_filter->name(), kProxyProtoFilterName); + // Overwrite. + ASSERT_TRUE(ppv_filter->mutable_typed_config()->PackFrom(proxy_protocol)); + }); +} + +INSTANTIATE_TEST_SUITE_P(IpVersions, ProxyProtoDisallowedVersionsIntegrationTest, + testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), + TestUtility::ipTestParamsToString); + +// Validate Envoy closes connection when PROXY protocol version 1 is used. +TEST_P(ProxyProtoDisallowedVersionsIntegrationTest, V1Disallowed) { + initialize(); + + IntegrationTcpClientPtr tcp_client = makeTcpConnection(lookupPort("tcp_proxy")); + ASSERT_TRUE(tcp_client->write("PROXY TCP4 1.2.3.4 254.254.254.254 12345 1234\r\nhello", + /*end_stream=*/false, /*verify=*/false)); + tcp_client->waitForDisconnect(); + + // Verify stats (with tags for proxy protocol version). + const auto found_counter = test_server_->counter("proxy_proto.versions.v1.found"); + EXPECT_EQ(found_counter->value(), 1UL); + EXPECT_EQ(found_counter->tagExtractedName(), "proxy_proto.found"); + EXPECT_THAT(found_counter->tags(), IsSupersetOf(Stats::TagVector{ + {"envoy.proxy_protocol_version", "1"}, + })); + + const auto disallowed_counter = test_server_->counter("proxy_proto.versions.v1.disallowed"); + EXPECT_EQ(disallowed_counter->value(), 1UL); + EXPECT_EQ(disallowed_counter->tagExtractedName(), "proxy_proto.disallowed"); + EXPECT_THAT(disallowed_counter->tags(), IsSupersetOf(Stats::TagVector{ + {"envoy.proxy_protocol_version", "1"}, + })); +} + +// Validate Envoy closes connection when PROXY protocol version 2 has parsing error. +TEST_P(ProxyProtoDisallowedVersionsIntegrationTest, V2Error) { + // A well-formed message with an unsupported address family + constexpr uint8_t buffer[] = {0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49, + 0x54, 0x0a, 0x21, 0x41, 0x00, 0x0c, 0x01, 0x02, 0x03, 0x04, + 0x00, 0x01, 0x01, 0x02, 0x03, 0x05, 0x00, 0x02, 'm', 'o', + 'r', 'e', ' ', 'd', 'a', 't', 'a'}; + + initialize(); + IntegrationTcpClientPtr tcp_client = makeTcpConnection(lookupPort("tcp_proxy")); + Buffer::OwnedImpl buf(buffer, sizeof(buffer)); + ASSERT_TRUE(tcp_client->write(buf.toString(), /*end_stream=*/false, /*verify=*/false)); + tcp_client->waitForDisconnect(); + + // Verify stats (with tags for proxy protocol version). + const auto found_counter = test_server_->counter("proxy_proto.versions.v2.error"); + EXPECT_EQ(found_counter->value(), 1UL); + EXPECT_EQ(found_counter->tagExtractedName(), "proxy_proto.error"); + EXPECT_THAT(found_counter->tags(), IsSupersetOf(Stats::TagVector{ + {"envoy.proxy_protocol_version", "2"}, + })); +} + } // namespace Envoy diff --git a/test/extensions/filters/listener/proxy_protocol/proxy_proto_integration_test.h b/test/extensions/filters/listener/proxy_protocol/proxy_proto_integration_test.h index bdc45933b7ff..64fb19c833f5 100644 --- a/test/extensions/filters/listener/proxy_protocol/proxy_proto_integration_test.h +++ b/test/extensions/filters/listener/proxy_protocol/proxy_proto_integration_test.h @@ -31,4 +31,9 @@ class ProxyProtoFilterChainMatchIntegrationTest : public ProxyProtoTcpIntegratio void send(const std::string& data); }; +class ProxyProtoDisallowedVersionsIntegrationTest : public ProxyProtoTcpIntegrationTest { +public: + ProxyProtoDisallowedVersionsIntegrationTest(); +}; + } // namespace Envoy diff --git a/test/extensions/filters/listener/proxy_protocol/proxy_protocol_test.cc b/test/extensions/filters/listener/proxy_protocol/proxy_protocol_test.cc index 26f55921fa3e..bd40f59e11eb 100644 --- a/test/extensions/filters/listener/proxy_protocol/proxy_protocol_test.cc +++ b/test/extensions/filters/listener/proxy_protocol/proxy_protocol_test.cc @@ -34,6 +34,7 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" +using envoy::config::core::v3::ProxyProtocolConfig; using envoy::config::core::v3::ProxyProtocolPassThroughTLVs; using Envoy::Extensions::Common::ProxyProtocol::PROXY_PROTO_V1_SIGNATURE_LEN; using Envoy::Extensions::Common::ProxyProtocol::PROXY_PROTO_V2_SIGNATURE_LEN; @@ -52,8 +53,16 @@ namespace ListenerFilters { namespace ProxyProtocol { namespace { -// Build again on the basis of the connection_handler_test.cc +TEST(ConfigTest, AllVersionsCannotBeDisallowed) { + envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol proto_config; + proto_config.mutable_disallowed_versions()->Add(ProxyProtocolConfig::V1); + proto_config.mutable_disallowed_versions()->Add(ProxyProtocolConfig::V2); + Stats::TestUtil::TestStore stats_store; + EXPECT_THROW(ProxyProtocol::Config(*stats_store.rootScope(), proto_config), + ProtoValidationException); +} +// Build again on the basis of the connection_handler_test.cc class ProxyProtocolTest : public testing::TestWithParam, public Network::ListenerConfig, public Network::FilterChainManager, @@ -97,7 +106,9 @@ class ProxyProtocolTest : public testing::TestWithParam bool { filter_manager.addAcceptFilter( nullptr, std::make_unique(std::make_shared( - listenerScope(), (nullptr != proto_config) - ? *proto_config - : envoy::extensions::filters::listener:: - proxy_protocol::v3::ProxyProtocol()))); + *stats_store_.rootScope(), + (nullptr != proto_config) ? *proto_config + : envoy::extensions::filters::listener:: + proxy_protocol::v3::ProxyProtocol()))); maybeExitDispatcher(); return true; })); @@ -242,6 +253,7 @@ TEST_P(ProxyProtocolTest, V1UnsupportedIPv4) { Cleanup cleaner = Network::Address::Ipv4Instance::forceProtocolUnsupportedForTest(true); write("PROXY TCP4 1.2.3.4 253.253.253.253 65535 1234\r\nmore data"); expectProxyProtoError(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v1.error").value(), 1); } TEST_P(ProxyProtocolTest, V1UnsupportedIPv6) { @@ -249,6 +261,7 @@ TEST_P(ProxyProtocolTest, V1UnsupportedIPv6) { Cleanup cleaner = Network::Address::Ipv6Instance::forceProtocolUnsupportedForTest(true); write("PROXY TCP6 1:2:3::4 5:6::7:8 65535 1234\r\nmore data"); expectProxyProtoError(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v1.error").value(), 1); } TEST_P(ProxyProtocolTest, V1Basic) { @@ -262,6 +275,7 @@ TEST_P(ProxyProtocolTest, V1Basic) { EXPECT_TRUE(server_connection_->connectionInfoProvider().localAddressRestored()); disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v1.found").value(), 1); } TEST_P(ProxyProtocolTest, AllowTinyNoProxyProtocol) { @@ -279,6 +293,7 @@ TEST_P(ProxyProtocolTest, AllowTinyNoProxyProtocol) { write(msg); expectData(msg); disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.not_found_allowed").value(), 1); } TEST_P(ProxyProtocolTest, AllowTinyNoProxyProtocolPartialMatchesV1First) { @@ -298,6 +313,7 @@ TEST_P(ProxyProtocolTest, AllowTinyNoProxyProtocolPartialMatchesV1First) { write(msg); expectData(msg); disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.not_found_allowed").value(), 1); } TEST_P(ProxyProtocolTest, AllowTinyNoProxyProtocolPartialMatchesV2First) { @@ -317,6 +333,7 @@ TEST_P(ProxyProtocolTest, AllowTinyNoProxyProtocolPartialMatchesV2First) { write(msg); expectData(msg); disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.not_found_allowed").value(), 1); } TEST_P(ProxyProtocolTest, AllowLargeNoProxyProtocol) { @@ -334,6 +351,7 @@ TEST_P(ProxyProtocolTest, AllowLargeNoProxyProtocol) { write(msg); expectData(msg); disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.not_found_allowed").value(), 1); } TEST_P(ProxyProtocolTest, V1Minimal) { @@ -352,6 +370,7 @@ TEST_P(ProxyProtocolTest, V1Minimal) { EXPECT_FALSE(server_connection_->connectionInfoProvider().localAddressRestored()); disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v1.found").value(), 1); } TEST_P(ProxyProtocolTest, V2Basic) { @@ -370,6 +389,7 @@ TEST_P(ProxyProtocolTest, V2Basic) { EXPECT_TRUE(server_connection_->connectionInfoProvider().localAddressRestored()); disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.found").value(), 1); } TEST_P(ProxyProtocolTest, BasicV6) { @@ -383,6 +403,7 @@ TEST_P(ProxyProtocolTest, BasicV6) { EXPECT_TRUE(server_connection_->connectionInfoProvider().localAddressRestored()); disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v1.found").value(), 1); } TEST_P(ProxyProtocolTest, V2BasicV6) { @@ -403,6 +424,7 @@ TEST_P(ProxyProtocolTest, V2BasicV6) { EXPECT_TRUE(server_connection_->connectionInfoProvider().localAddressRestored()); disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.found").value(), 1); } TEST_P(ProxyProtocolTest, V2UnsupportedIPv4) { @@ -416,6 +438,7 @@ TEST_P(ProxyProtocolTest, V2UnsupportedIPv4) { Cleanup cleaner = Network::Address::Ipv4Instance::forceProtocolUnsupportedForTest(true); write(buffer, sizeof(buffer)); expectProxyProtoError(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.error").value(), 1); } TEST_P(ProxyProtocolTest, V2UnsupportedIPv6) { @@ -431,6 +454,7 @@ TEST_P(ProxyProtocolTest, V2UnsupportedIPv6) { Cleanup cleaner = Network::Address::Ipv6Instance::forceProtocolUnsupportedForTest(true); write(buffer, sizeof(buffer)); expectProxyProtoError(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.error").value(), 1); } TEST_P(ProxyProtocolTest, V2UnsupportedAF) { @@ -443,6 +467,7 @@ TEST_P(ProxyProtocolTest, V2UnsupportedAF) { write(buffer, sizeof(buffer)); expectProxyProtoError(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.error").value(), 1); } TEST_P(ProxyProtocolTest, ErrorRecv_2) { @@ -625,6 +650,7 @@ TEST_P(ProxyProtocolTest, V2NotLocalOrOnBehalf) { write(buffer, sizeof(buffer)); expectProxyProtoError(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.error").value(), 1); } TEST_P(ProxyProtocolTest, V2LocalConnection) { @@ -646,6 +672,7 @@ TEST_P(ProxyProtocolTest, V2LocalConnection) { } EXPECT_FALSE(server_connection_->connectionInfoProvider().localAddressRestored()); disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.found").value(), 1); } TEST_P(ProxyProtocolTest, V2LocalConnectionExtension) { @@ -667,6 +694,7 @@ TEST_P(ProxyProtocolTest, V2LocalConnectionExtension) { } EXPECT_FALSE(server_connection_->connectionInfoProvider().localAddressRestored()); disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.found").value(), 1); } TEST_P(ProxyProtocolTest, V2LocalConnectionFilterState) { @@ -695,6 +723,7 @@ TEST_P(ProxyProtocolTest, V2LocalConnectionFilterState) { } EXPECT_FALSE(server_connection_->connectionInfoProvider().localAddressRestored()); disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.found").value(), 1); } TEST_P(ProxyProtocolTest, V2ShortV4) { @@ -706,6 +735,7 @@ TEST_P(ProxyProtocolTest, V2ShortV4) { write(buffer, sizeof(buffer)); expectProxyProtoError(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.error").value(), 1); } TEST_P(ProxyProtocolTest, V2ShortV4WithAllowNoProxyProtocol) { @@ -719,6 +749,7 @@ TEST_P(ProxyProtocolTest, V2ShortV4WithAllowNoProxyProtocol) { write(buffer, sizeof(buffer)); expectProxyProtoError(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.error").value(), 1); } TEST_P(ProxyProtocolTest, V2ShortAddrV4) { @@ -731,6 +762,7 @@ TEST_P(ProxyProtocolTest, V2ShortAddrV4) { write(buffer, sizeof(buffer)); expectProxyProtoError(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.error").value(), 1); } TEST_P(ProxyProtocolTest, V2ShortV6) { @@ -743,6 +775,7 @@ TEST_P(ProxyProtocolTest, V2ShortV6) { write(buffer, sizeof(buffer)); expectProxyProtoError(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.error").value(), 1); } TEST_P(ProxyProtocolTest, V2ShortAddrV6) { @@ -757,6 +790,7 @@ TEST_P(ProxyProtocolTest, V2ShortAddrV6) { write(buffer, sizeof(buffer)); expectProxyProtoError(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.error").value(), 1); } TEST_P(ProxyProtocolTest, V2AF_UNIX) { @@ -769,6 +803,7 @@ TEST_P(ProxyProtocolTest, V2AF_UNIX) { write(buffer, sizeof(buffer)); expectProxyProtoError(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.error").value(), 1); } TEST_P(ProxyProtocolTest, V2BadCommand) { @@ -781,6 +816,7 @@ TEST_P(ProxyProtocolTest, V2BadCommand) { write(buffer, sizeof(buffer)); expectProxyProtoError(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.error").value(), 1); } TEST_P(ProxyProtocolTest, V2WrongVersion) { @@ -792,6 +828,7 @@ TEST_P(ProxyProtocolTest, V2WrongVersion) { connect(false); write(buffer, sizeof(buffer)); expectProxyProtoError(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.error").value(), 1); } TEST_P(ProxyProtocolTest, V1TooLong) { @@ -802,6 +839,8 @@ TEST_P(ProxyProtocolTest, V1TooLong) { write(buffer, sizeof(buffer)); } expectProxyProtoError(); + // Not tracked as v1 due to missing /r/n at end + EXPECT_EQ(stats_store_.counter("proxy_proto.not_found_disallowed").value(), 1); } TEST_P(ProxyProtocolTest, V1TooLongWithAllowNoProxyProtocol) { @@ -814,6 +853,11 @@ TEST_P(ProxyProtocolTest, V1TooLongWithAllowNoProxyProtocol) { write(buffer, sizeof(buffer)); } expectProxyProtoError(); + // Not allowed as unknown because of PROXY v1 signature match. + // Not tracked as v1 due to missing /r/n at end. + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v1.error").value(), 0); + EXPECT_EQ(stats_store_.counter("proxy_proto.not_found_allowed").value(), 0); + EXPECT_EQ(stats_store_.counter("proxy_proto.not_found_disallowed").value(), 1); } TEST_P(ProxyProtocolTest, V2ParseExtensions) { @@ -834,6 +878,7 @@ TEST_P(ProxyProtocolTest, V2ParseExtensions) { write(data, sizeof(data)); expectData("DATA"); disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.found").value(), 1); } TEST_P(ProxyProtocolTest, V2ParseExtensionsRecvError) { @@ -961,6 +1006,7 @@ TEST_P(ProxyProtocolTest, V2ParseExtensionsFrag) { write(data, sizeof(data)); expectData("DATA"); disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.found").value(), 1); } TEST_P(ProxyProtocolTest, Fragmented) { @@ -982,6 +1028,7 @@ TEST_P(ProxyProtocolTest, Fragmented) { EXPECT_TRUE(server_connection_->connectionInfoProvider().localAddressRestored()); disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v1.found").value(), 1); } TEST_P(ProxyProtocolTest, V2Fragmented1) { @@ -1004,6 +1051,7 @@ TEST_P(ProxyProtocolTest, V2Fragmented1) { EXPECT_TRUE(server_connection_->connectionInfoProvider().localAddressRestored()); disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.found").value(), 1); } TEST_P(ProxyProtocolTest, V2Fragmented2) { @@ -1026,6 +1074,7 @@ TEST_P(ProxyProtocolTest, V2Fragmented2) { EXPECT_TRUE(server_connection_->connectionInfoProvider().localAddressRestored()); disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.found").value(), 1); } TEST_P(ProxyProtocolTest, V2Fragmented3) { @@ -1050,6 +1099,7 @@ TEST_P(ProxyProtocolTest, V2Fragmented3) { EXPECT_TRUE(server_connection_->connectionInfoProvider().localAddressRestored()); disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.found").value(), 1); } TEST_P(ProxyProtocolTest, V2Fragmented4Error) { @@ -1291,6 +1341,7 @@ TEST_P(ProxyProtocolTest, PartialRead) { EXPECT_TRUE(server_connection_->connectionInfoProvider().localAddressRestored()); disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v1.found").value(), 1); } TEST_P(ProxyProtocolTest, PartialV1ReadWithAllowNoProxyProtocol) { @@ -1312,6 +1363,7 @@ TEST_P(ProxyProtocolTest, PartialV1ReadWithAllowNoProxyProtocol) { "254.254.254.254"); EXPECT_TRUE(server_connection_->connectionInfoProvider().localAddressRestored()); disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v1.found").value(), 1); } TEST_P(ProxyProtocolTest, TinyPartialV1ReadWithAllowNoProxyProtocol) { @@ -1337,6 +1389,7 @@ TEST_P(ProxyProtocolTest, TinyPartialV1ReadWithAllowNoProxyProtocol) { "254.254.254.254"); EXPECT_TRUE(server_connection_->connectionInfoProvider().localAddressRestored()); disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v1.found").value(), 1); } TEST_P(ProxyProtocolTest, V2PartialRead) { @@ -1362,6 +1415,7 @@ TEST_P(ProxyProtocolTest, V2PartialRead) { EXPECT_TRUE(server_connection_->connectionInfoProvider().localAddressRestored()); disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.found").value(), 1); } TEST_P(ProxyProtocolTest, PartialV2ReadWithAllowNoProxyProtocol) { @@ -1391,6 +1445,7 @@ TEST_P(ProxyProtocolTest, PartialV2ReadWithAllowNoProxyProtocol) { "1.2.3.4"); EXPECT_TRUE(server_connection_->connectionInfoProvider().localAddressRestored()); disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.found").value(), 1); } TEST_P(ProxyProtocolTest, TinyPartialV2ReadWithAllowNoProxyProtocol) { @@ -1420,6 +1475,7 @@ TEST_P(ProxyProtocolTest, TinyPartialV2ReadWithAllowNoProxyProtocol) { "1.2.3.4"); EXPECT_TRUE(server_connection_->connectionInfoProvider().localAddressRestored()); disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.found").value(), 1); } const std::string ProxyProtocol = "envoy.filters.listener.proxy_protocol"; @@ -1463,6 +1519,7 @@ TEST_P(ProxyProtocolTest, V2ParseExtensionsLargeThanInitMaxReadBytes) { EXPECT_EQ(tlv_data, value_s); disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.found").value(), 1); } TEST_P(ProxyProtocolTest, V2ExtractTlvOfInterest) { @@ -1502,6 +1559,7 @@ TEST_P(ProxyProtocolTest, V2ExtractTlvOfInterest) { auto value_s = fields.at("PP2 type authority").string_value(); ASSERT_THAT(value_s, ElementsAre(0x66, 0x6f, 0x6f, 0x2e, 0x63, 0x6f, 0x6d)); disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.found").value(), 1); } TEST_P(ProxyProtocolTest, V2ExtractTlvOfInterestAndEmitWithSpecifiedMetadataNamespace) { @@ -1542,6 +1600,7 @@ TEST_P(ProxyProtocolTest, V2ExtractTlvOfInterestAndEmitWithSpecifiedMetadataName auto value_s = fields.at("PP2 type authority").string_value(); ASSERT_THAT(value_s, ElementsAre(0x66, 0x6f, 0x6f, 0x2e, 0x63, 0x6f, 0x6d)); disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.found").value(), 1); } TEST_P(ProxyProtocolTest, V2ExtractMultipleTlvsOfInterest) { @@ -1601,6 +1660,7 @@ TEST_P(ProxyProtocolTest, V2ExtractMultipleTlvsOfInterest) { ElementsAre(0x01, 0x76, 0x70, 0x63, 0x2d, 0x30, 0x32, 0x35, 0x74, 0x65, 0x73, 0x74, 0x32, 0x66, 0x61, 0x36, 0x63, 0x36, 0x33, 0x68, 0x61, 0x37)); disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.found").value(), 1); } TEST_P(ProxyProtocolTest, V2ExtractMultipleTlvsOfInterestAndSanitiseNonUtf8) { @@ -1665,6 +1725,7 @@ TEST_P(ProxyProtocolTest, V2ExtractMultipleTlvsOfInterestAndSanitiseNonUtf8) { ElementsAre(0x01, 0x76, 0x70, 0x63, 0x2d, 0x30, replacement, 0x35, 0x74, 0x65, 0x73, 0x74, 0x32, 0x66, 0x61, 0x36, 0x63, 0x36, 0x33, 0x68, replacement, 0x37)); disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.found").value(), 1); } TEST_P(ProxyProtocolTest, V2WillNotOverwriteTLV) { @@ -1714,6 +1775,7 @@ TEST_P(ProxyProtocolTest, V2WillNotOverwriteTLV) { ASSERT_THAT(value_type_authority, ElementsAre(0x66, 0x6f, 0x6f, 0x2e, 0x63, 0x6f, 0x6d)); disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.found").value(), 1); } TEST_P(ProxyProtocolTest, V2WrongTLVLength) { @@ -1736,6 +1798,7 @@ TEST_P(ProxyProtocolTest, V2WrongTLVLength) { write(tlv, sizeof(tlv)); expectProxyProtoError(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.error").value(), 1); } TEST_P(ProxyProtocolTest, V2IncompleteTLV) { @@ -1765,6 +1828,7 @@ TEST_P(ProxyProtocolTest, V2IncompleteTLV) { write(tlv2, sizeof(tlv2)); expectProxyProtoError(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.error").value(), 1); } TEST_P(ProxyProtocolTest, V2ExtractTLVToFilterState) { @@ -1812,6 +1876,7 @@ TEST_P(ProxyProtocolTest, V2ExtractTLVToFilterState) { proxy_proto_data.tlv_vector_[1].value.end())); disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.found").value(), 1); } TEST_P(ProxyProtocolTest, V2ExtractTLVToFilterStateIncludeEmpty) { @@ -1848,6 +1913,7 @@ TEST_P(ProxyProtocolTest, V2ExtractTLVToFilterStateIncludeEmpty) { EXPECT_EQ(0, proxy_proto_data.tlv_vector_.size()); disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.found").value(), 1); } TEST_P(ProxyProtocolTest, V2ExtractTLVToFilterStateIncludeTlV) { @@ -1888,6 +1954,7 @@ TEST_P(ProxyProtocolTest, V2ExtractTLVToFilterStateIncludeTlV) { EXPECT_EQ("foo.com", std::string(proxy_proto_data.tlv_vector_[0].value.begin(), proxy_proto_data.tlv_vector_[0].value.end())); disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.found").value(), 1); } TEST_P(ProxyProtocolTest, MalformedProxyLine) { @@ -1898,6 +1965,22 @@ TEST_P(ProxyProtocolTest, MalformedProxyLine) { write("\n"); expectProxyProtoError(); + // Tracked as v1 because of trailing \r\n + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v1.error").value(), 1); +} + +TEST_P(ProxyProtocolTest, MalformedProxyLineWithAllowNoProxyProtocol) { + envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol proto_config; + proto_config.set_allow_requests_without_proxy_protocol(true); + connect(true, &proto_config); + + std::string msg = "BOGUS\r\n"; + write(msg); + expectData(msg); + disconnect(); + // Tracked as unknown because `set_allow_requests_without_proxy_protocol` matches v1 signature + // differently that previous test case. + EXPECT_EQ(stats_store_.counter("proxy_proto.not_found_allowed").value(), 1); } TEST_P(ProxyProtocolTest, ProxyLineTooLarge) { @@ -1905,66 +1988,77 @@ TEST_P(ProxyProtocolTest, ProxyLineTooLarge) { write("012345678901234567890123456789012345678901234567890123456789" "012345678901234567890123456789012345678901234567890123456789"); expectProxyProtoError(); + EXPECT_EQ(stats_store_.counter("proxy_proto.not_found_disallowed").value(), 1); } TEST_P(ProxyProtocolTest, NotEnoughFields) { connect(false); write("PROXY TCP6 1:2:3::4 5:6::7:8 1234\r\nmore data"); expectProxyProtoError(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v1.error").value(), 1); } TEST_P(ProxyProtocolTest, UnsupportedProto) { connect(false); write("PROXY UDP6 1:2:3::4 5:6::7:8 1234 5678\r\nmore data"); expectProxyProtoError(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v1.error").value(), 1); } TEST_P(ProxyProtocolTest, InvalidSrcAddress) { connect(false); write("PROXY TCP4 230.0.0.1 10.1.1.3 1234 5678\r\nmore data"); expectProxyProtoError(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v1.error").value(), 1); } TEST_P(ProxyProtocolTest, InvalidDstAddress) { connect(false); write("PROXY TCP4 10.1.1.2 0.0.0.0 1234 5678\r\nmore data"); expectProxyProtoError(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v1.error").value(), 1); } TEST_P(ProxyProtocolTest, BadPort) { connect(false); write("PROXY TCP6 1:2:3::4 5:6::7:8 1234 abc\r\nmore data"); expectProxyProtoError(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v1.error").value(), 1); } TEST_P(ProxyProtocolTest, NegativePort) { connect(false); write("PROXY TCP6 1:2:3::4 5:6::7:8 -1 1234\r\nmore data"); expectProxyProtoError(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v1.error").value(), 1); } TEST_P(ProxyProtocolTest, PortOutOfRange) { connect(false); write("PROXY TCP6 1:2:3::4 5:6::7:8 66776 1234\r\nmore data"); expectProxyProtoError(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v1.error").value(), 1); } TEST_P(ProxyProtocolTest, BadAddress) { connect(false); write("PROXY TCP6 1::2:3::4 5:6::7:8 1234 5678\r\nmore data"); expectProxyProtoError(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v1.error").value(), 1); } TEST_P(ProxyProtocolTest, AddressVersionsNotMatch) { connect(false); write("PROXY TCP4 [1:2:3::4] 1.2.3.4 1234 5678\r\nmore data"); expectProxyProtoError(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v1.error").value(), 1); } TEST_P(ProxyProtocolTest, AddressVersionsNotMatch2) { connect(false); write("PROXY TCP4 1.2.3.4 [1:2:3: 1234 4]:5678\r\nmore data"); expectProxyProtoError(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v1.error").value(), 1); } TEST_P(ProxyProtocolTest, Truncated) { @@ -2089,9 +2183,280 @@ TEST_P(ProxyProtocolTest, DrainError) { write("PROXY TCP4 1.2.3.4 253.253.253.253 65535 1234\r\nmore data"); expectProxyProtoError(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v1.error").value(), 1); } #endif +class ProxyProtocolDisallowedVersionsTest : public ProxyProtocolTest { +public: + virtual envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol + createConfig(const std::vector& disallowed_versions) const { + envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol proto_config; + for (const auto& version : disallowed_versions) { + proto_config.mutable_disallowed_versions()->Add(version); + } + return proto_config; + } +}; + +// Parameterize the listener socket address version. +INSTANTIATE_TEST_SUITE_P(IpVersions, ProxyProtocolDisallowedVersionsTest, + testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), + TestUtility::ipTestParamsToString); + +TEST_P(ProxyProtocolDisallowedVersionsTest, V1DisallowedV2BasicAllowed) { + envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol proto_config = + createConfig({ProxyProtocolConfig::V1}); + connect(true, &proto_config); + + // A well-formed ipv4/tcp message, no extensions + constexpr uint8_t buffer[] = {0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49, + 0x54, 0x0a, 0x21, 0x11, 0x00, 0x0c, 0x01, 0x02, 0x03, 0x04, + 0x00, 0x01, 0x01, 0x02, 0x03, 0x05, 0x00, 0x02, 'm', 'o', + 'r', 'e', ' ', 'd', 'a', 't', 'a'}; + write(buffer, sizeof(buffer)); + expectData("more data"); + disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.found").value(), 1); +} + +TEST_P(ProxyProtocolDisallowedVersionsTest, V2DisallowedV2BasicRejected) { + envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol proto_config = + createConfig({ProxyProtocolConfig::V2}); + connect(false, &proto_config); + + // A well-formed ipv4/tcp message, no extensions + constexpr uint8_t buffer[] = {0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49, + 0x54, 0x0a, 0x21, 0x11, 0x00, 0x0c, 0x01, 0x02, 0x03, 0x04, + 0x00, 0x01, 0x01, 0x02, 0x03, 0x05, 0x00, 0x02, 'm', 'o', + 'r', 'e', ' ', 'd', 'a', 't', 'a'}; + write(buffer, sizeof(buffer)); + expectConnectionError(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.found").value(), 1); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.disallowed").value(), 1); +} + +TEST_P(ProxyProtocolDisallowedVersionsTest, V1DisallowedV2ShortError) { + envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol proto_config = + createConfig({ProxyProtocolConfig::V1}); + connect(false, &proto_config); + + // An ipv4/tcp connection that has incorrect addr-len encoded + constexpr uint8_t buffer[] = {0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49, + 0x54, 0x0a, 0x21, 0x21, 0x00, 0x04, 0x00, 0x08, 0x00, 0x02, + 'm', 'o', 'r', 'e', ' ', 'd', 'a', 't', 'a'}; + write(buffer, sizeof(buffer)); + expectProxyProtoError(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.error").value(), 1); +} + +TEST_P(ProxyProtocolDisallowedVersionsTest, V2DisallowedV2ShortRejected) { + envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol proto_config = + createConfig({ProxyProtocolConfig::V2}); + connect(false, &proto_config); + + // An ipv4/tcp connection that has incorrect addr-len encoded + constexpr uint8_t buffer[] = {0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49, + 0x54, 0x0a, 0x21, 0x21, 0x00, 0x04, 0x00, 0x08, 0x00, 0x02, + 'm', 'o', 'r', 'e', ' ', 'd', 'a', 't', 'a'}; + write(buffer, sizeof(buffer)); + expectConnectionError(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.found").value(), 1); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.disallowed").value(), 1); +} + +TEST_P(ProxyProtocolDisallowedVersionsTest, V2DisallowedV1BasicAllowed) { + envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol proto_config = + createConfig({ProxyProtocolConfig::V2}); + connect(true, &proto_config); + + write("PROXY TCP4 1.2.3.4 253.253.253.253 65535 1234\r\nmore data"); + expectData("more data"); + disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v1.found").value(), 1); +} + +TEST_P(ProxyProtocolDisallowedVersionsTest, V1DisallowedV1BasicRejected) { + envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol proto_config = + createConfig({ProxyProtocolConfig::V1}); + connect(false, &proto_config); + + write("PROXY TCP4 1.2.3.4 253.253.253.253 65535 1234\r\nmore data"); + expectConnectionError(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v1.found").value(), 1); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v1.disallowed").value(), 1); +} + +TEST_P(ProxyProtocolDisallowedVersionsTest, V2DisallowedV1BadPortError) { + envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol proto_config = + createConfig({ProxyProtocolConfig::V2}); + connect(false, &proto_config); + + write("PROXY TCP6 1:2:3::4 5:6::7:8 1234 abc\r\nmore data"); + expectProxyProtoError(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v1.error").value(), 1); +} + +TEST_P(ProxyProtocolDisallowedVersionsTest, V1DisallowedV1BadPortError) { + envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol proto_config = + createConfig({ProxyProtocolConfig::V1}); + connect(false, &proto_config); + + write("PROXY TCP6 1:2:3::4 5:6::7:8 1234 abc\r\nmore data"); + expectConnectionError(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v1.found").value(), 1); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v1.disallowed").value(), 1); +} + +// Tests a combination of `disallowed_versions` and `allow_requests_without_proxy_protocol`. +class ProxyProtocolDisallowedVersionsWithNoProxyProtoTest + : public ProxyProtocolDisallowedVersionsTest { +public: + envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol createConfig( + const std::vector& disallowed_versions) const override { + auto proto_config = ProxyProtocolDisallowedVersionsTest::createConfig(disallowed_versions); + proto_config.set_allow_requests_without_proxy_protocol(true); + return proto_config; + } +}; + +// Parameterize the listener socket address version. +INSTANTIATE_TEST_SUITE_P(IpVersions, ProxyProtocolDisallowedVersionsWithNoProxyProtoTest, + testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), + TestUtility::ipTestParamsToString); + +TEST_P(ProxyProtocolDisallowedVersionsWithNoProxyProtoTest, V1DisallowedV2BasicAllowed) { + envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol proto_config = + createConfig({ProxyProtocolConfig::V1}); + connect(true, &proto_config); + + // A well-formed ipv4/tcp message, no extensions + constexpr uint8_t buffer[] = {0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49, + 0x54, 0x0a, 0x21, 0x11, 0x00, 0x0c, 0x01, 0x02, 0x03, 0x04, + 0x00, 0x01, 0x01, 0x02, 0x03, 0x05, 0x00, 0x02, 'm', 'o', + 'r', 'e', ' ', 'd', 'a', 't', 'a'}; + write(buffer, sizeof(buffer)); + expectData("more data"); + disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.found").value(), 1); +} + +TEST_P(ProxyProtocolDisallowedVersionsWithNoProxyProtoTest, V2DisallowedV2BasicAllowed) { + envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol proto_config = + createConfig({ProxyProtocolConfig::V2}); + connect(true, &proto_config); + + // A well-formed ipv4/tcp message, no extensions + constexpr uint8_t buffer[] = {0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49, + 0x54, 0x0a, 0x21, 0x11, 0x00, 0x0c, 0x01, 0x02, 0x03, 0x04, + 0x00, 0x01, 0x01, 0x02, 0x03, 0x05, 0x00, 0x02, 'm', 'o', + 'r', 'e', ' ', 'd', 'a', 't', 'a'}; + write(buffer, sizeof(buffer)); + expectData(std::string(buffer, buffer + sizeof(buffer))); + disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.not_found_allowed").value(), 1); +} + +TEST_P(ProxyProtocolDisallowedVersionsWithNoProxyProtoTest, V1DisallowedV2ShortError) { + envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol proto_config = + createConfig({ProxyProtocolConfig::V1}); + connect(false, &proto_config); + + // An ipv4/tcp connection that has incorrect addr-len encoded + constexpr uint8_t buffer[] = {0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49, + 0x54, 0x0a, 0x21, 0x21, 0x00, 0x04, 0x00, 0x08, 0x00, 0x02, + 'm', 'o', 'r', 'e', ' ', 'd', 'a', 't', 'a'}; + write(buffer, sizeof(buffer)); + expectProxyProtoError(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.error").value(), 1); +} + +TEST_P(ProxyProtocolDisallowedVersionsWithNoProxyProtoTest, V2DisallowedV2ShortAllowed) { + envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol proto_config = + createConfig({ProxyProtocolConfig::V2}); + connect(true, &proto_config); + + // An ipv4/tcp connection that has incorrect addr-len encoded + constexpr uint8_t buffer[] = {0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49, + 0x54, 0x0a, 0x21, 0x21, 0x00, 0x04, 0x00, 0x08, 0x00, 0x02, + 'm', 'o', 'r', 'e', ' ', 'd', 'a', 't', 'a'}; + write(buffer, sizeof(buffer)); + expectData(std::string(buffer, buffer + sizeof(buffer))); + disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.not_found_allowed").value(), 1); +} + +TEST_P(ProxyProtocolDisallowedVersionsWithNoProxyProtoTest, V2DisallowedV1BasicAllowed) { + envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol proto_config = + createConfig({ProxyProtocolConfig::V2}); + connect(true, &proto_config); + + write("PROXY TCP4 1.2.3.4 253.253.253.253 65535 1234\r\nmore data"); + expectData("more data"); + disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v1.found").value(), 1); +} + +TEST_P(ProxyProtocolDisallowedVersionsWithNoProxyProtoTest, V1DisallowedV1BasicAllowed) { + envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol proto_config = + createConfig({ProxyProtocolConfig::V1}); + connect(true, &proto_config); + + write("PROXY TCP4 1.2.3.4 253.253.253.253 65535 1234\r\nmore data"); + expectData("PROXY TCP4 1.2.3.4 253.253.253.253 65535 1234\r\nmore data"); + disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.not_found_allowed").value(), 1); +} + +TEST_P(ProxyProtocolDisallowedVersionsWithNoProxyProtoTest, V2DisallowedV1BadPortError) { + envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol proto_config = + createConfig({ProxyProtocolConfig::V2}); + connect(false, &proto_config); + + write("PROXY TCP6 1:2:3::4 5:6::7:8 1234 abc\r\nmore data"); + expectProxyProtoError(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v1.error").value(), 1); +} + +TEST_P(ProxyProtocolDisallowedVersionsWithNoProxyProtoTest, V1DisallowedV1BadPortAllowed) { + envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol proto_config = + createConfig({ProxyProtocolConfig::V1}); + connect(true, &proto_config); + + write("PROXY TCP6 1:2:3::4 5:6::7:8 1234 abc\r\nmore data"); + expectData("PROXY TCP6 1:2:3::4 5:6::7:8 1234 abc\r\nmore data"); + disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.not_found_allowed").value(), 1); +} + +// In direct comparison to V1TooLongWithAllowNoProxyProtocol. +TEST_P(ProxyProtocolDisallowedVersionsWithNoProxyProtoTest, + V1DisallowedAllowNoProxyProtocolAndV1NotMatched) { + envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol proto_config = + createConfig({ProxyProtocolConfig::V1}); + connect(true, &proto_config); + + write("PROXY TCP4 1.2.3.4 2.3.4.5 100 100 RANDOM ENDING"); + expectData("PROXY TCP4 1.2.3.4 2.3.4.5 100 100 RANDOM ENDING"); + disconnect(); + // Not tracked as v1 due to missing /r/n at end. + EXPECT_EQ(stats_store_.counter("proxy_proto.not_found_allowed").value(), 1); +} + +TEST_P(ProxyProtocolDisallowedVersionsWithNoProxyProtoTest, V2DisallowedAllowTinyNoProxyProtocol) { + // Allows a small request (less bytes than v1/v2 signature) through even though it doesn't use + // proxy protocol + envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol proto_config = + createConfig({ProxyProtocolConfig::V2}); // Essentially NOOP. + connect(true, &proto_config); + + std::string msg = "data"; + write(msg); + expectData(msg); + disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.not_found_allowed").value(), 1); +} + class WildcardProxyProtocolTest : public testing::TestWithParam, public Network::ListenerConfig, public Network::FilterChainManager, @@ -2130,7 +2495,7 @@ class WildcardProxyProtocolTest : public testing::TestWithParam(std::make_shared( - listenerScope(), + *stats_store_.rootScope(), envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol()))); return true; })); @@ -2148,7 +2513,9 @@ class WildcardProxyProtocolTest : public testing::TestWithParam runtime_; testing::NiceMock random_; - Stats::IsolatedStoreImpl stats_store_; + Stats::TestUtil::TestStore stats_store_; Api::ApiPtr api_; Event::DispatcherPtr dispatcher_; BasicResourceLimitImpl open_connections_; @@ -2260,6 +2627,7 @@ TEST_P(WildcardProxyProtocolTest, Basic) { EXPECT_TRUE(server_connection_->connectionInfoProvider().localAddressRestored()); disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v1.found").value(), 1); } TEST_P(WildcardProxyProtocolTest, BasicV6) { @@ -2275,6 +2643,7 @@ TEST_P(WildcardProxyProtocolTest, BasicV6) { EXPECT_TRUE(server_connection_->connectionInfoProvider().localAddressRestored()); disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v1.found").value(), 1); } TEST(ProxyProtocolConfigFactoryTest, TestCreateFactory) {