Skip to content

Commit 63ea518

Browse files
yanavlasovphlax
authored andcommitted
Add option to reject early CONNECT data
Signed-off-by: Yan Avlasov <yavlasov@google.com>
1 parent ab21237 commit 63ea518

File tree

8 files changed

+93
-0
lines changed

8 files changed

+93
-0
lines changed

changelogs/current.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ behavior_changes:
77
The dynamic module ABI has been updated to support streaming body manipulation. This change also
88
fixed potential incorrect behavior when access or modify the request or response body. See
99
https://github.com/envoyproxy/envoy/issues/40918 for more details.
10+
- area: http
11+
change: |
12+
Added runtime flag ``envoy.reloadable_features.reject_early_connect_data`` to reject ``CONNECT`` requests
13+
that receive data before Envoy sent a ``200`` response to the client. While this is not a strictly compliant behavior
14+
it is very common as a latency reducing measure. As such the option is disabled by default.
1015
1116
minor_behavior_changes:
1217
# *Changes that may cause incompatibilities for some users, but should not for most*

docs/root/configuration/http/http_conn_man/response_code_details.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Below are the list of reasons the HttpConnectionManager or Router filter may sen
2525
downstream_remote_disconnect, The client disconnected unexpectedly.
2626
duration_timeout, The max connection duration was exceeded.
2727
direct_response, A direct response was generated by the router filter.
28+
early_connect_data, Data was received for a CONNECT request before 200 response headers were sent.
2829
filter_added_invalid_request_data, A filter added request data at the wrong stage in the filter chain.
2930
filter_added_invalid_response_data, A filter added response data at the wrong stage in the filter chain.
3031
filter_chain_not_found, The request was rejected due to no matching filter chain.

envoy/stream_info/stream_info.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,8 @@ struct ResponseCodeDetailValues {
246246
const std::string FilterAddedInvalidRequestData = "filter_added_invalid_request_data";
247247
// A filter called addDecodedData at the wrong point in the filter chain.
248248
const std::string FilterAddedInvalidResponseData = "filter_added_invalid_response_data";
249+
// Data was received for a CONNECT request before 200 response headers were sent.
250+
const std::string EarlyConnectData = "early_connect_data";
249251
// Changes or additions to details should be reflected in
250252
// docs/root/configuration/http/http_conn_man/response_code_details.rst
251253
};

source/common/router/router.cc

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -957,7 +957,19 @@ void Filter::sendNoHealthyUpstreamResponse(absl::optional<std::string> optional_
957957
absl::nullopt, details);
958958
}
959959

