Skip to content

Commit 3700377

Browse files
committed
health check: use the ALPN-negotiated protocol for HTTP health checks
HTTP health checks negotiated ALPN whenever TLS was in use and then discarded the result, always speaking whatever `codec_client_type` said. That made heterogeneous and mid-migration upstreams impossible to health check with a single configuration, and let contradictory config (`codec_client_type: HTTP2` with `tls_options.alpn_protocols: ["http/1.1"]`) load cleanly and then fail forever. Use the negotiated protocol to select the health check codec, with `codec_client_type` retained as the value used when nothing is negotiated, i.e. plaintext health checks and peers that do not do ALPN. A protocol this health checker cannot speak also falls back to `codec_client_type`, so upstreams that route health checks by a custom ALPN identifier are unaffected. The ALPN offered on health check connections is deliberately left unchanged, so no configuration that works today changes behavior on the wire. Since the codec can only be chosen once the handshake has completed, a TLS health check connection is now connected first and the codec client attached on the Connected event, mirroring what `HttpConnPoolImplMixed` already does on the data plane. Plaintext and HTTP/3 health checks keep creating the codec client up front. Guarded by `envoy.reloadable_features.health_check_use_negotiated_protocol`. Fixes #46848 Signed-off-by: Huabing (Robin) Zhao <zhaohuabing@gmail.com> Signed-off-by: Huabing (Robin) Zhao <huabing@tetrate.io>
1 parent ada756c commit 3700377

13 files changed

Lines changed: 783 additions & 68 deletions

File tree

