Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

upstream_proxy_protocol: Introduce custom TLV support #37591

Open
wants to merge 28 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
09d7ce1
Introduce custom TLV support for upstream PP2 headers
timflannagan Nov 15, 2024
d0a3b62
pp2: Add custom TLV entries field to protocol protocol config API
timflannagan Dec 11, 2024
abc5088
pp2: Silence linting violation
timflannagan Dec 11, 2024
bdc38a9
Merge remote-tracking branch 'upstream/main' into feat/custom-pp2-ups…
timflannagan Dec 11, 2024
6d94e1d
pp2: Add additional unit tests
timflannagan Dec 13, 2024
2406b71
test: Fix linting violations
timflannagan Dec 13, 2024
9dbb9ff
Update changelog entry
timflannagan Dec 16, 2024
37f6be1
Merge remote-tracking branch 'upstream/main' into feat/custom-pp2-ups…
timflannagan Dec 16, 2024
9c5ac97
Merge remote-tracking branch 'upstream/main' into feat/custom-pp2-ups…
timflannagan Dec 17, 2024
da65ca7
Merge remote-tracking branch 'upstream/main' into feat/custom-pp2-ups…
timflannagan Jan 3, 2025
b656b36
*: Rename entries -> custom_tlvs in proxy_protocol.proto
timflannagan Jan 3, 2025
d23df4f
Fix changelog format errors and expand on precendence behavior in pro…
timflannagan Jan 7, 2025
1c02dc1
*: Rename custom_tlvs -> added_tlvs in proxy_protocol.proto
timflannagan Jan 7, 2025
e50c959
api: Revisit added_tlvs proto field documentation to clarify behavior
timflannagan Jan 7, 2025
f1ed670
source,test: Refactor added_tlvs code
timflannagan Jan 8, 2025
5ec9dd8
api: Attempt to fix docs sanity check
timflannagan Jan 8, 2025
118792b
api: Fix docs precheck
timflannagan Jan 8, 2025
c5a5619
pp2: Add ASSERT, move duplicate test case, remove TODOs
timflannagan Jan 8, 2025
7e5454a
pp2: Consolidate tests to add explicit precedence & override cases
timflannagan Jan 8, 2025
ad46d23
pp2: Refactor code, address review comments, etc
timflannagan Jan 16, 2025
3122ab3
Merge remote-tracking branch 'upstream/main' into feat/custom-pp2-ups…
timflannagan Jan 16, 2025
a6de035
Fix format errors
timflannagan Jan 22, 2025
c1e9f63
Merge remote-tracking branch 'upstream/main' into feat/custom-pp2-ups…
timflannagan Jan 22, 2025
b343e67
api: Fix YAML identation in PP2 code block
timflannagan Jan 22, 2025
7c5d4ed
api: Use literalinclude for added_tlvs code example
timflannagan Jan 22, 2025
876af00
configs: Fix proxy_protocol.yaml format errors
timflannagan Jan 22, 2025
977bb4b
Merge remote-tracking branch 'upstream/main' into feat/custom-pp2-ups…
timflannagan Jan 24, 2025
5ebdb87
Fix invalid proxy_protocol.yaml config
timflannagan Jan 24, 2025
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
13 changes: 13 additions & 0 deletions api/envoy/config/core/v3/proxy_protocol.proto
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ message ProxyProtocolPassThroughTLVs {
repeated uint32 tlv_type = 2 [(validate.rules).repeated = {items {uint32 {lt: 256}}}];
}

// Represents a single Type-Length-Value (TLV) entry.
message TlvEntry {
// The type of the TLV. Must be a uint8 (0-255) as per the Proxy Protocol v2 specification.
uint32 type = 1 [(validate.rules).uint32 = {lt: 256}];

// The value of the TLV. Must be at least one byte long.
bytes value = 2 [(validate.rules).bytes = {min_len: 1}];
}