960+
bool Filter::isEarlyConnectData() {
961+
return downstream_headers_ != nullptr && Http::HeaderUtility::isConnect(*downstream_headers_) &&
962+
!downstream_response_started_ &&
963+
Runtime::runtimeFeatureEnabled("envoy.reloadable_features.reject_early_connect_data");
964+
}
965+
960966
Http::FilterDataStatus Filter::decodeData(Buffer::Instance& data, bool end_stream) {
967+
ENVOY_STREAM_LOG(debug, "router decoding data: {}", *callbacks_, data.length());
968+
if (data.length() > 0 && isEarlyConnectData()) {
969+
callbacks_->sendLocalReply(Http::Code::BadRequest, "", nullptr, absl::nullopt,
970+
StreamInfo::ResponseCodeDetails::get().EarlyConnectData);
971+
return Http::FilterDataStatus::StopIterationNoBuffer;
972+
}
961973
// upstream_requests_.size() cannot be > 1 because that only happens when a per
962974
// try timeout occurs with hedge_on_per_try_timeout enabled but the per
963975
// try timeout timer is not started until onRequestComplete(). It could be zero

source/common/router/router.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,7 @@ class Filter : Logger::Loggable<Logger::Id::router>,
601601
// Process Orca Load Report if necessary (e.g. cluster has lrsReportMetricNames).
602602
void maybeProcessOrcaLoadReport(const Envoy::Http::HeaderMap& headers_or_trailers,
603603
UpstreamRequest& upstream_request);
604+
bool isEarlyConnectData();
604605

605606
RetryStatePtr retry_state_;
606607
const FilterConfigSharedPtr config_;

source/common/runtime/runtime_features.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,10 @@ FALSE_RUNTIME_GUARD(envoy_reloadable_features_quic_disable_client_early_data);
182182

183183
FALSE_RUNTIME_GUARD(envoy_reloadable_features_ext_proc_graceful_grpc_close);
184184

185+
// TODO(yavlasov): Enabling by default will be hugely disruptive to existing traffic.
186+
// Replace with a config option (default off) post CVE release.
187+
FALSE_RUNTIME_GUARD(envoy_reloadable_features_reject_early_connect_data);
188+
185189
// Block of non-boolean flags. Use of int flags is deprecated. Do not add more.
186190
ABSL_FLAG(uint64_t, re2_max_program_size_error_level, 100, ""); // NOLINT
187191
ABSL_FLAG(uint64_t, re2_max_program_size_warn_level, // NOLINT

test/integration/fake_upstream.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -665,6 +665,11 @@ class FakeRawConnection : public FakeConnectionBase {
665665
data_.clear();
666666
}
667667

668+
bool hasData() {
669+
absl::MutexLock lock(&lock_);
670+
return !data_.empty();
671+
}
672+
668673
private:
669674
struct ReadFilter : public Network::ReadFilterBaseImpl {
670675
ReadFilter(FakeRawConnection& parent) : parent_(parent) {}

test/integration/tcp_tunneling_integration_test.cc

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,69 @@ TEST_P(ConnectTerminationIntegrationTest, IgnoreH11HostField) {
447447
sendRawHttpAndWaitForResponse(lookupPort("http"), full_request.c_str(), &response, true););
448448
}
449449

450+
TEST_P(ConnectTerminationIntegrationTest, EarlyConnectDataRejectedWithOverride) {
451+
config_helper_.addRuntimeOverride("envoy.reloadable_features.reject_early_connect_data", "true");
452+
initialize();
453+
454+
codec_client_ = makeHttpConnection(lookupPort("http"));
455+
// Send CONNECT request and immediately send some data without waiting for 200
456+
// response from Envoy.
457+
auto encoder_decoder = codec_client_->startRequest(connect_headers_);
458+
request_encoder_ = &encoder_decoder.first;
459+
codec_client_->sendData(*request_encoder_, "premature data", false);
460+
response_ = std::move(encoder_decoder.second);
461+
462+
// Envoy will try top open upstream connection before the premature CONNECT data is detected.
463+
ASSERT_TRUE(fake_upstreams_[0]->waitForRawConnection(fake_raw_upstream_connection_));
464+
465+
response_->waitForHeaders();
466+
EXPECT_EQ(response_->headers().getStatusValue(), "400");
467+
EXPECT_TRUE(response_->waitForEndStream());
468+
469+
// Because the downstream connection is closed by Envoy without sending any data the
470+
// upstream connection will remain in the pool and will not be closed.
471+
// However it should not have any data in it.
472+
EXPECT_FALSE(fake_raw_upstream_connection_->hasData());
473+
cleanupUpstreamAndDownstream();
474+
}
475+
476+
TEST_P(ConnectTerminationIntegrationTest, EarlyConnectDataAllowedByDefault) {
477+
initialize();
478+
479+
codec_client_ = makeHttpConnection(lookupPort("http"));
480+
// Send CONNECT request and immediately send some data without waiting for 200
481+
// response from Envoy.
482+
auto encoder_decoder = codec_client_->startRequest(connect_headers_);
483+
request_encoder_ = &encoder_decoder.first;
484+
codec_client_->sendData(*request_encoder_, "premature data", false);
485+
response_ = std::move(encoder_decoder.second);
486+
487+
// Wait for the data to arrive upstream.
488+
ASSERT_TRUE(fake_upstreams_[0]->waitForRawConnection(fake_raw_upstream_connection_));
489+
ASSERT_TRUE(fake_raw_upstream_connection_->waitForData(
490+
FakeRawConnection::waitForInexactMatch("premature data")));
491+
492+
// Send some data downstream.
493+
ASSERT_TRUE(fake_raw_upstream_connection_->write("upstream_send_data"));
494+
495+
// Wait for the headers and data to arrive downstream.
496+
response_->waitForHeaders();
497+
response_->waitForBodyData(strlen("upstream_send_data"));
498+
EXPECT_EQ("upstream_send_data", response_->body());
499+
500+
codec_client_->sendData(*request_encoder_, "", true);
501+
ASSERT_TRUE(fake_raw_upstream_connection_->waitForHalfClose());
502+
503+
ASSERT_TRUE(fake_raw_upstream_connection_->close());
504+
if (downstream_protocol_ == Http::CodecType::HTTP1) {
505+
ASSERT_TRUE(codec_client_->waitForDisconnect());
506+
} else {
507+
ASSERT_TRUE(response_->waitForEndStream());
508+
ASSERT_FALSE(response_->reset());
509+
}
510+
cleanupUpstreamAndDownstream();
511+
}
512+
450513
INSTANTIATE_TEST_SUITE_P(HttpAndIpVersions, ConnectTerminationIntegrationTest,
451514
testing::ValuesIn(HttpProtocolIntegrationTest::getProtocolTestParams(
452515
{Http::CodecType::HTTP1, Http::CodecType::HTTP2,

0 commit comments

Comments
 (0)