api/envoy/config/core/v3/health_check.proto

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,10 @@ message HealthCheck {
153153
repeated type.v3.Int64Range retriable_statuses = 12;
154154

155155
// Use specified application protocol for health checks.
156+
//
157+
// When the health check connection negotiates a protocol via ALPN, the negotiated protocol
158+
// selects the codec instead and this field is only used when nothing is negotiated, i.e. for
159+
// plaintext health checks and peers that do not support ALPN.
156160
type.v3.CodecClientType codec_client_type = 10 [(validate.rules).enum = {defined_only: true}];
157161

158162
// An optional service name parameter which is used to validate the identity of
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
HTTP health checks now select their codec from the protocol negotiated by ALPN, instead of always
2+
using :ref:`codec_client_type
3+
<envoy_v3_api_field_config.core.v3.HealthCheck.HttpHealthCheck.codec_client_type>`. A health check
4+
connection that negotiates ``h2`` is checked over HTTP/2 and one that negotiates ``http/1.1`` over
5+
HTTP/1.1; ``codec_client_type`` is now the value used when nothing is negotiated, i.e. for
6+
plaintext health checks and peers that do not do ALPN. The ALPN offered on health check
7+
connections is unchanged, and gRPC health checks are unaffected since gRPC requires HTTP/2. This
8+
behavior can be reverted by setting runtime guard
9+
``envoy.reloadable_features.health_check_use_negotiated_protocol`` to ``false``.

source/common/runtime/runtime_features.cc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ RUNTIME_GUARD(envoy_reloadable_features_grpc_side_stream_flow_control);
7474
RUNTIME_GUARD(envoy_reloadable_features_happy_eyeballs_sort_non_ip_addresses);
7575
RUNTIME_GUARD(envoy_reloadable_features_header_mutation_url_encode_query_params);
7676
RUNTIME_GUARD(envoy_reloadable_features_health_check_after_cluster_warming);
77+
RUNTIME_GUARD(envoy_reloadable_features_health_check_use_negotiated_protocol);
7778
RUNTIME_GUARD(envoy_reloadable_features_hide_transport_failure_reason_in_response_body);
7879
RUNTIME_GUARD(envoy_reloadable_features_http1_close_connection_on_zombie_stream_complete);
7980
RUNTIME_GUARD(envoy_reloadable_features_http2_discard_host_header);

source/extensions/health_checkers/http/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ envoy_cc_extension(
1919
deps = [
2020
"//source/common/http:codec_client_lib",
2121
"//source/common/http:response_decoder_impl_base",
22+
"//source/common/http:utility_lib",
2223
"//source/common/upstream:health_checker_lib",
2324
"//source/common/upstream:host_utility_lib",
2425
"//source/extensions/health_checkers/common:health_checker_base_lib",

source/extensions/health_checkers/http/health_checker_impl.cc

Lines changed: 129 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include "source/common/grpc/common.h"
2020
#include "source/common/http/header_map_impl.h"
2121
#include "source/common/http/header_utility.h"
22+
#include "source/common/http/utility.h"
2223
#include "source/common/network/address_impl.h"
2324
#include "source/common/network/socket_impl.h"
2425
#include "source/common/network/utility.h"
@@ -43,6 +44,23 @@ getMethod(const envoy::config::core::v3::RequestMethod config_method) {
4344
return config_method;
4445
}
4546

47+
bool useNegotiatedProtocol() {
48+
return Runtime::runtimeFeatureEnabled(
49+
"envoy.reloadable_features.health_check_use_negotiated_protocol");
50+
}
51+
52+
// Maps the protocol negotiated by ALPN to a codec type, falling back to `default_codec_type` when
53+
// nothing was negotiated or the negotiated protocol is not one this health checker can speak.
54+
Http::CodecType codecTypeFromAlpn(absl::string_view alpn, Http::CodecType default_codec_type) {
55+
if (alpn == Http::Utility::AlpnNames::get().Http11) {
56+
return Http::CodecType::HTTP1;
57+
}
58+
if (alpn == Http::Utility::AlpnNames::get().Http2) {
59+
return Http::CodecType::HTTP2;
60+
}
61+
return default_codec_type;
62+
}
63+
4664
} // namespace
4765

4866
Upstream::HealthCheckerSharedPtr HttpHealthCheckerFactory::createCustomHealthChecker(
@@ -200,7 +218,7 @@ Http::Protocol codecClientTypeToProtocol(Http::CodecType codec_client_type) {
200218
PANIC_DUE_TO_CORRUPT_ENUM
201219
}
202220

203-
Http::Protocol HttpHealthCheckerImpl::protocol() const {
221+
Http::Protocol HttpHealthCheckerImpl::configuredProtocol() const {
204222
return codecClientTypeToProtocol(codec_client_type_);
205223
}
206224

@@ -217,16 +235,30 @@ HttpHealthCheckerImpl::HttpActiveHealthCheckSession::HttpActiveHealthCheckSessio
217235

218236
HttpHealthCheckerImpl::HttpActiveHealthCheckSession::~HttpActiveHealthCheckSession() {
219237
ASSERT(client_ == nullptr);
238+
ASSERT(pending_connection_ == nullptr);
220239
}
221240

222241
void HttpHealthCheckerImpl::HttpActiveHealthCheckSession::onDeferredDelete() {
242+
// If there is an active request or a connection being established it will get reset, so make
243+
// sure we ignore the reset. This must be set before closing, since close() raises LocalClose
244+
// synchronously.
245+
expect_reset_ = true;
246+
resetPendingConnection();
223247
if (client_) {
224-
// If there is an active request it will get reset, so make sure we ignore the reset.
225-
expect_reset_ = true;
226248
client_->close(Network::ConnectionCloseType::Abort);
227249
}
228250
}
229251

252+
void HttpHealthCheckerImpl::HttpActiveHealthCheckSession::resetPendingConnection() {
253+
if (pending_connection_ == nullptr) {
254+
return;
255+
}
256+
pending_connection_->removeConnectionCallbacks(pending_connection_callback_impl_);
257+
pending_connection_->close(Network::ConnectionCloseType::Abort);
258+
pending_host_description_.reset();
259+
parent_.dispatcher_.deferredDelete(std::move(pending_connection_));
260+
}
261+
230262
void HttpHealthCheckerImpl::HttpActiveHealthCheckSession::decodeHeaders(
231263
Http::ResponseHeaderMapPtr&& headers, bool end_stream) {
232264
ASSERT(!response_headers_);
@@ -269,6 +301,7 @@ void HttpHealthCheckerImpl::HttpActiveHealthCheckSession::onEvent(Network::Conne
269301
// TODO(lilika) : Support connection pooling
270302
void HttpHealthCheckerImpl::HttpActiveHealthCheckSession::onInterval() {
271303
if (!client_) {
304+
ASSERT(pending_connection_ == nullptr);
272305
Upstream::Host::CreateConnectionData conn =
273306
host_->createHealthCheckConnection(parent_.dispatcher_, parent_.transportSocketOptions(),
274307
parent_.transportSocketMatchMetadata().get());
@@ -279,13 +312,90 @@ void HttpHealthCheckerImpl::HttpActiveHealthCheckSession::onInterval() {
279312
handleFailure(envoy::data::core::v3::NETWORK);
280313
return;
281314
}
282-
client_.reset(parent_.createCodecClient(conn));
283-
client_->addConnectionCallbacks(connection_callback_impl_);
284-
client_->setCodecConnectionCallbacks(http_connection_callback_impl_);
315+
316+
// ALPN is only negotiated on secure transports, and a QUIC connection reports no negotiated
317+
// protocol, so HTTP/3 has nothing to select a codec from. In every other case the configured
318+
// codec is the only possible answer, so the codec client is created up front and the request
319+
// is sent while the connection is still being established, as it has always been.
320+
if (useNegotiatedProtocol() && parent_.codec_client_type_ != Http::CodecType::HTTP3 &&
321+
conn.connection_->ssl() != nullptr) {
322+
// Reset these before connecting: a leftover `expect_reset_` from a previous timeout would
323+
// otherwise suppress the failure for this attempt.
324+
expect_reset_ = false;
325+
reuse_connection_ = parent_.reuse_connection_;
326+
pending_host_description_ = conn.host_description_;
327+
pending_connection_ = std::move(conn.connection_);
328+
pending_connection_->addConnectionCallbacks(pending_connection_callback_impl_);
329+
// Apply the connection settings that the codec client would otherwise have applied before
330+
// connecting, so that the connect and the handshake behave as they did when the codec client
331+
// was created up front.
332+
pending_connection_->detectEarlyCloseWhenReadDisabled(false);
333+
pending_connection_->noDelay(true);
334+
// The codec is chosen from the negotiated protocol and the request is sent once the
335+
// connection is established. See onPendingConnectionEvent().
336+
pending_connection_->connect();
337+
return;
338+
}
339+
340+
attachCodecClient(conn, parent_.codec_client_type_);
285341
expect_reset_ = false;
286342
reuse_connection_ = parent_.reuse_connection_;
287343
}
288344

345+
sendRequest();
346+
}
347+
348+
void HttpHealthCheckerImpl::HttpActiveHealthCheckSession::attachCodecClient(
349+
Upstream::Host::CreateConnectionData& data, Http::CodecType codec_type) {
350+
client_.reset(parent_.createCodecClient(data, codec_type));
351+
client_->addConnectionCallbacks(connection_callback_impl_);
352+
client_->setCodecConnectionCallbacks(http_connection_callback_impl_);
353+
protocol_ = codecClientTypeToProtocol(codec_type);
354+
}
355+
356+
void HttpHealthCheckerImpl::HttpActiveHealthCheckSession::onPendingConnectionEvent(
357+
Network::ConnectionEvent event) {
358+
ASSERT(pending_connection_ != nullptr);
359+
360+
if (event == Network::ConnectionEvent::RemoteClose ||
361+
event == Network::ConnectionEvent::LocalClose) {
362+
ENVOY_CONN_LOG(debug, "connect failure reason={} health_flags={}", *pending_connection_,
363+
pending_connection_->transportFailureReason(),
364+
HostUtility::healthFlagsToString(*host_));
365+
pending_connection_->removeConnectionCallbacks(pending_connection_callback_impl_);
366+
pending_host_description_.reset();
367+
parent_.dispatcher_.deferredDelete(std::move(pending_connection_));
368+
if (!expect_reset_) {
369+
// handleFailure() may deferred delete this session, so nothing may be touched afterwards.
370+
handleFailure(envoy::data::core::v3::NETWORK);
371+
}
372+
return;
373+
}
374+
375+
// Both Connected and ConnectedZeroRtt mean the handshake is done.
376+
if (event != Network::ConnectionEvent::Connected &&
377+
event != Network::ConnectionEvent::ConnectedZeroRtt) {
378+
return;
379+
}
380+
381+
// The negotiated protocol - if any - is now known. Anything other than a protocol this health
382+
// checker can speak falls back to the configured codec.
383+
const std::string alpn = pending_connection_->nextProtocol();
384+
const Http::CodecType codec_type = codecTypeFromAlpn(alpn, parent_.codec_client_type_);
385+
ENVOY_CONN_LOG(debug, "health check negotiated alpn='{}', using {}", *pending_connection_, alpn,
386+
Http::Utility::getProtocolString(codecClientTypeToProtocol(codec_type)));
387+
388+
// Hand the established connection over to a codec client. Adding and removing connection
389+
// callbacks while this event is being delivered is safe, and the codec client will simply see
390+
// the same Connected event once this returns.
391+
pending_connection_->removeConnectionCallbacks(pending_connection_callback_impl_);
392+
Upstream::Host::CreateConnectionData data{std::move(pending_connection_),
393+
std::move(pending_host_description_)};
394+
attachCodecClient(data, codec_type);
395+
sendRequest();
396+
}
397+
398+
void HttpHealthCheckerImpl::HttpActiveHealthCheckSession::sendRequest() {
289399
Http::RequestEncoder* request_encoder = &client_->newStream(*this);
290400
request_encoder->getStream().addCallbacks(*this);
291401
request_in_flight_ = true;
@@ -474,6 +584,15 @@ bool HttpHealthCheckerImpl::HttpActiveHealthCheckSession::shouldClose() const {
474584

475585
void HttpHealthCheckerImpl::HttpActiveHealthCheckSession::onTimeout() {
476586
request_in_flight_ = false;
587+
if (pending_connection_) {
588+
ENVOY_CONN_LOG(debug, "connect timeout health_flags={}", *pending_connection_,
589+
HostUtility::healthFlagsToString(*host_));
590+
// The caller records the timeout as a failure. resetPendingConnection() detaches the callbacks
591+
// before closing, so the close it triggers is not reported a second time.
592+
resetPendingConnection();
593+
return;
594+
}
595+
477596
if (client_) {
478597
ENVOY_CONN_LOG(debug, "connection/stream timeout health_flags={}", *client_,
479598
HostUtility::healthFlagsToString(*host_));
@@ -500,10 +619,10 @@ HttpHealthCheckerImpl::codecClientType(const envoy::type::v3::CodecClientType& t
500619
}
501620

502621
Http::CodecClient*
503-
ProdHttpHealthCheckerImpl::createCodecClient(Upstream::Host::CreateConnectionData& data) {
504-
return new Http::CodecClientProd(codec_client_type_, std::move(data.connection_),
505-
data.host_description_, dispatcher_, random_generator_,
506-
transportSocketOptions());
622+
ProdHttpHealthCheckerImpl::createCodecClient(Upstream::Host::CreateConnectionData& data,
623+
Http::CodecType codec_type) {
624+
return new Http::CodecClientProd(codec_type, std::move(data.connection_), data.host_description_,
625+
dispatcher_, random_generator_, transportSocketOptions());
507626
}
508627

509628
} // namespace Upstream

source/extensions/health_checkers/http/health_checker_impl.h

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,9 @@ class HttpHealthCheckerImpl : public HealthCheckerImplBase {
5151
Server::Configuration::HealthCheckerFactoryContext& context,
5252
HealthCheckEventLoggerPtr&& event_logger);
5353

54-
// Returns the HTTP protocol used for the health checker.
55-
Http::Protocol protocol() const;
54+
// Returns the HTTP protocol derived from `codec_client_type`. Note that a session whose codec was
55+
// selected from the ALPN-negotiated protocol may be speaking something else.
56+
Http::Protocol configuredProtocol() const;
5657

5758
/**
5859
* Utility class checking if given http status matches configured expectations.
@@ -87,6 +88,14 @@ class HttpHealthCheckerImpl : public HealthCheckerImplBase {
8788
enum class HealthCheckResult { Succeeded, Degraded, Failed, Retriable };
8889
HealthCheckResult healthCheckResult(uint64_t response_code);
8990
bool shouldClose() const;
91+
// Attaches a codec client of `codec_type` to `data`'s connection and wires up the callbacks.
92+
void attachCodecClient(Upstream::Host::CreateConnectionData& data, Http::CodecType codec_type);
93+
// Encodes the health check request on `client_`. Requires `client_ != nullptr`.
94+
void sendRequest();
95+
// Handles events on a connection that has not been handed to a codec client yet.
96+
void onPendingConnectionEvent(Network::ConnectionEvent event);
97+
// Aborts and disposes of `pending_connection_`, if any.
98+
void resetPendingConnection();
9099

91100
// ActiveHealthCheckSession
92101
void onInterval() override;
@@ -126,6 +135,23 @@ class HttpHealthCheckerImpl : public HealthCheckerImplBase {
126135
HttpActiveHealthCheckSession& parent_;
127136
};
128137

138+
// Callbacks for a connection that is still being established and has no codec client yet.
139+
// Deliberately distinct from ConnectionCallbackImpl so that every codec-driven callback can
140+
// continue to assume `client_ != nullptr`.
141+
class PendingConnectionCallbackImpl : public Network::ConnectionCallbacks {
142+
public:
143+
PendingConnectionCallbackImpl(HttpActiveHealthCheckSession& parent) : parent_(parent) {}
144+
// Network::ConnectionCallbacks
145+
void onEvent(Network::ConnectionEvent event) override {
146+
parent_.onPendingConnectionEvent(event);
147+
}
148+
void onAboveWriteBufferHighWatermark() override {}
149+
void onBelowWriteBufferLowWatermark() override {}
150+
151+
private:
152+
HttpActiveHealthCheckSession& parent_;
153+
};
154+
129155
class HttpConnectionCallbackImpl : public Http::ConnectionCallbacks {
130156
public:
131157
HttpConnectionCallbackImpl(HttpActiveHealthCheckSession& parent) : parent_(parent) {}
@@ -137,23 +163,31 @@ class HttpHealthCheckerImpl : public HealthCheckerImplBase {
137163
};
138164

139165
ConnectionCallbackImpl connection_callback_impl_{*this};
166+
PendingConnectionCallbackImpl pending_connection_callback_impl_{*this};
140167
HttpConnectionCallbackImpl http_connection_callback_impl_{*this};
141168
HttpHealthCheckerImpl& parent_;
142169
Http::CodecClientPtr client_;
170+
// Set while a connection is being established and the codec has not been chosen yet. Mutually
171+
// exclusive with `client_`.
172+
Network::ClientConnectionPtr pending_connection_;
173+
HostDescriptionConstSharedPtr pending_host_description_;
143174
Http::ResponseHeaderMapPtr response_headers_;
144175
Buffer::InstancePtr response_body_;
145176
const std::string& hostname_;
146177
Network::ConnectionInfoProviderSharedPtr local_connection_info_provider_;
147178
// Keep small members (bools and enums) at the end of class, to reduce alignment overhead.
148-
const Http::Protocol protocol_;
179+
// Not const: when the codec is chosen from the negotiated ALPN protocol this is updated to
180+
// match what the connection actually speaks.
181+
Http::Protocol protocol_;
149182
bool expect_reset_ : 1 = false;
150183
bool reuse_connection_ : 1 = false;
151184
bool request_in_flight_ : 1 = false;
152185
};
153186

154187
using HttpActiveHealthCheckSessionPtr = std::unique_ptr<HttpActiveHealthCheckSession>;
155188

156-
virtual Http::CodecClient* createCodecClient(Upstream::Host::CreateConnectionData& data) PURE;
189+
virtual Http::CodecClient* createCodecClient(Upstream::Host::CreateConnectionData& data,
190+
Http::CodecType codec_type) PURE;
157191

158192
// HealthCheckerImplBase
159193
ActiveHealthCheckSessionPtr makeSession(HostSharedPtr host) override {
@@ -188,7 +222,8 @@ class ProdHttpHealthCheckerImpl : public HttpHealthCheckerImpl {
188222
using HttpHealthCheckerImpl::HttpHealthCheckerImpl;
189223

190224
// HttpHealthCheckerImpl
191-
Http::CodecClient* createCodecClient(Upstream::Host::CreateConnectionData& data) override;
225+
Http::CodecClient* createCodecClient(Upstream::Host::CreateConnectionData& data,
226+
Http::CodecType codec_type) override;
192227
};
193228

194229
} // namespace Upstream

test/common/upstream/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ envoy_cc_test(
347347
"//test/mocks/protobuf:protobuf_mocks",
348348
"//test/mocks/runtime:runtime_mocks",
349349
"//test/mocks/server:health_checker_factory_context_mocks",
350+
"//test/mocks/ssl:ssl_mocks",
350351
"//test/mocks/upstream:cluster_info_mocks",
351352
"//test/mocks/upstream:cluster_priority_set_mocks",
352353
"//test/mocks/upstream:health_check_event_logger_mocks",

test/common/upstream/health_check_fuzz.cc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,8 @@ void HttpHealthCheckFuzz::respond(test::common::upstream::Respond respond, bool
142142
response_headers->setStatus(status);
143143

144144
// Responding with http can cause client to close, if so create a new one.
145-
const bool client_will_close =
146-
Http::HeaderUtility::shouldCloseConnection(health_checker_->protocol(), *response_headers);
145+
const bool client_will_close = Http::HeaderUtility::shouldCloseConnection(
146+
health_checker_->configuredProtocol(), *response_headers);
147147

148148
// Check if there is a response body.
149149
bool has_response_body = !respond.http_respond().body().empty();

0 commit comments

Comments
 (0)