message ProxyProtocolConfig {
enum Version {
// PROXY protocol version 1. Human readable format.
Expand All @@ -47,4 +56,8 @@ message ProxyProtocolConfig {
// This config controls which TLVs can be passed to upstream if it is Proxy Protocol
// V2 header. If there is no setting for this field, no TLVs will be passed through.
ProxyProtocolPassThroughTLVs pass_through_tlvs = 2;

// Custom TLV metadata to be sent in the upstream PROXY protocol header. This field
// is only valid when the version is V2.
repeated TlvEntry entries = 3;
timflannagan marked this conversation as resolved.
Show resolved Hide resolved
}
5 changes: 5 additions & 0 deletions changelogs/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,11 @@ new_features:
change: |
Added new health check filter stats including total requests, successful/failed checks, cached responses, and
cluster health status counters. These stats help track health check behavior and cluster health state.
- area: proxy_protocol
timflannagan marked this conversation as resolved.
Show resolved Hide resolved
change: |
Added support for injecting custom TLVs into the Proxy Protocol v2 (PP2) header for upstream transport sockets.
This feature allows dynamic customization of PP2 headersby defining TLV key-value pairs in an endpoint host's
typed metadata under the ``envoy.transport_sockets.proxy_protocol`` namespace.

deprecated:
- area: rbac
Expand Down
3 changes: 3 additions & 0 deletions source/common/config/well_known_names.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ class MetadataFilterValues {
const std::string ENVOY_LB = "envoy.lb";
// Filter namespace for built-in transport socket match in cluster.
const std::string ENVOY_TRANSPORT_SOCKET_MATCH = "envoy.transport_socket_match";
// Filter namespace for storing custom upstream PP TLVs in metadata.
const std::string ENVOY_TRANSPORT_SOCKETS_PROXY_PROTOCOL =
"envoy.transport_sockets.proxy_protocol";
// Proxy address configuration namespace for HTTP/1.1 proxy transport sockets.
const std::string ENVOY_HTTP11_PROXY_TRANSPORT_SOCKET_ADDR =
"envoy.http11_proxy_transport_socket.proxy_address";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,36 @@ void generateV2Header(const Network::Address::Ip& source_address,
}

bool generateV2Header(const Network::ProxyProtocolData& proxy_proto_data, Buffer::Instance& out,
bool pass_all_tlvs, const absl::flat_hash_set<uint8_t>& pass_through_tlvs) {
uint64_t extension_length = 0;
for (auto&& tlv : proxy_proto_data.tlv_vector_) {
bool pass_all_tlvs, const absl::flat_hash_set<uint8_t>& pass_through_tlvs,
const std::vector<Envoy::Network::ProxyProtocolTLV>& custom_tlvs) {
std::vector<Envoy::Network::ProxyProtocolTLV> combined_tlv_vector;
combined_tlv_vector.reserve(custom_tlvs.size() + proxy_proto_data.tlv_vector_.size());

absl::flat_hash_set<uint8_t> seen_types;
for (const auto& tlv : custom_tlvs) {
if (seen_types.contains(tlv.type)) {
ENVOY_LOG_MISC(warn, "Ignoring duplicate custom TLV type {}", tlv.type);
timflannagan marked this conversation as resolved.
Show resolved Hide resolved
continue;
}
seen_types.insert(tlv.type);
combined_tlv_vector.emplace_back(tlv);
}
for (const auto& tlv : proxy_proto_data.tlv_vector_) {
if (!pass_all_tlvs && !pass_through_tlvs.contains(tlv.type)) {
// Skip any TLV when pass_all_tlvs is disabled, or the TLV is not in the pass_through_tlvs.
continue;
}
if (seen_types.contains(tlv.type)) {
// Skip any duplicate TLVs from being added to the combined TLV vector.
ENVOY_LOG_MISC(warn, "Ignoring duplicate custom TLV type {}", tlv.type);
timflannagan marked this conversation as resolved.
Show resolved Hide resolved
continue;
}
seen_types.insert(tlv.type);
combined_tlv_vector.emplace_back(tlv);
}

uint64_t extension_length = 0;
for (auto&& tlv : combined_tlv_vector) {
extension_length += PROXY_PROTO_V2_TLV_TYPE_LENGTH_LEN + tlv.value.size();
if (extension_length > std::numeric_limits<uint16_t>::max()) {
ENVOY_LOG_MISC(
Expand All @@ -141,16 +165,13 @@ bool generateV2Header(const Network::ProxyProtocolData& proxy_proto_data, Buffer
generateV2Header(src.addressAsString(), dst.addressAsString(), src.port(), dst.port(),
src.version(), static_cast<uint16_t>(extension_length), out);

// Generate the TLV vector.
for (auto&& tlv : proxy_proto_data.tlv_vector_) {
if (!pass_all_tlvs && !pass_through_tlvs.contains(tlv.type)) {
continue;
}
for (auto&& tlv : combined_tlv_vector) {
out.add(&tlv.type, 1);
uint16_t size = htons(static_cast<uint16_t>(tlv.value.size()));
out.add(&size, sizeof(uint16_t));
out.add(&tlv.value.front(), tlv.value.size());
}

return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ void generateV2LocalHeader(Buffer::Instance& out);

// Generates the v2 PROXY protocol header including the TLV vector into the specified buffer.
bool generateV2Header(const Network::ProxyProtocolData& proxy_proto_data, Buffer::Instance& out,
bool pass_all_tlvs, const absl::flat_hash_set<uint8_t>& pass_through_tlvs);
bool pass_all_tlvs, const absl::flat_hash_set<uint8_t>& pass_through_tlvs,
const std::vector<Envoy::Network::ProxyProtocolTLV>& custom_tlvs);

} // namespace ProxyProtocol
} // namespace Common
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,10 @@ ReadOrParseState Filter::readExtensions(Network::ListenerFilterBuffer& buffer) {
auto raw_slice = buffer.rawSlice();
// waiting for more data if there is no enough data for extensions.
if (raw_slice.len_ < (proxy_protocol_header_.value().wholeHeaderLength())) {
ENVOY_LOG(
trace,
"waiting for more data to read extensions. Buffer length: {}, extension header length {}",
raw_slice.len_, proxy_protocol_header_.value().wholeHeaderLength());
return ReadOrParseState::TryAgainLater;
}

Expand Down
2 changes: 2 additions & 0 deletions source/extensions/transport_sockets/proxy_protocol/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,11 @@ envoy_cc_library(
"//source/common/common:hex_lib",
"//source/common/common:scalar_to_byte_vector_lib",
"//source/common/common:utility_lib",
"//source/common/config:well_known_names",
"//source/common/network:address_lib",
"//source/extensions/common/proxy_protocol:proxy_protocol_header_lib",
"//source/extensions/transport_sockets/common:passthrough_lib",
"@envoy_api//envoy/config/core/v3:pkg_cc_proto",
"@envoy_api//envoy/extensions/transport_sockets/proxy_protocol/v3:pkg_cc_proto",
],
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@
#include <sstream>

#include "envoy/config/core/v3/proxy_protocol.pb.h"
#include "envoy/extensions/transport_sockets/proxy_protocol/v3/upstream_proxy_protocol.pb.h"
#include "envoy/extensions/transport_sockets/proxy_protocol/v3/upstream_proxy_protocol.pb.validate.h"
#include "envoy/network/transport_socket.h"

#include "source/common/buffer/buffer_impl.h"
#include "source/common/common/hex.h"
#include "source/common/common/scalar_to_byte_vector.h"
#include "source/common/common/utility.h"
#include "source/common/config/well_known_names.h"
#include "source/common/network/address_impl.h"
#include "source/common/protobuf/utility.h"
#include "source/extensions/common/proxy_protocol/proxy_protocol_header.h"

using envoy::config::core::v3::ProxyProtocolConfig;
Expand All @@ -36,6 +40,15 @@ UpstreamProxyProtocolSocket::UpstreamProxyProtocolSocket(
pass_through_tlvs_.insert(0xFF & tlv_type);
}
}
for (const auto& entry : config.entries()) {
if (entry.value().empty()) {
timflannagan marked this conversation as resolved.
Show resolved Hide resolved
ENVOY_LOG(warn, "Skipping custom TLV with type {} due to empty value", entry.type());
continue;
}
config_tlvs_.emplace_back(Network::ProxyProtocolTLV{
static_cast<uint8_t>(entry.type()),
std::vector<unsigned char>(entry.value().begin(), entry.value().end())});
timflannagan marked this conversation as resolved.
Show resolved Hide resolved
}
}

void UpstreamProxyProtocolSocket::setTransportSocketCallbacks(
Expand Down Expand Up @@ -91,9 +104,21 @@ void UpstreamProxyProtocolSocket::generateHeaderV2() {
if (!options_ || !options_->proxyProtocolOptions().has_value()) {
Common::ProxyProtocol::generateV2LocalHeader(header_buffer_);
} else {
// process any custom TLVs from the host metadata.
auto host_metadata_tlv_types = processCustomTLVsFromHost();
// backfill any custom TLVs defined the config that are not in the host metadata.
for (const auto& tlv : config_tlvs_) {
// Skip any TLV that is already in the custom TLVs. We want the host
// metadata value to take precedence when there is a conflict.
if (host_metadata_tlv_types.contains(tlv.type)) {
continue;
}
custom_tlvs_.push_back(tlv);
}

const auto options = options_->proxyProtocolOptions().value();
if (!Common::ProxyProtocol::generateV2Header(options, header_buffer_, pass_all_tlvs_,
pass_through_tlvs_)) {
pass_through_tlvs_, custom_tlvs_)) {
// There is a warn log in generateV2Header method.
stats_.v2_tlvs_exceed_max_length_.inc();
}
Expand Down Expand Up @@ -165,6 +190,50 @@ void UpstreamProxyProtocolSocketFactory::hashKey(
}
}

absl::flat_hash_set<uint8_t> UpstreamProxyProtocolSocket::processCustomTLVsFromHost() {
absl::flat_hash_set<uint8_t> host_metadata_tlv_types;

timflannagan marked this conversation as resolved.
Show resolved Hide resolved
const auto& upstream_info = callbacks_->connection().streamInfo().upstreamInfo();
if (upstream_info == nullptr) {
return host_metadata_tlv_types;
}
Upstream::HostDescriptionConstSharedPtr host = upstream_info->upstreamHost();
if (host == nullptr) {
return host_metadata_tlv_types;
}
auto metadata = host->metadata();
if (metadata == nullptr) {
return host_metadata_tlv_types;
}

const auto filter_it = metadata->typed_filter_metadata().find(
Envoy::Config::MetadataFilters::get().ENVOY_TRANSPORT_SOCKETS_PROXY_PROTOCOL);
if (filter_it == metadata->typed_filter_metadata().end()) {
ENVOY_LOG(trace, "No custom TLVs found in upstream host metadata");
return host_metadata_tlv_types;
}

ProxyProtocolConfig tlvs_metadata;
if (!filter_it->second.UnpackTo(&tlvs_metadata)) {
ENVOY_LOG(warn, "Failed to unpack custom TLVs from upstream host metadata");
return host_metadata_tlv_types;
}

// process the custom TLVs from the host metadata first.
for (const auto& entry : tlvs_metadata.entries()) {
// prevent empty values from being added to custom TLVs.
if (entry.value().empty()) {
ENVOY_LOG(warn, "Skipping custom TLV with type {} due to empty value", entry.type());
continue;
}
custom_tlvs_.emplace_back(Network::ProxyProtocolTLV{
timflannagan marked this conversation as resolved.
Show resolved Hide resolved
static_cast<uint8_t>(entry.type()),
std::vector<unsigned char>(entry.value().begin(), entry.value().end())});
}

return host_metadata_tlv_types;
}

} // namespace ProxyProtocol
} // namespace TransportSockets
} // namespace Extensions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class UpstreamProxyProtocolSocket : public TransportSockets::PassthroughSocket,
void generateHeader();
void generateHeaderV1();
void generateHeaderV2();
absl::flat_hash_set<uint8_t> processCustomTLVsFromHost();
Network::IoResult writeHeader();

Network::TransportSocketOptionsConstSharedPtr options_;
Expand All @@ -52,6 +53,8 @@ class UpstreamProxyProtocolSocket : public TransportSockets::PassthroughSocket,
const UpstreamProxyProtocolStats& stats_;
const bool pass_all_tlvs_;
absl::flat_hash_set<uint8_t> pass_through_tlvs_{};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't related to this PR, but instead of this (and your new config) being copied by value here, the normal pattern is to hold a shared_ptr to a Config object that has already converted the protobuf config to the internal represenation, and done validation such as no duplicates.

std::vector<Envoy::Network::ProxyProtocolTLV> custom_tlvs_{};
std::vector<Envoy::Network::ProxyProtocolTLV> config_tlvs_{};
};

class UpstreamProxyProtocolSocketFactory : public PassthroughFactory {
Expand Down
Loading