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

healthcheck: support TCP health check with ProxyProtocol #31691

Merged
merged 13 commits into from
May 14, 2024
8 changes: 8 additions & 0 deletions api/envoy/config/core/v3/health_check.proto
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package envoy.config.core.v3;
import "envoy/config/core/v3/base.proto";
import "envoy/config/core/v3/event_service_config.proto";
import "envoy/config/core/v3/extension.proto";
import "envoy/config/core/v3/proxy_protocol.proto";
import "envoy/type/matcher/v3/string.proto";
import "envoy/type/v3/http.proto";
import "envoy/type/v3/range.proto";
Expand Down Expand Up @@ -177,6 +178,13 @@ message HealthCheck {
// payload block must be found, and in the order specified, but not
// necessarily contiguous.
repeated Payload receive = 2;

// When setting this value, it tries to attempt health check request with ProxyProtocol.
// When ``send`` is presented, they are sent after preceding ProxyProtocol header.
// Only ProxyProtocol header is sent when ``send`` is not presented.
// It allows to use both ProxyProtocol V1 and V2. In V1, it presents L3/L4. In V2, it includes
// LOCAL command and doesn't include L3/L4.
ProxyProtocolConfig proxy_protocol_config = 3;
}

message RedisHealthCheck {
Expand Down
4 changes: 4 additions & 0 deletions changelogs/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -72,5 +72,9 @@ new_features:
<envoy_v3_api_field_config.route.v3.RouteAction.RequestMirrorPolicy.disable_shadow_host_suffix_append>`
in :ref:`request_mirror_policies <envoy_v3_api_field_config.route.v3.RouteAction.request_mirror_policies>`
for disabling appending of the ``-shadow`` suffix to the shadowed host/authority header.
- area: healthcheck
change: |
Added support to healthcheck with ProxyProtocol in TCP Healthcheck by setting
:ref:`health_check_config <envoy_v3_api_field_config.core.v3.HealthCheck.TcpHealthCheck.proxy_protocol_config>`.

deprecated:
27 changes: 24 additions & 3 deletions source/extensions/health_checkers/tcp/health_checker_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include "source/common/router/router.h"
#include "source/common/runtime/runtime_features.h"
#include "source/common/upstream/host_utility.h"
#include "source/extensions/common/proxy_protocol/proxy_protocol_header.h"

#include "absl/strings/match.h"
#include "absl/strings/str_cat.h"
Expand Down Expand Up @@ -56,7 +57,11 @@ TcpHealthCheckerImpl::TcpHealthCheckerImpl(const Cluster& cluster,
auto bytes_or_error = PayloadMatcher::loadProtoBytes(send_repeated);
THROW_IF_STATUS_NOT_OK(bytes_or_error, throw);
return bytes_or_error.value();
}()) {
}()),
proxy_protocol_config_(config.tcp_health_check().has_proxy_protocol_config()
? std::make_unique<envoy::config::core::v3::ProxyProtocolConfig>(
config.tcp_health_check().proxy_protocol_config())
: nullptr) {
auto bytes_or_error = PayloadMatcher::loadProtoBytes(config.tcp_health_check().receive());
THROW_IF_STATUS_NOT_OK(bytes_or_error, throw);
receive_bytes_ = bytes_or_error.value();
Expand Down Expand Up @@ -141,12 +146,28 @@ void TcpHealthCheckerImpl::TcpActiveHealthCheckSession::onInterval() {
client_->noDelay(true);
}

Buffer::OwnedImpl data;
bool should_write_data = false;

if (parent_.proxy_protocol_config_ != nullptr) {
if (parent_.proxy_protocol_config_->version() ==
envoy::config::core::v3::ProxyProtocolConfig::V1) {
auto src_addr = client_->connectionInfoProvider().localAddress()->ip();
auto dst_addr = client_->connectionInfoProvider().remoteAddress()->ip();
Extensions::Common::ProxyProtocol::generateV1Header(*src_addr, *dst_addr, data);
} else if (parent_.proxy_protocol_config_->version() ==
envoy::config::core::v3::ProxyProtocolConfig::V2) {
Extensions::Common::ProxyProtocol::generateV2LocalHeader(data);
}
should_write_data = true;
}
if (!parent_.send_bytes_.empty()) {
Buffer::OwnedImpl data;
for (const std::vector<uint8_t>& segment : parent_.send_bytes_) {
data.add(segment.data(), segment.size());
}

should_write_data = true;
}
if (should_write_data) {
client_->write(data, false);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ class TcpHealthCheckerImpl : public HealthCheckerImplBase {

const PayloadMatcher::MatchSegments send_bytes_;
PayloadMatcher::MatchSegments receive_bytes_;
const std::unique_ptr<envoy::config::core::v3::ProxyProtocolConfig> proxy_protocol_config_;
};

} // namespace Upstream
Expand Down
42 changes: 42 additions & 0 deletions test/common/upstream/health_checker_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4277,6 +4277,25 @@ class TcpHealthCheckerImplTest : public testing::Test,
allocHealthChecker(yaml);
}

void setupDataProxyProtocol() {
std::string yaml = R"EOF(
timeout: 1s
interval: 1s
unhealthy_threshold: 2
healthy_threshold: 2
reuse_connection: false
tcp_health_check:
proxy_protocol_config:
version: v2
send:
text: "01"
receive:
- text: "02"
)EOF";

allocHealthChecker(yaml);
}

void expectSessionCreate() {
interval_timer_ = new Event::MockTimer(&dispatcher_);
timeout_timer_ = new Event::MockTimer(&dispatcher_);
Expand Down Expand Up @@ -4776,6 +4795,29 @@ TEST_F(TcpHealthCheckerImplTest, ConnectionLocalFailure) {
EXPECT_EQ(0UL, cluster_->info_->stats_store_.counter("health_check.passive_failure").value());
}

TEST_F(TcpHealthCheckerImplTest, SuccessProxyProtocol) {
InSequence s;

setupDataProxyProtocol();
cluster_->prioritySet().getMockHostSet(0)->hosts_ = {
makeTestHost(cluster_->info_, "tcp://127.0.0.1:80", simTime())};
expectSessionCreate();
expectClientCreate();
EXPECT_CALL(*connection_, write(_, _));
EXPECT_CALL(*timeout_timer_, enableTimer(_, _));
health_checker_->start();

connection_->runHighWatermarkCallbacks();
connection_->runLowWatermarkCallbacks();
connection_->raiseEvent(Network::ConnectionEvent::Connected);

EXPECT_CALL(*timeout_timer_, disableTimer());
EXPECT_CALL(*interval_timer_, enableTimer(_, _));
Buffer::OwnedImpl response;
addUint8(response, 2);
read_filter_->onData(response, false);
}

class TestGrpcHealthCheckerImpl : public GrpcHealthCheckerImpl {
public:
using GrpcHealthCheckerImpl::GrpcHealthCheckerImpl;
Expand Down
71 changes: 71 additions & 0 deletions test/integration/health_check_integration_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,44 @@ class TcpHealthCheckIntegrationTest : public Event::TestUsingSimulatedTime,
ASSERT_TRUE(cluster_data.host_fake_raw_connection_->waitForData(
FakeRawConnection::waitForInexactMatch("Ping")));
}

void
initProxyProtoHealthCheck(uint32_t cluster_idx,
envoy::config::core::v3::ProxyProtocolConfig proxy_protocol_config) {
auto& cluster_data = clusters_[cluster_idx];
auto health_check = addHealthCheck(cluster_data.cluster_);
health_check->mutable_tcp_health_check()->mutable_send()->set_text("50696E67"); // "Ping"
health_check->mutable_tcp_health_check()->add_receive()->set_text("506F6E67"); // "Pong"
health_check->mutable_tcp_health_check()->mutable_proxy_protocol_config()->CopyFrom(
proxy_protocol_config);

// Introduce the cluster using compareDiscoveryRequest / sendDiscoveryResponse.
EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true));
sendDiscoveryResponse<envoy::config::cluster::v3::Cluster>(
Config::TypeUrl::get().Cluster, {cluster_data.cluster_}, {cluster_data.cluster_}, {}, "55");

// Wait for upstream to receive TCP HC request.
ASSERT_TRUE(
cluster_data.host_upstream_->waitForRawConnection(cluster_data.host_fake_raw_connection_));
if (proxy_protocol_config.version() ==
envoy::config::core::v3::ProxyProtocolConfig_Version_V1) {
ASSERT_TRUE(
cluster_data.host_fake_raw_connection_->waitForData([](const std::string& data) -> bool {
if (GetParam() == Network::Address::IpVersion::v4) {
return data.find("Ping") != std::string::npos &&
data.find("PROXY TCP4 127.0.0.1 127.0.0.1") != std::string::npos;
}
return data.find("Ping") != std::string::npos &&
data.find("PROXY TCP6 ::1 ::1") != std::string::npos;
}));
} else {
// ProxyProtocol Signature + Local Command + "Ping"
const char header[] = {0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49,
0x54, 0x0a, 0x20, 0x00, 0x00, 0x00, 0x50, 0x69, 0x6e, 0x67};
ASSERT_TRUE(cluster_data.host_fake_raw_connection_->waitForData(
FakeRawConnection::waitForInexactMatch(std::string(header).c_str())));
}
}
};

INSTANTIATE_TEST_SUITE_P(IpVersions, TcpHealthCheckIntegrationTest,
Expand Down Expand Up @@ -607,6 +645,39 @@ TEST_P(TcpHealthCheckIntegrationTest, SingleEndpointWrongResponseTcp) {
EXPECT_EQ(1, test_server_->counter("cluster.cluster_1.health_check.failure")->value());
}

// Tests that a healthy endpoint returns a valid TCP health check response with ProxyProtocol.
TEST_P(TcpHealthCheckIntegrationTest, SingleEndpointHealthyTcpWithProxyProtocolV1) {
envoy::config::core::v3::ProxyProtocolConfig proxy_protocol_config;
proxy_protocol_config.set_version(envoy::config::core::v3::ProxyProtocolConfig_Version_V1);

const uint32_t cluster_idx = 0;
initialize();
initProxyProtoHealthCheck(cluster_idx, proxy_protocol_config);

AssertionResult result = clusters_[cluster_idx].host_fake_raw_connection_->write("Pong");
RELEASE_ASSERT(result, result.message());

test_server_->waitForCounterGe("cluster.cluster_1.health_check.success", 1);
EXPECT_EQ(1, test_server_->counter("cluster.cluster_1.health_check.success")->value());
EXPECT_EQ(0, test_server_->counter("cluster.cluster_1.health_check.failure")->value());
}

TEST_P(TcpHealthCheckIntegrationTest, SingleEndpointHealthyTcpWithProxyProtocolV2) {
envoy::config::core::v3::ProxyProtocolConfig proxy_protocol_config;
proxy_protocol_config.set_version(envoy::config::core::v3::ProxyProtocolConfig_Version_V2);

const uint32_t cluster_idx = 0;
initialize();
initProxyProtoHealthCheck(cluster_idx, proxy_protocol_config);

AssertionResult result = clusters_[cluster_idx].host_fake_raw_connection_->write("Pong");
RELEASE_ASSERT(result, result.message());

test_server_->waitForCounterGe("cluster.cluster_1.health_check.success", 1);
EXPECT_EQ(1, test_server_->counter("cluster.cluster_1.health_check.success")->value());
EXPECT_EQ(0, test_server_->counter("cluster.cluster_1.health_check.failure")->value());
}

// Tests that no TCP health check response results in timeout and unhealthy endpoint.
TEST_P(TcpHealthCheckIntegrationTest, SingleEndpointTimeoutTcp) {
const uint32_t cluster_idx = 0;
Expand Down