Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
1 change: 1 addition & 0 deletions docs/root/version_history/current.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Bug Fixes

* ext_authz: fix the ext_authz http filter to correctly set response flags to ``UAEX`` when a connection is denied.
* ext_authz: fix the ext_authz network filter to correctly set response flag and code details to ``UAEX`` when a connection is denied.
* hcm: stop processing the response if encoding it has caused downstream reset. The fix is guarded by ``envoy.reloadable_features.handle_stream_reset_during_hcm_encoding``.
* listener: fixed the crash when updating listeners that do not bind to port.
* tcp: fixed a bug where upstream circuit breakers applied HTTP per-request bounds to TCP connections.
* thrift_proxy: fix the thrift_proxy connection manager to correctly report success/error response metrics when performing :ref:`payload passthrough <envoy_v3_api_field_extensions.filters.network.thrift_proxy.v3.ThriftProxy.payload_passthrough>`.
Expand Down
4 changes: 4 additions & 0 deletions source/common/http/conn_manager_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1538,6 +1538,10 @@ void ConnectionManagerImpl::ActiveStream::onResetStream(StreamResetReason reset_
filter_manager_.streamInfo().setResponseCodeDetails(
StreamInfo::ResponseCodeDetails::get().Overload);
}
if (Runtime::runtimeFeatureEnabled(
"envoy.reloadable_features.handle_stream_reset_during_hcm_encoding")) {
filter_manager_.onDownstreamReset();
}

