Skip to content

Commit

Permalink
proxy_protocol_filter: Add configuration to match only specific proxy…
Browse files Browse the repository at this point in the history
… protocol versions, new stats (envoyproxy#32861)

---------

Signed-off-by: Teju Nareddy <tnareddy@confluent.io>
  • Loading branch information
nareddyt committed Apr 11, 2024
1 parent 9134d6a commit 91ada95
Show file tree
Hide file tree
Showing 10 changed files with 717 additions and 38 deletions.
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;
}
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 @@ -198,6 +198,9 @@ TagNameValues::TagNameValues() {

// listener_local_rate_limit.(<stat_prefix>.)
addTokenized(LOCAL_LISTENER_RATELIMIT_PREFIX, "listener_local_ratelimit.$.**");

// proxy_proto.(versions.v<version_number>.)**
addRe2(PROXY_PROTOCOL_VERSION, R"(^proxy_proto\.(versions\.v(\d)\.)\w+)", "proxy_proto.versions");
}

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 @@ -153,6 +153,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 = "proxy_proto.";
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

0 comments on commit 91ada95

Please sign in to comment.