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

proxy_protocol_filter: Add configuration to match only specific proxy protocol versions, new stats #32861

Merged
merged 36 commits into from
Mar 29, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
787d32b
Code changes
nareddyt Mar 12, 2024
3010892
Format
nareddyt Mar 12, 2024
c285452
Docs
nareddyt Mar 12, 2024
c846b2e
Docs fix
nareddyt Mar 12, 2024
0a8f787
Add release notes
nareddyt Mar 12, 2024
ebcb7dd
Fix typo
nareddyt Mar 12, 2024
cfb3be7
Fix typo
nareddyt Mar 12, 2024
ae9c3e3
review comments
nareddyt Mar 13, 2024
5df93bb
update doc
nareddyt Mar 13, 2024
03b353e
Fix docs
nareddyt Mar 13, 2024
ed99e3c
Fix windows build
nareddyt Mar 13, 2024
c0614b3
Attempt to fix test failure
nareddyt Mar 13, 2024
d27a95c
Merge branch 'main' into pp-allowed-versions-stats
nareddyt Mar 18, 2024
7eded4e
Review comments: switch to disallowed_versions
nareddyt Mar 18, 2024
eca88f9
Fix doc
nareddyt Mar 18, 2024
0414a49
Review comments: doc and enum handling
nareddyt Mar 19, 2024
abdb083
Merge branch 'main' into pp-allowed-versions-stats
nareddyt Mar 19, 2024
b65c3aa
Review comments: enums and integration test
nareddyt Mar 19, 2024
1780d17
Make new stats rooted under listener
nareddyt Mar 19, 2024
e1fa890
fix test
nareddyt Mar 19, 2024
69ac950
Merge branch 'main' into pp-allowed-versions-stats
nareddyt Mar 20, 2024
ee28b67
Review comments: rename stats and make `not_found` a non-versioned stat
nareddyt Mar 20, 2024
e0a1147
Add tag extracted names for stats
nareddyt Mar 20, 2024
4a26f8e
runtime safetey for enum
nareddyt Mar 20, 2024
8abab59
Merge branch 'main' into pp-allowed-versions-stats
nareddyt Mar 22, 2024
68b8385
Merge branch 'main' into pp-allowed-versions-stats
nareddyt Mar 25, 2024
ccfdb1b
review comments: remove listener scope, throw validation exception, i…
nareddyt Mar 25, 2024
ec1e042
Merge remote-tracking branch 'origin/pp-allowed-versions-stats' into …
nareddyt Mar 25, 2024
bed5d6a
fix changelog
nareddyt Mar 25, 2024
f5640d6
fix changelog
nareddyt Mar 25, 2024
84e63ac
fix changelog
nareddyt Mar 25, 2024
f9c33a1
fix spelling
nareddyt Mar 26, 2024
4a02c62
review comments: change root stats scope, fix docs
nareddyt Mar 26, 2024
2c0f33f
Merge branch 'main' into pp-allowed-versions-stats
nareddyt Mar 27, 2024
5ae0954
review comments: fix stats tag extraction
nareddyt Mar 27, 2024
9674dbf
review comments: stats prefix name
nareddyt Mar 27, 2024
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 @@ -70,4 +70,19 @@ message ProxyProtocol {
// :ref:`core.v3.ProxyProtocolConfig.pass_through_tlvs <envoy_v3_api_field_config.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 to match. See https://www.haproxy.org/download/2.1/doc/proxy-protocol.txt for details.
// By default, the filter will match any version.
//
// When the filter receives PROXY protocol data that does not match the specified versions,
// it will reject the connection.
//
// .. attention::
//
// When used in conjunction with the :ref:`allow_requests_without_proxy_protocol <envoy_v3_api_field_extensions.filters.listener.proxy_protocol.v3.ProxyProtocol.allow_requests_without_proxy_protocol>`,
// the filter will only attempt to match signatures for the allowed versions.
// For example, when ``allowed_versions=V1``, ``allow_requests_without_proxy_protocol=true``,
// and an incoming request matches the V2 signature, the filter will allow the request through as if it did not contain
// PROXY protocol information.
nareddyt marked this conversation as resolved.
Show resolved Hide resolved
repeated config.core.v3.ProxyProtocolConfig.Version allowed_versions = 4;
nareddyt marked this conversation as resolved.
Show resolved Hide resolved
}
7 changes: 7 additions & 0 deletions changelogs/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,13 @@ new_features:
Update ``aws_request_signing`` filter to support optionally sending the aws signature in query parameters rather than headers,
by specifying the :ref:`query_string <envoy_v3_api_field_extensions.filters.http.aws_request_signing.v3.AwsRequestSigning.query_string>`
configuration section.
- area: proxy_protocol
change: |
Added :ref:`allowed_versions <envoy_v3_api_field_extensions.filters.listener.proxy_protocol.v3.ProxyProtocol.allowed_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 allowed/denied/error by PROXY protocol version.

deprecated:
- area: listener
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <envoy_v3_api_field_extensions.filters.listener.proxy_protocol.v3.ProxyProtocol.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.
Expand All @@ -29,10 +33,21 @@ 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:

.. csv-table::
:header: Name, Type, Description
:widths: 1, 1, 2

downstream_cx_proxy_proto_error, Counter, Total number of connections with proxy protocol errors

The filter also emits the statistics rooted at *downstream_cx_proxy_proto.<version>.* for each matched proxy protocol version.
Proxy protocol versions include ``v1``, ``v2``, and ``not_found``.

.. csv-table::
:header: Name, Type, Description
:widths: 1, 1, 2

downstream_cx_proxy_proto_error, Counter, Total proxy protocol errors
allowed, Counter, Total number of connections allowed
denied, Counter, Total number of connections rejected due to :ref:`allowed_versions <envoy_v3_api_field_extensions.filters.listener.proxy_protocol.v3.ProxyProtocol.allowed_versions>`.
error, Counter, Total number of connections rejected due to parsing error
121 changes: 109 additions & 12 deletions source/extensions/filters/listener/proxy_protocol/proxy_protocol.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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;
Expand All @@ -48,10 +50,47 @@ namespace Extensions {
namespace ListenerFilters {
namespace ProxyProtocol {

constexpr absl::string_view kVersionedStatsPrefix = "downstream_cx_proxy_proto.";

ProxyProtocolStats ProxyProtocolStats::create(Stats::Scope& scope) {
return {
/*general_stats_=*/{GENERAL_PROXY_PROTOCOL_STATS(POOL_COUNTER(scope))},
/*not_found_=*/
{VERSIONED_PROXY_PROTOCOL_STATS(
POOL_COUNTER_PREFIX(scope, absl::StrCat(kVersionedStatsPrefix, "not_found.")))},
/*v1_=*/
{VERSIONED_PROXY_PROTOCOL_STATS(
POOL_COUNTER_PREFIX(scope, absl::StrCat(kVersionedStatsPrefix, "v1.")))},
/*v2_=*/
{VERSIONED_PROXY_PROTOCOL_STATS(
POOL_COUNTER_PREFIX(scope, absl::StrCat(kVersionedStatsPrefix, "v2.")))},
};
}

void VersionedProxyProtocolStats::increment(ReadOrParseState decision) {
switch (decision) {
case ReadOrParseState::Done:
allowed_.inc();
break;
case ReadOrParseState::TryAgainLater:
// Do nothing.
break;
case ReadOrParseState::Error:
error_.inc();
break;
case ReadOrParseState::SkipFilter:
allowed_.inc();
break;
case ReadOrParseState::Denied:
denied_.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() ==
Expand All @@ -67,6 +106,26 @@ Config::Config(
pass_through_tlvs_.insert(0xFF & tlv_type);
}
}

if (proto_config.allowed_versions().empty()) {
ENVOY_LOG(debug, "No allowed_versions specified, allowing all PROXY protocol versions.");
allow_v1_ = true;
allow_v2_ = true;
} else {
for (const auto& version : proto_config.allowed_versions()) {
switch (version) {
case ProxyProtocolConfig::V1:
allow_v1_ = true;
break;
case ProxyProtocolConfig::V2:
allow_v2_ = true;
break;
default:
throw EnvoyException(
absl::StrCat("Unknown proxy protocol version (enum int cast): ", version));
}
}
}
}

const KeyValuePair* Config::isTlvTypeNeeded(uint8_t type) const {
Expand All @@ -87,8 +146,28 @@ bool Config::isPassThroughTlvTypeNeeded(uint8_t tlv_type) const {

size_t Config::numberOfNeededTlvTypes() const { return tlv_types_.size(); }

bool Config::allowRequestsWithoutProxyProtocol() const {
return allow_requests_without_proxy_protocol_;
bool Config::isVersionAllowed(ProxyProtocolVersion version) const {
switch (version) {
case ProxyProtocolVersion::NotFound:
return allow_requests_without_proxy_protocol_;
case ProxyProtocolVersion::V1:
return allow_v1_;
case ProxyProtocolVersion::V2:
return allow_v2_;
}
return false; // Should never reach here, but needed for windows compiler warning.
nareddyt marked this conversation as resolved.
Show resolved Hide resolved
}

VersionedProxyProtocolStats& Config::versionToStatsStruct(ProxyProtocolVersion version) {
switch (version) {
case ProxyProtocolVersion::NotFound:
return stats_.not_found_;
case ProxyProtocolVersion::V1:
return stats_.v1_;
case ProxyProtocolVersion::V2:
return stats_.v2_;
}
return stats_.not_found_; // Should never reach here, but needed for windows compiler warning.
}

Network::FilterStatus Filter::onAccept(Network::ListenerFilterCallbacks& cb) {
Expand All @@ -99,10 +178,18 @@ 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_

VersionedProxyProtocolStats& versioned_stats = config_->versionToStatsStruct(header_version_);
versioned_stats.increment(read_state);

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_.general_stats_.downstream_cx_proxy_proto_error_
.inc(); // Keep for backwards-compatibility
cb_->socket().ioHandle().close();
return Network::FilterStatus::StopIteration;
case ReadOrParseState::TryAgainLater:
Expand Down Expand Up @@ -497,10 +584,12 @@ ReadOrParseState Filter::readProxyHeader(Network::ListenerFilterBuffer& buffer)
auto raw_slice = buffer.rawSlice();
const char* buf = static_cast<const char*>(raw_slice.mem_);

if (config_.get()->allowRequestsWithoutProxyProtocol()) {
auto matchv2 = !memcmp(buf, PROXY_PROTO_V2_SIGNATURE,
if (config_->isVersionAllowed(ProxyProtocolVersion::NotFound)) {
auto matchv2 = config_->isVersionAllowed(ProxyProtocolVersion::V2) &&
!memcmp(buf, PROXY_PROTO_V2_SIGNATURE,
std::min<size_t>(PROXY_PROTO_V2_SIGNATURE_LEN, raw_slice.len_));
auto matchv1 = !memcmp(buf, PROXY_PROTO_V1_SIGNATURE,
auto matchv1 = config_->isVersionAllowed(ProxyProtocolVersion::V1) &&
!memcmp(buf, PROXY_PROTO_V1_SIGNATURE,
std::min<size_t>(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
Expand All @@ -513,15 +602,19 @@ ReadOrParseState Filter::readProxyHeader(Network::ListenerFilterBuffer& buffer)
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)");
return ReadOrParseState::Error;
}
}

if (header_version_ == V2) {
if (header_version_ == ProxyProtocolVersion::V2) {
if (!config_->isVersionAllowed(ProxyProtocolVersion::V2)) {
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");
Expand Down Expand Up @@ -560,7 +653,7 @@ ReadOrParseState Filter::readProxyHeader(Network::ListenerFilterBuffer& buffer)
// for more data.
break;
} else {
header_version_ = V1;
header_version_ = ProxyProtocolVersion::V1;
search_index_++;
}
break;
Expand All @@ -571,7 +664,11 @@ ReadOrParseState Filter::readProxyHeader(Network::ListenerFilterBuffer& buffer)
return ReadOrParseState::Error;
}

if (header_version_ == V1) {
if (header_version_ == ProxyProtocolVersion::V1) {
if (!config_->isVersionAllowed(ProxyProtocolVersion::V1)) {
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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,54 @@ 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, SkipFilter, Denied };

/**
* All stats for the proxy protocol. @see stats_macros.h
* Non-versioned general stats for the filter.
* Kept for backwards compatibility.
* @see stats_macros.h
*/
// clang-format off
#define ALL_PROXY_PROTOCOL_STATS(COUNTER) \
#define GENERAL_PROXY_PROTOCOL_STATS(COUNTER) \
COUNTER(downstream_cx_proxy_proto_error)
// clang-format on

struct GeneralProxyProtocolStats {
GENERAL_PROXY_PROTOCOL_STATS(GENERATE_COUNTER_STRUCT)
};

/**
* Stats reported for each version of the proxy protocol.
* @see stats_macros.h
*/
// clang-format off
#define VERSIONED_PROXY_PROTOCOL_STATS(COUNTER) \
COUNTER(allowed) \
COUNTER(denied) \
COUNTER(error)
nareddyt marked this conversation as resolved.
Show resolved Hide resolved
// 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)
GeneralProxyProtocolStats general_stats_;
VersionedProxyProtocolStats not_found_;
VersionedProxyProtocolStats v1_;
VersionedProxyProtocolStats v2_;

static ProxyProtocolStats create(Stats::Scope& scope);
};

/**
Expand Down Expand Up @@ -66,24 +101,26 @@ class Config : public Logger::Loggable<Logger::Id::filter> {
bool isPassThroughTlvTypeNeeded(uint8_t type) const;

/**
* Filter configuration that determines if we should pass-through requests without
* proxy protocol. Should only be configured to true for trusted downstreams.
* Return true if the given PROXY protocol version should be parsed by the filter.
*/
bool allowRequestsWithoutProxyProtocol() const;
bool isVersionAllowed(ProxyProtocolVersion version) const;
nareddyt marked this conversation as resolved.
Show resolved Hide resolved

/**
* Return the stats for the given PROXY protocol version.
*/
VersionedProxyProtocolStats& versionToStatsStruct(ProxyProtocolVersion version);

private:
absl::flat_hash_map<uint8_t, KeyValuePair> tlv_types_;
const bool allow_requests_without_proxy_protocol_;
const bool pass_all_tlvs_;
absl::flat_hash_set<uint8_t> pass_through_tlvs_{};
bool allow_v1_{false};
bool allow_v2_{false};
};

using ConfigSharedPtr = std::shared_ptr<Config>;

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)
Expand Down Expand Up @@ -134,7 +171,7 @@ class Filter : public Network::ListenerFilter, Logger::Loggable<Logger::Id::filt
// The index in buf_ where the search for '\r\n' should continue from
size_t search_index_{1};

ProxyProtocolVersion header_version_{Unknown};
ProxyProtocolVersion header_version_{ProxyProtocolVersion::NotFound};

ConfigSharedPtr config_;

Expand Down
Loading
Loading