Skip to content

Commit 876753a

Browse files
UDP Tunneling: Optionally propagate response headers and trailers to downstream info (envoyproxy#30597)
Add support for saving upstream response headers and trailers to downstream info Risk Level: low Testing: integration tests Docs Changes: API Signed-off-by: Issa Abu Kalbein <iabukalbein@microsoft.com>
1 parent 4f46697 commit 876753a

7 files changed

Lines changed: 266 additions & 4 deletions

File tree

api/envoy/extensions/filters/udp/udp_proxy/v3/udp_proxy.proto

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ message UdpProxyConfig {
6666

6767
// Configuration for tunneling UDP over other transports or application layers.
6868
// Tunneling is currently supported over HTTP/2.
69-
// [#next-free-field: 10]
69+
// [#next-free-field: 12]
7070
message UdpTunnelingConfig {
7171
// Configuration for UDP datagrams buffering.
7272
message BufferOptions {
@@ -160,6 +160,14 @@ message UdpProxyConfig {
160160
// while the upstream is not ready will be dropped. In case this field is set but the options
161161
// are not configured, the default values will be applied as described in the ``BufferOptions``.
162162
BufferOptions buffer_options = 9;
163+
164+
// Save the response headers to the downstream info filter state for consumption
165+
// by the session filters. The filter state key is ``envoy.udp_proxy.propagate_response_headers``.
166+
bool propagate_response_headers = 10;
167+
168+
// Save the response trailers to the downstream info filter state for consumption
169+
// by the session filters. The filter state key is ``envoy.udp_proxy.propagate_response_trailers``.
170+
bool propagate_response_trailers = 11;
163171
}
164172

165173
// The stat prefix used when emitting UDP proxy filter stats.

changelogs/current.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,5 +189,12 @@ new_features:
189189
Ratelimit supports optional additional prefix to use when emitting statistics with :ref:`stat_prefix
190190
<envoy_v3_api_field_extensions.filters.http.ratelimit.v3.RateLimit.stat_prefix>`
191191
configuration flag.
192+
- area: udp_proxy
193+
change: |
194+
added support for propagating the response headers in :ref:`UdpTunnelingConfig
195+
<envoy_v3_api_field_extensions.filters.udp.udp_proxy.v3.UdpProxyConfig.UdpTunnelingConfig.propagate_response_headers>` and
196+
response trailers in :ref:`UdpTunnelingConfig
197+
<envoy_v3_api_field_extensions.filters.udp.udp_proxy.v3.UdpProxyConfig.UdpTunnelingConfig.propagate_response_trailers>` to
198+
the downstream info filter state.
192199
193200
deprecated:

source/extensions/filters/udp/udp_proxy/config.cc

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,25 @@ constexpr uint32_t DefaultMaxConnectAttempts = 1;
1111
constexpr uint32_t DefaultMaxBufferedDatagrams = 1024;
1212
constexpr uint64_t DefaultMaxBufferedBytes = 16384;
1313

14+
ProtobufTypes::MessagePtr TunnelResponseHeadersOrTrailers::serializeAsProto() const {
15+
auto proto_out = std::make_unique<envoy::config::core::v3::HeaderMap>();
16+
value().iterate([&proto_out](const Http::HeaderEntry& e) -> Http::HeaderMap::Iterate {
17+
auto* new_header = proto_out->add_headers();
18+
new_header->set_key(std::string(e.key().getStringView()));
19+
new_header->set_value(std::string(e.value().getStringView()));
20+
return Http::HeaderMap::Iterate::Continue;
21+
});
22+
return proto_out;
23+
}
24+
25+
const std::string& TunnelResponseHeaders::key() {
26+
CONSTRUCT_ON_FIRST_USE(std::string, "envoy.udp_proxy.propagate_response_headers");
27+
}
28+
29+
const std::string& TunnelResponseTrailers::key() {
30+
CONSTRUCT_ON_FIRST_USE(std::string, "envoy.udp_proxy.propagate_response_trailers");
31+
}
32+
1433
TunnelingConfigImpl::TunnelingConfigImpl(const TunnelingConfig& config,
1534
Server::Configuration::FactoryContext& context)
1635
: header_parser_(Envoy::Router::HeaderParser::configure(config.headers_to_add())),
@@ -31,7 +50,9 @@ TunnelingConfigImpl::TunnelingConfigImpl(const TunnelingConfig& config,
3150
? PROTOBUF_GET_WRAPPED_OR_DEFAULT(config.buffer_options(),
3251
max_buffered_bytes,
3352
DefaultMaxBufferedBytes)
34-
: DefaultMaxBufferedBytes) {
53+
: DefaultMaxBufferedBytes),
54+
propagate_response_headers_(config.propagate_response_headers()),
55+
propagate_response_trailers_(config.propagate_response_trailers()) {
3556
if (!post_path_.empty() && !use_post_) {
3657
throw EnvoyException("Can't set a post path when POST method isn't used");
3758
}

source/extensions/filters/udp/udp_proxy/config.h

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,43 @@ namespace UdpProxy {
1414
using TunnelingConfig =
1515
envoy::extensions::filters::udp::udp_proxy::v3::UdpProxyConfig::UdpTunnelingConfig;
1616

17+
/**
18+
* Base class for both tunnel response headers and trailers.
19+
*/
20+
class TunnelResponseHeadersOrTrailers : public StreamInfo::FilterState::Object {
21+
public:
22+
ProtobufTypes::MessagePtr serializeAsProto() const override;
23+
virtual const Http::HeaderMap& value() const PURE;
24+
};
25+
26+
/**
27+
* Response headers for the tunneling connections.
28+
*/
29+
class TunnelResponseHeaders : public TunnelResponseHeadersOrTrailers {
30+
public:
31+
TunnelResponseHeaders(Http::ResponseHeaderMapPtr&& response_headers)
32+
: response_headers_(std::move(response_headers)) {}
33+
const Http::HeaderMap& value() const override { return *response_headers_; }
34+
static const std::string& key();
35+
36+
private:
37+
const Http::ResponseHeaderMapPtr response_headers_;
38+
};
39+
40+
/**
41+
* Response trailers for the tunneling connections.
42+
*/
43+
class TunnelResponseTrailers : public TunnelResponseHeadersOrTrailers {
44+
public:
45+
TunnelResponseTrailers(Http::ResponseTrailerMapPtr&& response_trailers)
46+
: response_trailers_(std::move(response_trailers)) {}
47+
const Http::HeaderMap& value() const override { return *response_trailers_; }
48+
static const std::string& key();
49+
50+
private:
51+
const Http::ResponseTrailerMapPtr response_trailers_;
52+
};
53+
1754
class TunnelingConfigImpl : public UdpTunnelingConfig {
1855
public:
1956
TunnelingConfigImpl(const TunnelingConfig& config,
@@ -37,6 +74,32 @@ class TunnelingConfigImpl : public UdpTunnelingConfig {
3774
uint32_t maxBufferedDatagrams() const override { return max_buffered_datagrams_; };
3875
uint64_t maxBufferedBytes() const override { return max_buffered_bytes_; };
3976

77+
void
78+
propagateResponseHeaders(Http::ResponseHeaderMapPtr&& headers,
79+
const StreamInfo::FilterStateSharedPtr& filter_state) const override {
80+
if (!propagate_response_headers_) {
81+
return;
82+
}
83+
84+
filter_state->setData(TunnelResponseHeaders::key(),
85+
std::make_shared<TunnelResponseHeaders>(std::move(headers)),
86+
StreamInfo::FilterState::StateType::ReadOnly,
87+
StreamInfo::FilterState::LifeSpan::Connection);
88+
}
89+
90+
void
91+
propagateResponseTrailers(Http::ResponseTrailerMapPtr&& trailers,
92+
const StreamInfo::FilterStateSharedPtr& filter_state) const override {
93+
if (!propagate_response_trailers_) {
94+
return;
95+
}
96+
97+
filter_state->setData(TunnelResponseTrailers::key(),
98+
std::make_shared<TunnelResponseTrailers>(std::move(trailers)),
99+
StreamInfo::FilterState::StateType::ReadOnly,
100+
StreamInfo::FilterState::LifeSpan::Connection);
101+
}
102+
40103
private:
41104
std::unique_ptr<Envoy::Router::HeaderParser> header_parser_;
42105
Formatter::FormatterPtr proxy_host_formatter_;
@@ -49,6 +112,8 @@ class TunnelingConfigImpl : public UdpTunnelingConfig {
49112
bool buffer_enabled_;
50113
uint32_t max_buffered_datagrams_;
51114
uint64_t max_buffered_bytes_;
115+
bool propagate_response_headers_;
116+
bool propagate_response_trailers_;
52117
};
53118

54119
class UdpProxyFilterConfigImpl : public UdpProxyFilterConfig,

source/extensions/filters/udp/udp_proxy/udp_proxy_filter.h

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,14 @@ class UdpTunnelingConfig {
102102
virtual bool bufferEnabled() const PURE;
103103
virtual uint32_t maxBufferedDatagrams() const PURE;
104104
virtual uint64_t maxBufferedBytes() const PURE;
105+
106+
virtual void
107+
propagateResponseHeaders(Http::ResponseHeaderMapPtr&& headers,
108+
const StreamInfo::FilterStateSharedPtr& filter_state) const PURE;
109+
110+
virtual void
111+
propagateResponseTrailers(Http::ResponseTrailerMapPtr&& trailers,
112+
const StreamInfo::FilterStateSharedPtr& filter_state) const PURE;
105113
};
106114

107115
using UdpTunnelingConfigPtr = std::unique_ptr<const UdpTunnelingConfig>;
@@ -314,6 +322,9 @@ class HttpUpstreamImpl : public HttpUpstream, protected Http::StreamCallbacks {
314322
is_valid_response = Http::HeaderUtility::isConnectUdpResponse(*headers);
315323
}
316324

325+
parent_.tunnel_config_.propagateResponseHeaders(std::move(headers),
326+
parent_.downstream_info_.filterState());
327+
317328
if (!is_valid_response || end_stream) {
318329
parent_.resetEncoder(Network::ConnectionEvent::LocalClose);
319330
} else if (parent_.tunnel_creation_callbacks_.has_value()) {
@@ -329,7 +340,9 @@ class HttpUpstreamImpl : public HttpUpstream, protected Http::StreamCallbacks {
329340
}
330341
}
331342

332-
void decodeTrailers(Http::ResponseTrailerMapPtr&&) override {
343+
void decodeTrailers(Http::ResponseTrailerMapPtr&& trailers) override {
344+
parent_.tunnel_config_.propagateResponseTrailers(std::move(trailers),
345+
parent_.downstream_info_.filterState());
333346
parent_.resetEncoder(Network::ConnectionEvent::LocalClose);
334347
}
335348

test/extensions/filters/udp/udp_proxy/mocks.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,14 @@ class MockUdpTunnelingConfig : public UdpTunnelingConfig {
5858
MOCK_METHOD(bool, bufferEnabled, (), (const));
5959
MOCK_METHOD(uint32_t, maxBufferedDatagrams, (), (const));
6060
MOCK_METHOD(uint64_t, maxBufferedBytes, (), (const));
61+
MOCK_METHOD(void, propagateResponseHeaders,
62+
(Http::ResponseHeaderMapPtr && headers,
63+
const StreamInfo::FilterStateSharedPtr& filter_state),
64+
(const));
65+
MOCK_METHOD(void, propagateResponseTrailers,
66+
(Http::ResponseTrailerMapPtr && trailers,
67+
const StreamInfo::FilterStateSharedPtr& filter_state),
68+
(const));
6169

6270
std::string default_proxy_host_ = "default.host.com";
6371
std::string default_target_host_ = "default.target.host";

test/integration/udp_tunneling_integration_test.cc

Lines changed: 141 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,8 @@ class UdpTunnelingIntegrationTest : public HttpProtocolIntegrationTest {
351351
absl::optional<BufferOptions> buffer_options_;
352352
absl::optional<std::string> idle_timeout_;
353353
std::string session_access_log_config_ = "";
354+
bool propagate_response_headers_ = false;
355+
bool propagate_response_trailers_ = false;
354356
};
355357

356358
void setup(const TestConfig& config) {
@@ -379,9 +381,12 @@ name: udp_proxy
379381
default_target_port: {}
380382
retry_options:
381383
max_connect_attempts: {}
384+
propagate_response_headers: {}
385+
propagate_response_trailers: {}
382386
)EOF",
383387
config.proxy_host_, config.target_host_, config.default_target_port_,
384-
config.max_connect_attempts_);
388+
config.max_connect_attempts_, config.propagate_response_headers_,
389+
config.propagate_response_trailers_);
385390

386391
if (config.buffer_options_.has_value()) {
387392
filter_config += fmt::format(R"EOF(
@@ -739,6 +744,141 @@ TEST_P(UdpTunnelingIntegrationTest, ConnectionAttemptRetry) {
739744
EXPECT_THAT(waitForAccessLog(access_log_filename), testing::HasSubstr("2"));
740745
}
741746

747+
TEST_P(UdpTunnelingIntegrationTest, PropagateValidResponseHeaders) {
748+
const std::string access_log_filename =
749+
TestEnvironment::temporaryPath(TestUtility::uniqueFilename());
750+
751+
const std::string session_access_log_config = fmt::format(R"EOF(
752+
access_log:
753+
- name: envoy.access_loggers.file
754+
typed_config:
755+
'@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
756+
path: {}
757+
log_format:
758+
text_format_source:
759+
inline_string: "%FILTER_STATE(envoy.udp_proxy.propagate_response_headers:TYPED)%\n"
760+
)EOF",
761+
access_log_filename);
762+
763+
const TestConfig config{"host.com",
764+
"target.com",
765+
1,
766+
30,
767+
false,
768+
"",
769+
BufferOptions{1, 30},
770+
absl::nullopt,
771+
session_access_log_config,
772+
true,
773+
false};
774+
setup(config);
775+
776+
const std::string datagram = "hello";
777+
establishConnection(datagram);
778+
779+
// Wait for buffered datagram.
780+
ASSERT_TRUE(upstream_request_->waitForData(*dispatcher_, expectedCapsules({datagram})));
781+
782+
sendCapsuleDownstream("response", true);
783+
test_server_->waitForGaugeEq("udp.foo.downstream_sess_active", 0);
784+
785+
// Verify response header value is in the access log.
786+
EXPECT_THAT(waitForAccessLog(access_log_filename), testing::HasSubstr("capsule-protocol"));
787+
}
788+
789+
TEST_P(UdpTunnelingIntegrationTest, PropagateInvalidResponseHeaders) {
790+
const std::string access_log_filename =
791+
TestEnvironment::temporaryPath(TestUtility::uniqueFilename());
792+
793+
const std::string session_access_log_config = fmt::format(R"EOF(
794+
access_log:
795+
- name: envoy.access_loggers.file
796+
typed_config:
797+
'@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
798+
path: {}
799+
log_format:
800+
text_format_source:
801+
inline_string: "%FILTER_STATE(envoy.udp_proxy.propagate_response_headers:TYPED)%\n"
802+
)EOF",
803+
access_log_filename);
804+
805+
const TestConfig config{"host.com",
806+
"target.com",
807+
1,
808+
30,
809+
false,
810+
"",
811+
BufferOptions{1, 30},
812+
absl::nullopt,
813+
session_access_log_config,
814+
true,
815+
false};
816+
setup(config);
817+
818+
client_->write("hello", *listener_address_);
819+
ASSERT_TRUE(fake_upstreams_[0]->waitForHttpConnection(*dispatcher_, fake_upstream_connection_));
820+
ASSERT_TRUE(fake_upstream_connection_->waitForNewStream(*dispatcher_, upstream_request_));
821+
ASSERT_TRUE(upstream_request_->waitForHeadersComplete());
822+
expectRequestHeaders(upstream_request_->headers());
823+
824+
Http::TestResponseHeaderMapImpl response_headers{{":status", "404"}};
825+
upstream_request_->encodeHeaders(response_headers, true);
826+
827+
test_server_->waitForCounterEq("cluster.cluster_0.upstream_cx_connect_attempts_exceeded", 1);
828+
test_server_->waitForCounterEq("cluster.cluster_0.udp.sess_tunnel_failure", 1);
829+
test_server_->waitForCounterEq("cluster.cluster_0.udp.sess_tunnel_success", 0);
830+
test_server_->waitForGaugeEq("udp.foo.downstream_sess_active", 0);
831+
832+
// Verify response header value is in the access log.
833+
EXPECT_THAT(waitForAccessLog(access_log_filename), testing::HasSubstr("404"));
834+
}
835+
836+
TEST_P(UdpTunnelingIntegrationTest, PropagateResponseTrailers) {
837+
const std::string access_log_filename =
838+
TestEnvironment::temporaryPath(TestUtility::uniqueFilename());
839+
840+
const std::string session_access_log_config = fmt::format(R"EOF(
841+
access_log:
842+
- name: envoy.access_loggers.file
843+
typed_config:
844+
'@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
845+
path: {}
846+
log_format:
847+
text_format_source:
848+
inline_string: "%FILTER_STATE(envoy.udp_proxy.propagate_response_trailers:TYPED)%\n"
849+
)EOF",
850+
access_log_filename);
851+
852+
const TestConfig config{"host.com",
853+
"target.com",
854+
1,
855+
30,
856+
false,
857+
"",
858+
BufferOptions{1, 30},
859+
absl::nullopt,
860+
session_access_log_config,
861+
false,
862+
true};
863+
setup(config);
864+
865+
const std::string datagram = "hello";
866+
establishConnection(datagram);
867+
868+
// Wait for buffered datagram.
869+
ASSERT_TRUE(upstream_request_->waitForData(*dispatcher_, expectedCapsules({datagram})));
870+
sendCapsuleDownstream("response", false);
871+
872+
const std::string trailer_value = "test-trailer-value";
873+
Http::TestResponseTrailerMapImpl response_trailers{{"test-trailer-name", trailer_value}};
874+
upstream_request_->encodeTrailers(response_trailers);
875+
876+
test_server_->waitForGaugeEq("udp.foo.downstream_sess_active", 0);
877+
878+
// Verify response trailer value is in the access log.
879+
EXPECT_THAT(waitForAccessLog(access_log_filename), testing::HasSubstr(trailer_value));
880+
}
881+
742882
INSTANTIATE_TEST_SUITE_P(IpAndHttpVersions, UdpTunnelingIntegrationTest,
743883
testing::ValuesIn(HttpProtocolIntegrationTest::getProtocolTestParams(
744884
{Http::CodecType::HTTP2}, {Http::CodecType::HTTP2})),

0 commit comments

Comments
 (0)