Skip to content

Commit ea10ba4

Browse files
authored
tcp_tunneling: fix half close on upstream trailers (envoyproxy#32325)
When TCP tunneling, if the HTTP upstream is sending trailers, the downstream doesn't get any indication of this. To support half close semantics, fixing this with runtime guard so that FIN is sent downstream in such cases. Risk Level: medium Testing: unit tests, integration tests Docs Changes: none Release Notes: none Platform Specific Features: none Runtime guard: ``envoy.reloadable_features.tcp_tunneling_send_downstream_fin_on_upstream_trailers`` Signed-off-by: ohadvano <[email protected]>
1 parent 35a7cfc commit ea10ba4

File tree

5 files changed

+64
-0
lines changed

5 files changed

+64
-0
lines changed

changelogs/current.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,12 @@ bug_fixes:
8585
Fix crash due to uncaught exception when the operating system does not support an address type (such as IPv6) that is
8686
received in an mTLS client cert IP SAN. These SANs will be ignored. This applies only when using formatter
8787
``%DOWNSTREAM_PEER_IP_SAN%``.
88+
- area: tcp_proxy
89+
change: |
90+
When tunneling TCP over HTTP, closed the downstream connection (for writing only) when upstream trailers are read
91+
to support half close semantics during TCP tunneling.
92+
This behavioral change can be temporarily reverted by setting runtime guard
93+
``envoy.reloadable_features.tcp_tunneling_send_downstream_fin_on_upstream_trailers`` to false.
8894
- area: jwt_authn
8995
change: |
9096
Fixed JWT extractor, which concatenated headers with a comma, resultig in invalid tokens.

source/common/runtime/runtime_features.cc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ RUNTIME_GUARD(envoy_reloadable_features_skip_dns_lookup_for_proxied_requests);
8787
RUNTIME_GUARD(envoy_reloadable_features_ssl_transport_failure_reason_format);
8888
RUNTIME_GUARD(envoy_reloadable_features_stateful_session_encode_ttl_in_cookie);
8989
RUNTIME_GUARD(envoy_reloadable_features_stop_decode_metadata_on_local_reply);
90+
RUNTIME_GUARD(envoy_reloadable_features_tcp_tunneling_send_downstream_fin_on_upstream_trailers);
9091
RUNTIME_GUARD(envoy_reloadable_features_test_feature_true);
9192
RUNTIME_GUARD(envoy_reloadable_features_thrift_allow_negative_field_ids);
9293
RUNTIME_GUARD(envoy_reloadable_features_thrift_connection_draining);

source/common/tcp_proxy/upstream.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include "envoy/upstream/thread_local_cluster.h"
99
#include "envoy/upstream/upstream.h"
1010

11+
#include "source/common/buffer/buffer_impl.h"
1112
#include "source/common/common/dump_state_utils.h"
1213
#include "source/common/http/codec_client.h"
1314
#include "source/common/router/header_parser.h"
@@ -196,6 +197,12 @@ class HttpUpstream : public GenericUpstream, protected Http::StreamCallbacks {
196197
void decodeTrailers(Http::ResponseTrailerMapPtr&& trailers) override {
197198
parent_.config_.propagateResponseTrailers(std::move(trailers),
198199
parent_.downstream_info_.filterState());
200+
if (Runtime::runtimeFeatureEnabled(
201+
"envoy.reloadable_features.tcp_tunneling_send_downstream_fin_on_upstream_trailers")) {
202+
Buffer::OwnedImpl data;
203+
parent_.upstream_callbacks_.onUpstreamData(data, /* end_stream = */ true);
204+
}
205+
199206
parent_.doneReading();
200207
}
201208
void decodeMetadata(Http::MetadataMapPtr&&) override {}

test/common/tcp_proxy/upstream_test.cc

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,28 @@ TEST_P(HttpUpstreamTest, UpstreamTrailersMarksDoneReading) {
201201
this->upstream_->responseDecoder().decodeTrailers(std::move(trailers));
202202
}
203203

204+
TEST_P(HttpUpstreamTest, UpstreamTrailersPropagateFinDownstream) {
205+
setupUpstream();
206+
EXPECT_CALL(encoder_.stream_, resetStream(_)).Times(0);
207+
upstream_->doneWriting();
208+
EXPECT_CALL(callbacks_, onUpstreamData(BufferStringEqual(""), true));
209+
Http::ResponseTrailerMapPtr trailers{new Http::TestResponseTrailerMapImpl{{"key", "value"}}};
210+
upstream_->responseDecoder().decodeTrailers(std::move(trailers));
211+
}
212+
213+
TEST_P(HttpUpstreamTest, UpstreamTrailersDontPropagateFinDownstreamWhenFeatureDisabled) {
214+
TestScopedRuntime scoped_runtime;
215+
scoped_runtime.mergeValues(
216+
{{"envoy.reloadable_features.tcp_tunneling_send_downstream_fin_on_upstream_trailers",
217+
"false"}});
218+
setupUpstream();
219+
EXPECT_CALL(encoder_.stream_, resetStream(_)).Times(0);
220+
upstream_->doneWriting();
221+
EXPECT_CALL(callbacks_, onUpstreamData(_, _)).Times(0);
222+
Http::ResponseTrailerMapPtr trailers{new Http::TestResponseTrailerMapImpl{{"key", "value"}}};
223+
upstream_->responseDecoder().decodeTrailers(std::move(trailers));
224+
}
225+
204226
class HttpUpstreamRequestEncoderTest : public testing::TestWithParam<Http::CodecType> {
205227
public:
206228
HttpUpstreamRequestEncoderTest() {

test/integration/tcp_tunneling_integration_test.cc

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1313,6 +1313,34 @@ TEST_P(TcpTunnelingIntegrationTest, CopyResponseTrailers) {
13131313
EXPECT_THAT(waitForAccessLog(access_log_filename), testing::HasSubstr(trailer_value));
13141314
}
13151315

1316+
TEST_P(TcpTunnelingIntegrationTest, DownstreamFinOnUpstreamTrailers) {
1317+
if (upstreamProtocol() == Http::CodecType::HTTP1) {
1318+
return;
1319+
}
1320+
1321+
initialize();
1322+
1323+
tcp_client_ = makeTcpConnection(lookupPort("tcp_proxy"));
1324+
ASSERT_TRUE(fake_upstreams_[0]->waitForHttpConnection(*dispatcher_, fake_upstream_connection_));
1325+
ASSERT_TRUE(fake_upstream_connection_->waitForNewStream(*dispatcher_, upstream_request_));
1326+
ASSERT_TRUE(upstream_request_->waitForHeadersComplete());
1327+
1328+
upstream_request_->encodeHeaders(default_response_headers_, false);
1329+
sendBidiData(fake_upstream_connection_);
1330+
1331+
// Send trailers
1332+
const std::string trailer_value = "trailer-value";
1333+
Http::TestResponseTrailerMapImpl response_trailers{{"test-trailer-name", trailer_value}};
1334+
upstream_request_->encodeTrailers(response_trailers);
1335+
1336+
// Upstream trailers should close the downstream connection for writing.
1337+
tcp_client_->waitForHalfClose();
1338+
1339+
// Close Connection
1340+
tcp_client_->close();
1341+
ASSERT_TRUE(upstream_request_->waitForEndStream(*dispatcher_));
1342+
}
1343+
13161344
TEST_P(TcpTunnelingIntegrationTest, CloseUpstreamFirst) {
13171345
initialize();
13181346

0 commit comments

Comments
 (0)