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 b90d08dc05f46..d0bb5ffa32d28 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 @@ -32,12 +32,36 @@ message ProxyProtocol { FILTER_STATE = 1; } + // Controls how TLV values are formatted when stored as strings (in dynamic metadata or filter state). + // + // TLV payloads are arbitrary binary data and may contain bytes > 0x7F that are not valid UTF-8. + // Since protobuf ``string`` fields require valid UTF-8, the default ``RAW_STRING`` format sanitizes + // non-UTF-8 bytes (replacing them with ``0x21`` / ``!``), which silently corrupts binary payloads. + // + // Use ``HEX_STRING`` for binary TLV values (e.g., Google Cloud PSC ``pscConnectionId``) to get a + // lossless hex-encoded representation. + enum TlvValueFormat { + // Store the TLV value as a sanitized UTF-8 string. Bytes that are not valid UTF-8 are replaced + // with ``0x21`` (``!``). This is the legacy behavior and is appropriate for TLV values that are + // known to contain only ASCII/UTF-8 text. + RAW_STRING = 0; + + // Store the TLV value as a hex-encoded string (e.g., ``00afc7ee0ac80002``). This is safe for + // all binary payloads and preserves the original bytes without any corruption. + HEX_STRING = 1; + } + message KeyValuePair { // The namespace — if this is empty, the filter's namespace will be used. string metadata_namespace = 1; // The key to use within the namespace. string key = 2 [(validate.rules).string = {min_len: 1}]; + + // Controls how the TLV value is formatted when stored as a string. + // Defaults to ``RAW_STRING`` (legacy behavior). Use ``HEX_STRING`` for binary TLV payloads + // to avoid data corruption from UTF-8 sanitization. + TlvValueFormat value_format = 3; } // A Rule defines what metadata to apply when a header is present or missing. diff --git a/changelogs/current.yaml b/changelogs/current.yaml index e974da8a0f404..70a0d000126f8 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -273,6 +273,13 @@ removed_config_or_runtime: # *Normally occurs at the end of the* :ref:`deprecation period ` new_features: +- area: proxy_protocol + change: | + Added :ref:`value_format ` + option to the proxy protocol listener filter's ``on_tlv_present`` rule configuration. When set to ``HEX_STRING``, + TLV values are stored as hex-encoded strings instead of sanitized UTF-8, preventing silent data corruption for + binary TLV payloads (e.g., Google Cloud PSC ``pscConnectionId``). Defaults to ``RAW_STRING`` for full + backwards compatibility. - area: dynamic_modules change: | Added upstream HTTP TCP bridge extension for dynamic modules. This enables modules to transform diff --git a/contrib/golang/filters/http/test/test_data/go.mod b/contrib/golang/filters/http/test/test_data/go.mod index 8f90996861c06..86a02d9010951 100644 --- a/contrib/golang/filters/http/test/test_data/go.mod +++ b/contrib/golang/filters/http/test/test_data/go.mod @@ -10,9 +10,9 @@ require ( require ( cel.dev/expr v0.25.1 // indirect - github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect ) replace github.com/envoyproxy/envoy => ../../../../../../ diff --git a/contrib/golang/router/cluster_specifier/test/test_data/simple/go.mod b/contrib/golang/router/cluster_specifier/test/test_data/simple/go.mod index 21d45eb369785..414ea69d0da1b 100644 --- a/contrib/golang/router/cluster_specifier/test/test_data/simple/go.mod +++ b/contrib/golang/router/cluster_specifier/test/test_data/simple/go.mod @@ -10,7 +10,7 @@ require ( require ( cel.dev/expr v0.25.1 // indirect - github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect + github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect ) diff --git a/docs/root/configuration/listeners/listener_filters/proxy_protocol.rst b/docs/root/configuration/listeners/listener_filters/proxy_protocol.rst index 761f8dfaf5145..b906b20644396 100644 --- a/docs/root/configuration/listeners/listener_filters/proxy_protocol.rst +++ b/docs/root/configuration/listeners/listener_filters/proxy_protocol.rst @@ -66,6 +66,44 @@ The filter supports two storage locations for TLV values, controlled by the action: name: allow +TLV Value Format +---------------- + +TLV payloads in PROXY Protocol v2 are arbitrary binary data, not UTF-8 text. By default (``RAW_STRING``), +the filter sanitizes non-UTF-8 bytes by replacing them with ``0x21`` (``!``), which can silently corrupt +binary payloads. For binary TLV values, use the ``HEX_STRING`` format to get a lossless hex-encoded +representation. + +The format is controlled per-rule via the +:ref:`value_format ` field: + +**RAW_STRING** (default) + The TLV value is stored as a sanitized UTF-8 string. Non-UTF-8 bytes are replaced with ``0x21`` (``!``). + This is the legacy behavior and is appropriate for TLV values known to contain only ASCII/UTF-8 text + (e.g., authority TLV ``0x02``). + +**HEX_STRING** + The TLV value is stored as a lowercase hex-encoded string (e.g., ``00afc7ee0ac80002``). This is safe + for all binary payloads and preserves the original bytes without any corruption. + + Example: Google Cloud PSC sends ``pscConnectionId`` as an 8-byte big-endian uint64 in TLV type ``0xE0``. + With ``HEX_STRING``, a ``pscConnectionId`` of ``49477946121388034`` is stored as ``"00afc7ee0ac80002"``. + +.. code-block:: yaml + + listener_filters: + - name: envoy.filters.listener.proxy_protocol + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.listener.proxy_protocol.v3.ProxyProtocol + rules: + - tlv_type: 0xE0 + on_tlv_present: + key: "psc_conn_id" + value_format: HEX_STRING + - tlv_type: 0x02 + on_tlv_present: + key: "authority" + This implementation supports both version 1 and version 2, it automatically determines on a per-connection basis which of the two versions is present. diff --git a/source/extensions/filters/listener/proxy_protocol/proxy_protocol.cc b/source/extensions/filters/listener/proxy_protocol/proxy_protocol.cc index 72caa90b6cdb9..2aa20dbec10ca 100644 --- a/source/extensions/filters/listener/proxy_protocol/proxy_protocol.cc +++ b/source/extensions/filters/listener/proxy_protocol/proxy_protocol.cc @@ -21,7 +21,7 @@ #include "source/common/common/assert.h" #include "source/common/common/empty_string.h" #include "source/common/common/fmt.h" -#include "source/common/common/hex.h" +#include "source/common/common/hex.h" // IWYU pragma: keep (used for Hex::encode in HEX_STRING path) #include "source/common/common/safe_memcpy.h" #include "source/common/common/utility.h" #include "source/common/network/address_impl.h" @@ -606,9 +606,18 @@ bool Filter::parseTlvs(const uint8_t* buf, size_t len) { absl::string_view tlv_value(reinterpret_cast(buf + idx), tlv_value_length); auto key_value_pair = config_->isTlvTypeNeeded(tlv_type); if (nullptr != key_value_pair) { - // Sanitize any non utf8 characters. - auto sanitised_tlv_value = MessageUtil::sanitizeUtf8String(tlv_value); - std::string sanitised_value(sanitised_tlv_value.data(), sanitised_tlv_value.size()); + // Compute the formatted string value based on the configured value_format. + std::string formatted_value; + if (key_value_pair->value_format() == + envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol::HEX_STRING) { + // Hex-encode the raw TLV bytes — lossless for all binary payloads. + formatted_value = + Hex::encode(reinterpret_cast(tlv_value.data()), tlv_value.size()); + } else { + // Default RAW_STRING: sanitize non-UTF-8 characters (legacy behavior). + auto sanitised_tlv_value = MessageUtil::sanitizeUtf8String(tlv_value); + formatted_value.assign(sanitised_tlv_value.data(), sanitised_tlv_value.size()); + } if (config_->tlvLocation() == envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol::FILTER_STATE) { @@ -628,7 +637,7 @@ bool Filter::parseTlvs(const uint8_t* buf, size_t len) { StreamInfo::FilterState::LifeSpan::Connection); ENVOY_LOG(trace, "proxy_protocol: Created TLV FilterState object"); } - tlv_filter_state_obj->addTlvValue(key_value_pair->key(), sanitised_value); + tlv_filter_state_obj->addTlvValue(key_value_pair->key(), formatted_value); ENVOY_LOG(trace, "proxy_protocol: Stored TLV type {} value in FilterState with key {}", tlv_type, key_value_pair->key()); } else { @@ -656,7 +665,7 @@ bool Filter::parseTlvs(const uint8_t* buf, size_t len) { } // Always populate untyped metadata for backwards compatibility. Protobuf::Value metadata_value; - metadata_value.set_string_value(sanitised_value.data(), sanitised_value.size()); + metadata_value.set_string_value(formatted_value.data(), formatted_value.size()); Protobuf::Struct metadata( (*cb_->dynamicMetadata().mutable_filter_metadata())[metadata_key]); metadata.mutable_fields()->insert({key_value_pair->key(), metadata_value}); 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 0ccc03200d1ef..668ea89c16ac3 100644 --- a/test/extensions/filters/listener/proxy_protocol/proxy_protocol_test.cc +++ b/test/extensions/filters/listener/proxy_protocol/proxy_protocol_test.cc @@ -2241,6 +2241,254 @@ TEST_P(ProxyProtocolTest, V2ExtractTLVToFilterStateSerializeMethods) { EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.found").value(), 1); } +// --- HEX_STRING value_format tests --- + +TEST_P(ProxyProtocolTest, V2TlvBinaryValuePreservedWithHexString) { + // Simulates GCP PSC pscConnectionId: 8-byte big-endian uint64 in TLV type 0xE0. + // pscConnectionId = 49477946121388034 = 0x00afc7ee0ac80002 + // Bytes 0xaf, 0xc7, 0xee, 0xc8 are all > 0x7F and would be corrupted by UTF-8 sanitization. + // PP2 header: ipv4/tcp, 12 bytes addr + 11 bytes TLV extension = 23 bytes payload + constexpr uint8_t buffer[] = {0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49, + 0x54, 0x0a, 0x21, 0x11, 0x00, 0x17, 0x01, 0x02, 0x03, 0x04, + 0x00, 0x01, 0x01, 0x02, 0x03, 0x05, 0x00, 0x02}; + // TLV type 0xE0, length 8, value = 0x00afc7ee0ac80002 + constexpr uint8_t tlv_gcp[] = {0xe0, 0x00, 0x08, 0x00, 0xaf, 0xc7, + 0xee, 0x0a, 0xc8, 0x00, 0x02}; + constexpr uint8_t data[] = {'D', 'A', 'T', 'A'}; + + envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol proto_config; + auto rule = proto_config.add_rules(); + rule->set_tlv_type(0xE0); + rule->mutable_on_tlv_present()->set_key("psc_conn_id"); + rule->mutable_on_tlv_present()->set_value_format( + envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol::HEX_STRING); + + connect(true, &proto_config); + write(buffer, sizeof(buffer)); + dispatcher_->run(Event::Dispatcher::RunType::NonBlock); + + write(tlv_gcp, sizeof(tlv_gcp)); + write(data, sizeof(data)); + expectData("DATA"); + + // Verify dynamic metadata contains hex-encoded value, not corrupted bytes. + EXPECT_EQ(1, server_connection_->streamInfo().dynamicMetadata().filter_metadata_size()); + auto metadata = server_connection_->streamInfo().dynamicMetadata().filter_metadata(); + EXPECT_EQ(1, metadata.count("envoy.filters.listener.proxy_protocol")); + auto fields = metadata.at("envoy.filters.listener.proxy_protocol").fields(); + EXPECT_EQ(1, fields.size()); + EXPECT_EQ("00afc7ee0ac80002", fields.at("psc_conn_id").string_value()); + + // Typed metadata should still contain the raw bytes (unchanged behavior). + auto typed_metadata = server_connection_->streamInfo().dynamicMetadata().typed_filter_metadata(); + EXPECT_EQ(1, typed_metadata.count("envoy.filters.listener.proxy_protocol")); + envoy::data::core::v3::TlvsMetadata tlvs_metadata; + auto status = MessageUtil::unpackTo( + typed_metadata["envoy.filters.listener.proxy_protocol"], tlvs_metadata); + EXPECT_EQ(absl::OkStatus(), status); + auto raw_bytes = (tlvs_metadata.typed_metadata()).at("psc_conn_id"); + ASSERT_THAT(raw_bytes, ElementsAre(0x00, 0xaf, 0xc7, 0xee, 0x0a, 0xc8, 0x00, 0x02)); + + disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.found").value(), 1); +} + +TEST_P(ProxyProtocolTest, V2TlvAsciiValueBothFormats) { + // TLV value = "foo.com" (all bytes < 0x80) — test that both RAW_STRING and HEX_STRING + // produce expected output for pure ASCII values. + // PP2 header: ipv4/tcp, 12 bytes addr + 14+10 = 24 bytes TLV extensions = 36 bytes payload + constexpr uint8_t buffer[] = {0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49, + 0x54, 0x0a, 0x21, 0x11, 0x00, 0x24, 0x01, 0x02, 0x03, 0x04, + 0x00, 0x01, 0x01, 0x02, 0x03, 0x05, 0x00, 0x02}; + // TLV type 0x02, length 7, value = "foo.com" + constexpr uint8_t tlv_authority[] = {0x02, 0x00, 0x07, 0x66, 0x6f, + 0x6f, 0x2e, 0x63, 0x6f, 0x6d}; + // TLV type 0x0f, length 7, value = "foo.com" (same bytes, different type) + constexpr uint8_t tlv_other[] = {0x0f, 0x00, 0x07, 0x66, 0x6f, + 0x6f, 0x2e, 0x63, 0x6f, 0x6d}; + // Padding TLV of type 0x00, length 1 to fill extension space + constexpr uint8_t tlv_pad[] = {0x00, 0x00, 0x01, 0xff}; + constexpr uint8_t data[] = {'D', 'A', 'T', 'A'}; + + envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol proto_config; + // Rule 1: RAW_STRING (default) + auto rule1 = proto_config.add_rules(); + rule1->set_tlv_type(0x02); + rule1->mutable_on_tlv_present()->set_key("authority_raw"); + // value_format defaults to RAW_STRING + + // Rule 2: HEX_STRING + auto rule2 = proto_config.add_rules(); + rule2->set_tlv_type(0x0f); + rule2->mutable_on_tlv_present()->set_key("authority_hex"); + rule2->mutable_on_tlv_present()->set_value_format( + envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol::HEX_STRING); + + connect(true, &proto_config); + write(buffer, sizeof(buffer)); + dispatcher_->run(Event::Dispatcher::RunType::NonBlock); + + write(tlv_pad, sizeof(tlv_pad)); + write(tlv_authority, sizeof(tlv_authority)); + write(tlv_other, sizeof(tlv_other)); + write(data, sizeof(data)); + expectData("DATA"); + + auto metadata = server_connection_->streamInfo().dynamicMetadata().filter_metadata(); + auto fields = metadata.at("envoy.filters.listener.proxy_protocol").fields(); + EXPECT_EQ(2, fields.size()); + // RAW_STRING: ASCII value is stored as-is. + EXPECT_EQ("foo.com", fields.at("authority_raw").string_value()); + // HEX_STRING: ASCII value is hex-encoded. + EXPECT_EQ("666f6f2e636f6d", fields.at("authority_hex").string_value()); + + disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.found").value(), 1); +} + +TEST_P(ProxyProtocolTest, V2TlvDefaultFormatIsRawStringBackwardsCompat) { + // Ensure that when value_format is not set, behavior is identical to the legacy RAW_STRING. + // Uses non-UTF-8 bytes to verify sanitization still happens. + constexpr uint8_t buffer[] = {0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49, + 0x54, 0x0a, 0x21, 0x11, 0x00, 0x17, 0x01, 0x02, 0x03, 0x04, + 0x00, 0x01, 0x01, 0x02, 0x03, 0x05, 0x00, 0x02}; + // TLV type 0xE0, length 8, value = 0x00afc7ee0ac80002 (contains bytes > 0x7F) + constexpr uint8_t tlv_gcp[] = {0xe0, 0x00, 0x08, 0x00, 0xaf, 0xc7, + 0xee, 0x0a, 0xc8, 0x00, 0x02}; + constexpr uint8_t data[] = {'D', 'A', 'T', 'A'}; + + envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol proto_config; + auto rule = proto_config.add_rules(); + rule->set_tlv_type(0xE0); + rule->mutable_on_tlv_present()->set_key("psc_conn_id"); + // Do NOT set value_format — should default to RAW_STRING + + connect(true, &proto_config); + write(buffer, sizeof(buffer)); + dispatcher_->run(Event::Dispatcher::RunType::NonBlock); + + write(tlv_gcp, sizeof(tlv_gcp)); + write(data, sizeof(data)); + expectData("DATA"); + + // Verify that with default RAW_STRING, non-UTF-8 bytes are still sanitized (replaced with 0x21). + auto metadata = server_connection_->streamInfo().dynamicMetadata().filter_metadata(); + auto fields = metadata.at("envoy.filters.listener.proxy_protocol").fields(); + auto value = fields.at("psc_conn_id").string_value(); + const char replacement = 0x21; + // 0x00 stays, 0xaf->!, 0xc7+0xee forms invalid UTF-8 sequence->!, 0x0a stays, + // 0xc8+0x00 forms invalid->!, 0x02 stays. + // The exact sanitization depends on MessageUtil::sanitizeUtf8String logic. + // Key assertion: the value should NOT be the correct hex "00afc7ee0ac80002". + EXPECT_NE("00afc7ee0ac80002", value); + // Verify at least one byte was replaced (the value contains '!' from sanitization). + EXPECT_TRUE(value.find(replacement) != std::string::npos); + + disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.found").value(), 1); +} + +TEST_P(ProxyProtocolTest, V2TlvHexStringInFilterState) { + // Verify HEX_STRING works correctly when tlv_location = FILTER_STATE. + constexpr uint8_t buffer[] = {0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49, + 0x54, 0x0a, 0x21, 0x11, 0x00, 0x17, 0x01, 0x02, 0x03, 0x04, + 0x00, 0x01, 0x01, 0x02, 0x03, 0x05, 0x00, 0x02}; + // TLV type 0xE0, length 8, value = 0x00afc7ee0ac80002 + constexpr uint8_t tlv_gcp[] = {0xe0, 0x00, 0x08, 0x00, 0xaf, 0xc7, + 0xee, 0x0a, 0xc8, 0x00, 0x02}; + constexpr uint8_t data[] = {'D', 'A', 'T', 'A'}; + + envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol proto_config; + proto_config.set_tlv_location( + envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol::FILTER_STATE); + auto rule = proto_config.add_rules(); + rule->set_tlv_type(0xE0); + rule->mutable_on_tlv_present()->set_key("psc_conn_id"); + rule->mutable_on_tlv_present()->set_value_format( + envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol::HEX_STRING); + + connect(true, &proto_config); + write(buffer, sizeof(buffer)); + dispatcher_->run(Event::Dispatcher::RunType::NonBlock); + + write(tlv_gcp, sizeof(tlv_gcp)); + write(data, sizeof(data)); + expectData("DATA"); + + // Verify filter state contains hex-encoded value. + auto& filter_state = server_connection_->streamInfo().filterState(); + constexpr absl::string_view kFilterStateKey = "envoy.network.proxy_protocol.tlv"; + EXPECT_TRUE(filter_state->hasDataWithName(kFilterStateKey)); + const auto* tlv_obj = filter_state->getDataReadOnlyGeneric(kFilterStateKey); + ASSERT_NE(nullptr, tlv_obj); + + auto field = tlv_obj->getField("psc_conn_id"); + ASSERT_TRUE(absl::holds_alternative(field)); + EXPECT_EQ("00afc7ee0ac80002", absl::get(field)); + + // Verify dynamic metadata is NOT populated when FILTER_STATE is used. + EXPECT_EQ(0, server_connection_->streamInfo().dynamicMetadata().filter_metadata_size()); + + disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.found").value(), 1); +} + +TEST_P(ProxyProtocolTest, V2TlvHexStringEdgeCases) { + // Edge cases: all-zero bytes, all-0xFF bytes, and empty TLV. + // PP2 header: ipv4/tcp, 12 bytes addr + (11+11+3) = 37 bytes TLV extensions + constexpr uint8_t buffer[] = {0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49, + 0x54, 0x0a, 0x21, 0x11, 0x00, 0x25, 0x01, 0x02, 0x03, 0x04, + 0x00, 0x01, 0x01, 0x02, 0x03, 0x05, 0x00, 0x02}; + // TLV type 0xE0, length 8, value = all zeros + constexpr uint8_t tlv_zeros[] = {0xe0, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00}; + // TLV type 0xE1, length 8, value = all 0xFF + constexpr uint8_t tlv_ffs[] = {0xe1, 0x00, 0x08, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff}; + // TLV type 0xE2, length 0, value = empty + constexpr uint8_t tlv_empty[] = {0xe2, 0x00, 0x00}; + constexpr uint8_t data[] = {'D', 'A', 'T', 'A'}; + + envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol proto_config; + auto rule1 = proto_config.add_rules(); + rule1->set_tlv_type(0xE0); + rule1->mutable_on_tlv_present()->set_key("all_zeros"); + rule1->mutable_on_tlv_present()->set_value_format( + envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol::HEX_STRING); + + auto rule2 = proto_config.add_rules(); + rule2->set_tlv_type(0xE1); + rule2->mutable_on_tlv_present()->set_key("all_ffs"); + rule2->mutable_on_tlv_present()->set_value_format( + envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol::HEX_STRING); + + auto rule3 = proto_config.add_rules(); + rule3->set_tlv_type(0xE2); + rule3->mutable_on_tlv_present()->set_key("empty_val"); + rule3->mutable_on_tlv_present()->set_value_format( + envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol::HEX_STRING); + + connect(true, &proto_config); + write(buffer, sizeof(buffer)); + dispatcher_->run(Event::Dispatcher::RunType::NonBlock); + + write(tlv_zeros, sizeof(tlv_zeros)); + write(tlv_ffs, sizeof(tlv_ffs)); + write(tlv_empty, sizeof(tlv_empty)); + write(data, sizeof(data)); + expectData("DATA"); + + auto metadata = server_connection_->streamInfo().dynamicMetadata().filter_metadata(); + auto fields = metadata.at("envoy.filters.listener.proxy_protocol").fields(); + EXPECT_EQ(3, fields.size()); + EXPECT_EQ("0000000000000000", fields.at("all_zeros").string_value()); + EXPECT_EQ("ffffffffffffffff", fields.at("all_ffs").string_value()); + EXPECT_EQ("", fields.at("empty_val").string_value()); + + disconnect(); + EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.found").value(), 1); +} + TEST_P(ProxyProtocolTest, MalformedProxyLine) { connect(false);