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 33 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 that won't be matched. Useful to limit the scope and attack surface of the filter.
//
// When the filter receives PROXY protocol data that is disallowed, it will reject the connection.
// By default, the filter will match all PROXY protocol versions.
// See https://www.haproxy.org/download/2.1/doc/proxy-protocol.txt for details.
//
// .. 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 not attempt to match signatures for the disallowed versions.
// For example, when ``disallowed_versions=V2``, ``allow_requests_without_proxy_protocol=true``,
// and an incoming request matches the V2 signature, the filter will allow the request through without any modification.
// The filter treats this request as if it did not have any PROXY protocol information.
repeated config.core.v3.ProxyProtocolConfig.Version disallowed_versions = 4;
}
7 changes: 7 additions & 0 deletions changelogs/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,13 @@ new_features:
change: |
Added :ref:`formatters <envoy_v3_api_field_extensions.access_loggers.fluentd.v3.FluentdAccessLogConfig.formatters>`
to Fluentd access logger to allow adding extension commands when formatter access logs.
- area: proxy_protocol
change: |
Added :ref:`disallowed_versions <envoy_v3_api_field_extensions.filters.listener.proxy_protocol.v3.ProxyProtocol.disallowed_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 found/disallowed/errored by PROXY protocol version.
- area: rbac
change: |
Added :ref:`rules_stat_prefix <envoy_v3_api_field_extensions.filters.http.rbac.v3.RBAC.rules_stat_prefix>`
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,33 @@ 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, rooted at *downstream_proxy_proto*

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

not_found_disallowed, Counter, "Total number of connections that don't contain the PROXY protocol header and are rejected."
not_found_allowed, Counter, "Total number of connections that don't contain the PROXY protocol header, but are allowed due to :ref:`allow_requests_without_proxy_protocol <envoy_v3_api_field_extensions.filters.listener.proxy_protocol.v3.ProxyProtocol.allow_requests_without_proxy_protocol>`."

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

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

found, Counter, "Total number of connections where the PROXY protocol header was found and parsed correctly."
disallowed, Counter, "Total number of ``found`` connections that are rejected due to :ref:`disallowed_versions <envoy_v3_api_field_extensions.filters.listener.proxy_protocol.v3.ProxyProtocol.disallowed_versions>`."
error, Counter, "Total number of connections where the PROXY protocol header was malformed (and the connection was rejected)."

The filter also emits the following legacy statistics, rooted at its own scope:

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

downstream_cx_proxy_proto_error, Counter, "Total number of connections with proxy protocol errors, i.e. ``v1.error``, ``v2.error``, and ``not_found_disallowed``."

downstream_cx_proxy_proto_error, Counter, Total proxy protocol errors
.. attention::
Prefer using the more-detailed non-legacy statistics above.
3 changes: 3 additions & 0 deletions source/common/config/well_known_names.cc
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,9 @@ TagNameValues::TagNameValues() {

// http.<stat_prefix>.rbac.(<rules_stat_prefix>.)*
addTokenized(RBAC_HTTP_PREFIX, "http.*.rbac.$.**");

// downstream_proxy_proto.versions.(<version>).**
addTokenized(PROXY_PROTOCOL_VERSION, "downstream_proxy_proto.versions.$.**");
nareddyt marked this conversation as resolved.
Show resolved Hide resolved
}

void TagNameValues::addRe2(const std::string& name, const std::string& regex,
Expand Down
2 changes: 2 additions & 0 deletions source/common/config/well_known_names.h
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ class TagNameValues {
const std::string THRIFT_PREFIX = "envoy.thrift_prefix";
// Stats prefix for the Redis Proxy network filter
const std::string REDIS_PREFIX = "envoy.redis_prefix";
// Proxy Protocol version for a connection (Proxy Protocol listener filter).
const std::string PROXY_PROTOCOL_VERSION = "envoy.proxy_protocol_version";

// Mapping from the names above to their respective regex strings.
const std::vector<std::pair<std::string, std::string>> name_regex_pairs_;
Expand Down
129 changes: 116 additions & 13 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,60 @@ namespace Extensions {
namespace ListenerFilters {
namespace ProxyProtocol {

constexpr absl::string_view kProxyProtoStatsPrefix = "downstream_proxy_proto.";
nareddyt marked this conversation as resolved.
Show resolved Hide resolved
constexpr absl::string_view kVersionStatsPrefix = "versions.";

ProxyProtocolStats ProxyProtocolStats::create(Stats::Scope& scope) {
return {
/*legacy_=*/{LEGACY_PROXY_PROTOCOL_STATS(POOL_COUNTER(scope))},
/*general_=*/
{GENERAL_PROXY_PROTOCOL_STATS(POOL_COUNTER_PREFIX(scope, kProxyProtoStatsPrefix))},
/*v1_=*/
{VERSIONED_PROXY_PROTOCOL_STATS(POOL_COUNTER_PREFIX(
scope, absl::StrCat(kProxyProtoStatsPrefix, kVersionStatsPrefix, "v1.")))},
/*v2_=*/
{VERSIONED_PROXY_PROTOCOL_STATS(POOL_COUNTER_PREFIX(
scope, absl::StrCat(kProxyProtoStatsPrefix, kVersionStatsPrefix, "v2.")))},
};
}

void GeneralProxyProtocolStats::increment(ReadOrParseState decision) {
switch (decision) {
case ReadOrParseState::Done:
not_found_allowed_.inc();
break;
case ReadOrParseState::TryAgainLater:
break; // Do nothing.
case ReadOrParseState::Error:
not_found_disallowed_.inc();
break;
case ReadOrParseState::Denied:
IS_ENVOY_BUG("ReadOrParseState can never be Denied when proxy protocol is not found");
break;
}
}

void VersionedProxyProtocolStats::increment(ReadOrParseState decision) {
switch (decision) {
case ReadOrParseState::Done:
found_.inc();
break;
case ReadOrParseState::TryAgainLater:
break; // Do nothing.
case ReadOrParseState::Error:
error_.inc();
break;
case ReadOrParseState::Denied:
found_.inc();
disallowed_.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 +119,24 @@ Config::Config(
pass_through_tlvs_.insert(0xFF & tlv_type);
}
}

for (const auto& version : proto_config.disallowed_versions()) {
switch (version) {
PANIC_ON_PROTO_ENUM_SENTINEL_VALUES;
case ProxyProtocolConfig::V1:
allow_v1_ = false;
break;
case ProxyProtocolConfig::V2:
allow_v2_ = false;
break;
}
}

// Remove this check if PROXY protocol v3 is ever introduced.
if (!allow_v1_ && !allow_v2_) {
throw ProtoValidationException(
"Proxy Protocol filter is misconfigured: all proxy protocol versions are disallowed.");
}
}

const KeyValuePair* Config::isTlvTypeNeeded(uint8_t type) const {
Expand All @@ -91,6 +161,10 @@ bool Config::allowRequestsWithoutProxyProtocol() const {
return allow_requests_without_proxy_protocol_;
}

bool Config::isVersionV1Allowed() const { return allow_v1_; }

bool Config::isVersionV2Allowed() const { return allow_v2_; }

Network::FilterStatus Filter::onAccept(Network::ListenerFilterCallbacks& cb) {
ENVOY_LOG(debug, "proxy_protocol: New connection accepted");
cb_ = &cb;
Expand All @@ -99,16 +173,31 @@ 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_

switch (header_version_) {
case ProxyProtocolVersion::V1:
config_->stats_.v1_.increment(read_state);
break;
case ProxyProtocolVersion::V2:
config_->stats_.v2_.increment(read_state);
break;
case ProxyProtocolVersion::NotFound:
config_->stats_.general_.increment(read_state);
break;
}

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_.legacy_.downstream_cx_proxy_proto_error_
.inc(); // Keep for backwards-compatibility
cb_->socket().ioHandle().close();
return Network::FilterStatus::StopIteration;
case ReadOrParseState::TryAgainLater:
return Network::FilterStatus::StopIteration;
case ReadOrParseState::SkipFilter:
return Network::FilterStatus::Continue;
case ReadOrParseState::Done:
return Network::FilterStatus::Continue;
}
Expand All @@ -125,6 +214,10 @@ ReadOrParseState Filter::parseBuffer(Network::ListenerFilterBuffer& buffer) {
if (read_header_state != ReadOrParseState::Done) {
return read_header_state;
}
if (header_version_ == ProxyProtocolVersion::NotFound) {
// Filter is skipped and request is allowed through.
return ReadOrParseState::Done;
}
}

// After parse the header, the extensions size is discovered. Then extend the buffer
Expand Down Expand Up @@ -497,31 +590,37 @@ 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_->allowRequestsWithoutProxyProtocol()) {
auto matchv2 = config_->isVersionV2Allowed() &&
!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_->isVersionV1Allowed() &&
!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
// short-circuit
ENVOY_LOG(trace, "request does not use v1 or v2 proxy protocol, forwarding as is");
return ReadOrParseState::SkipFilter;
return ReadOrParseState::Done;
}
}

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_->isVersionV2Allowed()) {
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 +659,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 +670,11 @@ ReadOrParseState Filter::readProxyHeader(Network::ListenerFilterBuffer& buffer)
return ReadOrParseState::Error;
}

if (header_version_ == V1) {
if (header_version_ == ProxyProtocolVersion::V1) {
if (!config_->isVersionV1Allowed()) {
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
Loading
Loading