Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 7 additions & 0 deletions changelogs/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,13 @@ removed_config_or_runtime:
# *Normally occurs at the end of the* :ref:`deprecation period <deprecated>`

new_features:
- area: proxy_protocol
change: |
Added :ref:`value_format <envoy_v3_api_field_extensions.filters.listener.proxy_protocol.v3.ProxyProtocol.KeyValuePair.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
Expand Down
6 changes: 3 additions & 3 deletions contrib/golang/filters/http/test/test_data/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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 => ../../../../../../
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <envoy_v3_api_field_extensions.filters.listener.proxy_protocol.v3.ProxyProtocol.KeyValuePair.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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -606,9 +606,18 @@ bool Filter::parseTlvs(const uint8_t* buf, size_t len) {
absl::string_view tlv_value(reinterpret_cast<char const*>(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<const uint8_t*>(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) {
Expand All @@ -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 {
Expand Down Expand Up @@ -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});
Expand Down
Loading