connection_manager_.doDeferredStreamDestroy(*this);
}
Expand Down
16 changes: 15 additions & 1 deletion source/common/http/filter_manager.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1013,11 +1013,16 @@ void FilterManager::sendDirectLocalReply(
state_.non_100_response_headers_encoded_ = true;
filter_manager_callbacks_.encodeHeaders(*filter_manager_callbacks_.responseHeaders(),
end_stream);

if (state_.saw_downstream_reset_) {
return;
}
maybeEndEncode(end_stream);
},
[&](Buffer::Instance& data, bool end_stream) -> void {
filter_manager_callbacks_.encodeData(data, end_stream);
if (state_.saw_downstream_reset_) {
return;
}
maybeEndEncode(end_stream);
}},
Utility::LocalReplyData{state_.is_grpc_request_, code, body, grpc_status, is_head_request});
Expand Down Expand Up @@ -1151,6 +1156,9 @@ void FilterManager::encodeHeaders(ActiveStreamEncoderFilter* filter, ResponseHea
const bool modified_end_stream = (end_stream && continue_data_entry == encoder_filters_.end());
state_.non_100_response_headers_encoded_ = true;
filter_manager_callbacks_.encodeHeaders(headers, modified_end_stream);
if (state_.saw_downstream_reset_) {
return;
}
maybeEndEncode(modified_end_stream);

if (!modified_end_stream) {
Expand Down Expand Up @@ -1290,6 +1298,9 @@ void FilterManager::encodeData(ActiveStreamEncoderFilter* filter, Buffer::Instan

const bool modified_end_stream = end_stream && trailers_added_entry == encoder_filters_.end();
filter_manager_callbacks_.encodeData(data, modified_end_stream);
if (state_.saw_downstream_reset_) {
return;
}
maybeEndEncode(modified_end_stream);

// If trailers were adding during encodeData we need to trigger decodeTrailers in order
Expand Down Expand Up @@ -1332,6 +1343,9 @@ void FilterManager::encodeTrailers(ActiveStreamEncoderFilter* filter,
}

filter_manager_callbacks_.encodeTrailers(trailers);
if (state_.saw_downstream_reset_) {
return;
}
maybeEndEncode(true);
}

Expand Down
6 changes: 5 additions & 1 deletion source/common/http/filter_manager.h
Original file line number Diff line number Diff line change
Expand Up @@ -945,6 +945,8 @@ class FilterManager : public ScopeTrackedObject,

void contextOnContinue(ScopeTrackedObjectStack& tracked_object_stack);

void onDownstreamReset() { state_.saw_downstream_reset_ = true; }

private:
// Indicates which filter to start the iteration with.
enum class FilterIterationStartState { AlwaysStartFromNext, CanStartFromCurrent };
Expand Down Expand Up @@ -1065,7 +1067,8 @@ class FilterManager : public ScopeTrackedObject,
: remote_complete_(false), local_complete_(false), has_1xx_headers_(false),
created_filter_chain_(false), is_head_request_(false), is_grpc_request_(false),
non_100_response_headers_encoded_(false), under_on_local_reply_(false),
decoder_filter_chain_aborted_(false), encoder_filter_chain_aborted_(false) {}
decoder_filter_chain_aborted_(false), encoder_filter_chain_aborted_(false),
saw_downstream_reset_(false) {}

uint32_t filter_call_state_{0};

Expand All @@ -1088,6 +1091,7 @@ class FilterManager : public ScopeTrackedObject,
// True when the filter chain iteration was aborted with local reply.
bool decoder_filter_chain_aborted_ : 1;
bool encoder_filter_chain_aborted_ : 1;
bool saw_downstream_reset_ : 1;

// The following 3 members are booleans rather than part of the space-saving bitfield as they
// are passed as arguments to functions expecting bools. Extend State using the bitfield
Expand Down
1 change: 1 addition & 0 deletions source/common/runtime/runtime_features.cc
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ constexpr const char* runtime_features[] = {
"envoy.reloadable_features.disable_tls_inspector_injection",
"envoy.reloadable_features.fix_added_trailers",
"envoy.reloadable_features.grpc_bridge_stats_disabled",
"envoy.reloadable_features.handle_stream_reset_during_hcm_encoding",
"envoy.reloadable_features.hash_multiple_header_values",
"envoy.reloadable_features.health_check.graceful_goaway_handling",
"envoy.reloadable_features.http2_consume_stream_refused_errors",
Expand Down
122 changes: 122 additions & 0 deletions test/common/http/conn_manager_impl_test_2.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2882,5 +2882,127 @@ TEST_F(HttpConnectionManagerImplTest, RequestRejectedViaIPDetection) {
EXPECT_EQ(1U, stats_.named_.downstream_rq_rejected_via_ip_detection_.value());
}

TEST_F(HttpConnectionManagerImplTest, DisconnectDuringEncodeHeader) {
setup(false, "envoy-server-test");
setupFilterChain(1, 0);

EXPECT_CALL(*decoder_filters_[0], decodeHeaders(_, true))
.WillOnce(Return(FilterHeadersStatus::StopIteration));
startRequest(/*end_stream=*/true);

EXPECT_CALL(response_encoder_, encodeHeaders(_, true))
.WillOnce(Invoke([&](const ResponseHeaderMap& headers, bool) -> void {
EXPECT_NE(nullptr, headers.Server());
EXPECT_EQ("envoy-server-test", headers.getServerValue());
conn_manager_->onEvent(Network::ConnectionEvent::LocalClose);
}));
EXPECT_CALL(*decoder_filters_[0], onStreamComplete());
EXPECT_CALL(*decoder_filters_[0], onDestroy());

ResponseHeaderMapPtr response_headers{new TestResponseHeaderMapImpl{{":status", "200"}}};
decoder_filters_[0]->callbacks_->streamInfo().setResponseCodeDetails("");
decoder_filters_[0]->callbacks_->encodeHeaders(std::move(response_headers), true, "details");
}

TEST_F(HttpConnectionManagerImplTest, DisconnectDuringEncodeBody) {
setup(false, "envoy-server-test");
setupFilterChain(1, 0);

EXPECT_CALL(*decoder_filters_[0], decodeHeaders(_, true))
.WillOnce(Return(FilterHeadersStatus::StopIteration));
startRequest(/*end_stream=*/true);

EXPECT_CALL(response_encoder_, encodeHeaders(_, false))
.WillOnce(Invoke([&](const ResponseHeaderMap& headers, bool) -> void {
EXPECT_NE(nullptr, headers.Server());
EXPECT_EQ("envoy-server-test", headers.getServerValue());
}));
EXPECT_CALL(response_encoder_, encodeData(_, true))
.WillOnce(Invoke([&](Buffer::Instance&, bool) -> void {
conn_manager_->onEvent(Network::ConnectionEvent::LocalClose);
}));
EXPECT_CALL(*decoder_filters_[0], onStreamComplete());
EXPECT_CALL(*decoder_filters_[0], onDestroy());

ResponseHeaderMapPtr response_headers{new TestResponseHeaderMapImpl{{":status", "200"}}};
decoder_filters_[0]->callbacks_->streamInfo().setResponseCodeDetails("");
decoder_filters_[0]->callbacks_->encodeHeaders(std::move(response_headers), false, "details");
Buffer::OwnedImpl response_body("response");
decoder_filters_[0]->callbacks_->encodeData(response_body, true);
}

TEST_F(HttpConnectionManagerImplTest, DisconnectDuringEncodeTrailer) {
setup(false, "envoy-server-test");
setupFilterChain(1, 0);

EXPECT_CALL(*decoder_filters_[0], decodeHeaders(_, true))
.WillOnce(Return(FilterHeadersStatus::StopIteration));
startRequest(/*end_stream=*/true);

EXPECT_CALL(response_encoder_, encodeHeaders(_, false))
.WillOnce(Invoke([&](const ResponseHeaderMap& headers, bool) -> void {
EXPECT_NE(nullptr, headers.Server());
EXPECT_EQ("envoy-server-test", headers.getServerValue());
}));
EXPECT_CALL(response_encoder_, encodeData(_, false));
EXPECT_CALL(response_encoder_, encodeTrailers(_))
.WillOnce(Invoke([&](const Http::ResponseTrailerMap&) -> void {
conn_manager_->onEvent(Network::ConnectionEvent::LocalClose);
}));
EXPECT_CALL(*decoder_filters_[0], onStreamComplete());
EXPECT_CALL(*decoder_filters_[0], onDestroy());

ResponseHeaderMapPtr response_headers{new TestResponseHeaderMapImpl{{":status", "200"}}};
decoder_filters_[0]->callbacks_->streamInfo().setResponseCodeDetails("");
decoder_filters_[0]->callbacks_->encodeHeaders(std::move(response_headers), false, "details");
Buffer::OwnedImpl response_body("response");
decoder_filters_[0]->callbacks_->encodeData(response_body, false);
decoder_filters_[0]->callbacks_->encodeTrailers(
ResponseTrailerMapPtr{new TestResponseTrailerMapImpl{{"some", "trailer"}}});
}

TEST_F(HttpConnectionManagerImplTest, DirectLocalReplyCausesDisconnect) {
initial_buffer_limit_ = 10;
setup(false, "");
setUpEncoderAndDecoder(false, false);
sendRequestHeadersAndData();

// Start the response without processing the request headers through all
// filters.
ResponseHeaderMapPtr response_headers{new TestResponseHeaderMapImpl{{":status", "200"}}};
EXPECT_CALL(*encoder_filters_[1], encodeHeaders(_, false))
.WillOnce(Return(FilterHeadersStatus::StopIteration));
decoder_filters_[0]->callbacks_->streamInfo().setResponseCodeDetails("");
decoder_filters_[0]->callbacks_->encodeHeaders(std::move(response_headers), false, "details");

// Now overload the buffer with response data. The filter returns
// StopIterationAndBuffer, which will trigger an early response.

expectOnDestroy();
Buffer::OwnedImpl fake_response("A long enough string to go over watermarks");
// Fake response starts doing through the filter.
EXPECT_CALL(*encoder_filters_[1], encodeData(_, false))
.WillOnce(Return(FilterDataStatus::StopIterationAndBuffer));
std::string response_body;
// The 500 goes directly to the encoder.
EXPECT_CALL(response_encoder_, encodeHeaders(_, false))
.WillOnce(Invoke([&](const ResponseHeaderMap& headers, bool) -> FilterHeadersStatus {
// Make sure this is a 500
EXPECT_EQ("500", headers.getStatusValue());
// Make sure Envoy standard sanitization has been applied.
EXPECT_TRUE(headers.Date() != nullptr);
EXPECT_EQ("response_payload_too_large",
decoder_filters_[0]->callbacks_->streamInfo().responseCodeDetails().value());
return FilterHeadersStatus::Continue;
}));
EXPECT_CALL(response_encoder_, encodeData(_, true))
.WillOnce(Invoke([&](Buffer::Instance&, bool) -> void {
conn_manager_->onEvent(Network::ConnectionEvent::LocalClose);
}));
decoder_filters_[0]->callbacks_->encodeData(fake_response, false);

EXPECT_EQ(1U, stats_.named_.rs_too_large_.value());
}

} // namespace Http
} // namespace Envoy