Skip to content

Commit 1aa745a

Browse files
authored
proxy protocol: add new option to set encoding of TLV value (#45391)
Commit Message: proxy protocol: add new option to set encoding of TLV value Additional Description: Allow to set value encode for specific TLV value. To close #45366 Risk Level: low. Testing: unit. Docs Changes: n/a. Release Notes: added. Platform Specific Features: n/a. --------- Signed-off-by: wbpcode/wangbaiping <wbphub@gmail.com> Signed-off-by: code <wbphub@gmail.com>
1 parent 8d72a23 commit 1aa745a

7 files changed

Lines changed: 209 additions & 3 deletions

File tree

api/envoy/extensions/filters/listener/proxy_protocol/v3/proxy_protocol.proto

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,38 @@ message ProxyProtocol {
3333
}
3434

3535
message KeyValuePair {
36+
// Specifies the encoding scheme that is used to encode the TLV value before it is
37+
// stored in dynamic metadata or filter state.
38+
enum ValueStringEncoding {
39+
// Unspecified encoding scheme. Defaults to ``SANITIZED_UTF8``.
40+
UNSPECIFIED = 0;
41+
42+
// The TLV value will be sanitized to a valid UTF-8 string before being stored:
43+
// any invalid UTF-8 sequences will be replaced with the ``!`` character.
44+
SANITIZED_UTF8 = 1;
45+
46+
// The raw TLV value will be encoded as a `Base64 <https://datatracker.ietf.org/doc/html/rfc4648#section-4>`_
47+
// string (with padding) before being stored. This is useful for binary TLV values that
48+
// are not valid UTF-8 strings.
49+
BASE64 = 2;
50+
}
51+
3652
// The namespace — if this is empty, the filter's namespace will be used.
3753
string metadata_namespace = 1;
3854

3955
// The key to use within the namespace.
4056
string key = 2 [(validate.rules).string = {min_len: 1}];
57+
58+
// The value encoding scheme that is used to encode the TLV value before it is stored in
59+
// dynamic metadata or filter state. If not set, defaults to ``SANITIZED_UTF8``, which
60+
// sanitizes the TLV value to a valid UTF-8 string.
61+
//
62+
// .. note::
63+
//
64+
// This option only applies to the legacy untyped dynamic metadata and filter state.
65+
// For the new typed dynamic metadata, the raw TLV value bytes are stored as is and
66+
// no encoding is applied.
67+
ValueStringEncoding value_string_encoding = 3;
4168
}
4269

4370
// A Rule defines what metadata to apply when a header is present or missing.

changelogs/changelogs.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,5 @@ areas:
110110
title: wasm
111111
dns:
112112
title: dns
113+
proxy_protocol:
114+
title: proxy_protocol
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Added :ref:`encoding
2+
<envoy_v3_api_field_extensions.filters.listener.proxy_protocol.v3.ProxyProtocol.KeyValuePair.value_string_encoding>` option to
3+
control how a TLV value is encoded before it is stored in dynamic metadata or filter state. By default the TLV
4+
value is sanitized to a valid UTF-8 string (previous behavior). Setting it to ``BASE64`` stores the raw TLV value
5+
as a base64-encoded string instead.

docs/root/configuration/listeners/listener_filters/proxy_protocol.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,29 @@ The filter supports two storage locations for TLV values, controlled by the
6666
action:
6767
name: allow
6868
69+
TLV Value Encoding
70+
------------------
71+
72+
By default, TLV values are sanitized to valid UTF-8 strings before being stored in
73+
dynamic metadata or filter state: any invalid UTF-8 sequences are replaced with the
74+
``!`` character. For binary TLV values, the
75+
:ref:`value_string_encoding <envoy_v3_api_field_extensions.filters.listener.proxy_protocol.v3.ProxyProtocol.KeyValuePair.value_string_encoding>`
76+
option can be set to ``BASE64`` to store the raw TLV value as a base64-encoded string instead.
77+
Note that this option only applies to the legacy untyped dynamic metadata and filter state;
78+
the typed dynamic metadata always stores the raw TLV value bytes as is:
79+
80+
.. code-block:: yaml
81+
82+
listener_filters:
83+
- name: envoy.filters.listener.proxy_protocol
84+
typed_config:
85+
"@type": type.googleapis.com/envoy.extensions.filters.listener.proxy_protocol.v3.ProxyProtocol
86+
rules:
87+
- tlv_type: 0xEA
88+
on_tlv_present:
89+
key: "aws_vpce_id"
90+
value_string_encoding: BASE64
91+
6992
This implementation supports both version 1 and version 2, it
7093
automatically determines on a per-connection basis which of the two
7194
versions is present.

source/extensions/filters/listener/proxy_protocol/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ envoy_cc_library(
2626
"//source/common/api:os_sys_calls_lib",
2727
"//source/common/buffer:buffer_lib",
2828
"//source/common/common:assert_lib",
29+
"//source/common/common:base64_lib",
2930
"//source/common/common:empty_string",
3031
"//source/common/common:hex_lib",
3132
"//source/common/common:minimal_logger_lib",

source/extensions/filters/listener/proxy_protocol/proxy_protocol.cc

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
#include "source/common/api/os_sys_calls_impl.h"
2121
#include "source/common/common/assert.h"
22+
#include "source/common/common/base64.h"
2223
#include "source/common/common/empty_string.h"
2324
#include "source/common/common/fmt.h"
2425
#include "source/common/common/hex.h"
@@ -604,9 +605,16 @@ bool Filter::parseTlvs(const uint8_t* buf, size_t len) {
604605
absl::string_view tlv_value(reinterpret_cast<char const*>(buf + idx), tlv_value_length);
605606
auto key_value_pair = config_->isTlvTypeNeeded(tlv_type);
606607
if (nullptr != key_value_pair) {
607-
// Sanitize any non utf8 characters.
608-
auto sanitised_tlv_value = MessageUtil::sanitizeUtf8String(tlv_value);
609-
std::string sanitised_value(sanitised_tlv_value.data(), sanitised_tlv_value.size());
608+
// Encode the TLV value as configured.
609+
std::string sanitised_value;
610+
if (key_value_pair->value_string_encoding() ==
611+
envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol::KeyValuePair::
612+
BASE64) {
613+
sanitised_value = Base64::encode(tlv_value.data(), tlv_value.size());
614+
} else {
615+
// Default sanitization is to replace non-UTF-8 characters with `!` character.
616+
sanitised_value = MessageUtil::sanitizeUtf8String(tlv_value);
617+
}
610618

611619
if (config_->tlvLocation() ==
612620
envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol::FILTER_STATE) {

test/extensions/filters/listener/proxy_protocol/proxy_protocol_test.cc

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1646,6 +1646,75 @@ TEST_P(ProxyProtocolTest, V2ExtractMultipleTlvsOfInterestAndSanitiseNonUtf8) {
16461646
EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.found").value(), 1);
16471647
}
16481648

1649+
TEST_P(ProxyProtocolTest, V2ExtractMultipleTlvsOfInterestAndEncodeAsBase64) {
1650+
// A well-formed ipv4/tcp with a pair of TLV extensions is accepted.
1651+
constexpr uint8_t buffer[] = {0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49,
1652+
0x54, 0x0a, 0x21, 0x11, 0x00, 0x39, 0x01, 0x02, 0x03, 0x04,
1653+
0x00, 0x01, 0x01, 0x02, 0x03, 0x05, 0x00, 0x02};
1654+
// A TLV of type 0x00 with size of 4 (1 byte is value).
1655+
constexpr uint8_t tlv1[] = {0x00, 0x00, 0x01, 0xff};
1656+
// A TLV of type 0x02 with size of 10 bytes (7 bytes are value). Second and last bytes in the
1657+
// value are non utf8 characters.
1658+
constexpr uint8_t tlv_type_authority[] = {0x02, 0x00, 0x07, 0x66, 0xfe,
1659+
0x6f, 0x2e, 0x63, 0x6f, 0xc1};
1660+
// A TLV of type 0x0f with size of 6 bytes (3 bytes are value).
1661+
constexpr uint8_t tlv3[] = {0x0f, 0x00, 0x03, 0xf0, 0x00, 0x0f};
1662+
// A TLV of type 0xea with size of 25 bytes (22 bytes are value). 7th and 21st bytes are non utf8
1663+
// characters.
1664+
constexpr uint8_t tlv_vpc_id[] = {0xea, 0x00, 0x16, 0x01, 0x76, 0x70, 0x63, 0x2d, 0x30,
1665+
0xc0, 0x35, 0x74, 0x65, 0x73, 0x74, 0x32, 0x66, 0x61,
1666+
0x36, 0x63, 0x36, 0x33, 0x68, 0xf9, 0x37};
1667+
constexpr uint8_t data[] = {'D', 'A', 'T', 'A'};
1668+
1669+
envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol proto_config;
1670+
auto rule_type_authority = proto_config.add_rules();
1671+
rule_type_authority->set_tlv_type(0x02);
1672+
rule_type_authority->mutable_on_tlv_present()->set_key("PP2 type authority");
1673+
rule_type_authority->mutable_on_tlv_present()->set_value_string_encoding(
1674+
envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol::KeyValuePair::
1675+
BASE64);
1676+
1677+
auto rule_vpc_id = proto_config.add_rules();
1678+
rule_vpc_id->set_tlv_type(0xea);
1679+
rule_vpc_id->mutable_on_tlv_present()->set_key("PP2 vpc id");
1680+
rule_vpc_id->mutable_on_tlv_present()->set_value_string_encoding(
1681+
envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol::KeyValuePair::
1682+
BASE64);
1683+
1684+
// A rule with default (SANITIZED_UTF8) encoding for comparison: the value will be sanitized
1685+
// to a valid UTF-8 string.
1686+
auto rule_tlv1 = proto_config.add_rules();
1687+
rule_tlv1->set_tlv_type(0x00);
1688+
rule_tlv1->mutable_on_tlv_present()->set_key("PP2 tlv1");
1689+
1690+
connect(true, &proto_config);
1691+
write(buffer, sizeof(buffer));
1692+
dispatcher_->run(Event::Dispatcher::RunType::NonBlock);
1693+
1694+
write(tlv1, sizeof(tlv1));
1695+
write(tlv_type_authority, sizeof(tlv_type_authority));
1696+
write(tlv3, sizeof(tlv3));
1697+
write(tlv_vpc_id, sizeof(tlv_vpc_id));
1698+
write(data, sizeof(data));
1699+
expectData("DATA");
1700+
1701+
auto metadata = server_connection_->streamInfo().dynamicMetadata().filter_metadata();
1702+
EXPECT_EQ(1, metadata.size());
1703+
EXPECT_EQ(1, metadata.count(ProxyProtocol));
1704+
1705+
auto fields = metadata.at(ProxyProtocol).fields();
1706+
EXPECT_EQ(3, fields.size());
1707+
1708+
// The raw TLV values (including the non utf8 characters) are encoded as base64.
1709+
EXPECT_EQ("Zv5vLmNvwQ==", fields.at("PP2 type authority").string_value());
1710+
EXPECT_EQ("AXZwYy0wwDV0ZXN0MmZhNmM2M2j5Nw==", fields.at("PP2 vpc id").string_value());
1711+
// The default encoding sanitizes the value to a valid UTF-8 string: the non utf8 byte
1712+
// 0xff is replaced with the `!` character.
1713+
EXPECT_EQ("!", fields.at("PP2 tlv1").string_value());
1714+
disconnect();
1715+
EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.found").value(), 1);
1716+
}
1717+
16491718
TEST_P(ProxyProtocolTest, V2ExtractMultipleTlvsOfInterestAndEmitTypedAndUntypedMetadata) {
16501719
// A well-formed ipv4/tcp with a pair of TLV extensions is accepted
16511720
constexpr uint8_t buffer[] = {0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49,
@@ -2097,6 +2166,77 @@ TEST_P(ProxyProtocolTest, V2ExtractTLVToFilterStateAsStringAccessor) {
20972166
EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.found").value(), 1);
20982167
}
20992168

2169+
TEST_P(ProxyProtocolTest, V2ExtractTLVToFilterStateWithBase64Encoding) {
2170+
constexpr uint8_t buffer[] = {0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49,
2171+
0x54, 0x0a, 0x21, 0x11, 0x00, 0x27, 0x01, 0x02, 0x03, 0x04,
2172+
0x00, 0x01, 0x01, 0x02, 0x03, 0x05, 0x00, 0x02};
2173+
constexpr uint8_t tlv1[] = {0x0, 0x0, 0x1, 0xff};
2174+
constexpr uint8_t tlv_type_authority[] = {0x02, 0x00, 0x07, 0x66, 0x6f,
2175+
0x6f, 0x2e, 0x63, 0x6f, 0x6d};
2176+
constexpr uint8_t tlv_vpce[] = {0xea, 0x00, 0x0a, 0x21, 0x76, 0x70, 0x63,
2177+
0x65, 0x2d, 0x30, 0x78, 0x78, 0x78};
2178+
constexpr uint8_t data[] = {'D', 'A', 'T', 'A'};
2179+
2180+
envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol proto_config;
2181+
proto_config.set_tlv_location(
2182+
envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol::FILTER_STATE);
2183+
auto rule1 = proto_config.add_rules();
2184+
rule1->set_tlv_type(0x02);
2185+
rule1->mutable_on_tlv_present()->set_key("PP2 type authority");
2186+
rule1->mutable_on_tlv_present()->set_value_string_encoding(
2187+
envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol::KeyValuePair::
2188+
BASE64);
2189+
auto rule2 = proto_config.add_rules();
2190+
rule2->set_tlv_type(0xea);
2191+
rule2->mutable_on_tlv_present()->set_key("aws_vpce_id");
2192+
rule2->mutable_on_tlv_present()->set_value_string_encoding(
2193+
envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol::KeyValuePair::
2194+
BASE64);
2195+
// A rule with default (SANITIZED_UTF8) encoding for comparison: the value will be sanitized
2196+
// to a valid UTF-8 string.
2197+
auto rule3 = proto_config.add_rules();
2198+
rule3->set_tlv_type(0x00);
2199+
rule3->mutable_on_tlv_present()->set_key("PP2 tlv1");
2200+
rule3->mutable_on_tlv_present()->set_value_string_encoding(
2201+
envoy::extensions::filters::listener::proxy_protocol::v3::ProxyProtocol::KeyValuePair::
2202+
SANITIZED_UTF8);
2203+
2204+
connect(true, &proto_config);
2205+
write(buffer, sizeof(buffer));
2206+
dispatcher_->run(Event::Dispatcher::RunType::NonBlock);
2207+
2208+
write(tlv1, sizeof(tlv1));
2209+
write(tlv_type_authority, sizeof(tlv_type_authority));
2210+
write(tlv_vpce, sizeof(tlv_vpce));
2211+
write(data, sizeof(data));
2212+
expectData("DATA");
2213+
2214+
auto& filter_state = server_connection_->streamInfo().filterState();
2215+
2216+
constexpr absl::string_view kFilterStateKey = "envoy.network.proxy_protocol.tlv";
2217+
EXPECT_TRUE(filter_state->hasDataWithName(kFilterStateKey));
2218+
const auto* tlv_obj = filter_state->getDataReadOnlyGeneric(kFilterStateKey);
2219+
ASSERT_NE(nullptr, tlv_obj);
2220+
2221+
// The raw TLV values are encoded as base64.
2222+
auto field1 = tlv_obj->getField("PP2 type authority");
2223+
ASSERT_TRUE(absl::holds_alternative<absl::string_view>(field1));
2224+
EXPECT_EQ("Zm9vLmNvbQ==", absl::get<absl::string_view>(field1));
2225+
2226+
auto field2 = tlv_obj->getField("aws_vpce_id");
2227+
ASSERT_TRUE(absl::holds_alternative<absl::string_view>(field2));
2228+
EXPECT_EQ("IXZwY2UtMHh4eA==", absl::get<absl::string_view>(field2));
2229+
2230+
// The default encoding sanitizes the value to a valid UTF-8 string: the non utf8 byte
2231+
// 0xff is replaced with the `!` character.
2232+
auto field3 = tlv_obj->getField("PP2 tlv1");
2233+
ASSERT_TRUE(absl::holds_alternative<absl::string_view>(field3));
2234+
EXPECT_EQ("!", absl::get<absl::string_view>(field3));
2235+
2236+
disconnect();
2237+
EXPECT_EQ(stats_store_.counter("proxy_proto.versions.v2.found").value(), 1);
2238+
}
2239+
21002240
TEST_P(ProxyProtocolTest, V2ExtractTLVToFilterStateDefaultBehavior) {
21012241
// Test that default behavior (DYNAMIC_METADATA) still works when tlv_location is not set
21022242
constexpr uint8_t buffer[] = {0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49,

0 commit comments

Comments
 (0)