Skip to content

Commit cd171d9

Browse files
authored
websocket: tunneling websockets (and upgrades in general) over H2 (envoyproxy#4188)
This allows tunneling over H2, unfortunately only enabled via nghttp2_option_set_no_http_messaging until nghttp2/nghttp2#1181 is sorted out. See the big warnings about not using (at least without knowing you're going to have a roll-out that may break backwards-compatibility some time in the not too distant future) Risk Level: Medium (changes are contained behind H2-with-Upgrade header which doesn't work today) Testing: unit tests, and turned up the full H1/H2 upstream/downstream in the integration test Docs Changes: for now, though I may take them out. I think they're useful for review. Release Notes: not added since we don't want folks using it (outside of testbeds) yet. envoyproxy#1630 Signed-off-by: Alyssa Wilk <alyssar@chromium.org>
1 parent b9dc5d9 commit cd171d9

File tree

23 files changed

+482
-68
lines changed

23 files changed

+482
-68
lines changed

api/envoy/api/v2/core/protocol.proto

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,20 @@ message Http2ProtocolOptions {
7676
// window. Currently, this has the same minimum/maximum/default as *initial_stream_window_size*.
7777
google.protobuf.UInt32Value initial_connection_window_size = 4
7878
[(validate.rules).uint32 = {gte: 65535, lte: 2147483647}];
79+
80+
// [#not-implemented-hide:] Hiding until nghttp2 has native support.
81+
//
82+
// Allows proxying Websocket and other upgrades over H2 connect.
83+
//
84+
// THIS IS NOT SAFE TO USE IN PRODUCTION
85+
//
86+
// This currently works via disabling all HTTP sanity checks for H2 traffic
87+
// which is a much larger hammer than we'd like to use. Eventually when
88+
// https://github.com/nghttp2/nghttp2/issues/1181 is resolved, this will work
89+
// with simply enabling CONNECT for H2. This may require some tweaks to the
90+
// headers making pre-CONNECT-support proxying not backwards compatible with
91+
// post-CONNECT-support proxying.
92+
bool allow_connect = 5;
7993
}
8094

8195
// [#not-implemented-hide:]

docs/root/intro/arch_overview/websocket.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,29 @@ one can set up custom
2020
for the given upgrade type, up to and including only using the router filter to send the WebSocket
2121
data upstream.
2222

23+
Handling H2 hops (implementation in progress)
24+
---------------------------------------------
25+
26+
Envoy currently has an alpha implementation of tunneling websockets over H2 streams for deployments
27+
that prefer a uniform H2 mesh throughout, for example, for a deployment of the form:
28+
29+
[Client] ---- HTTP/1.1 ---- [Front Envoy] ---- HTTP/2 ---- [Sidecar Envoy ---- H1 ---- App]
30+
31+
In this case, if a client is for example using WebSocket, we want the Websocket to arive at the
32+
upstream server functionally intact, which means it needs to traverse the HTTP/2 hop.
33+
34+
TODO(alyssawilk) copy the warnings from the config here, or just land the docs when we unhide.
35+
36+
This is accomplished via
37+
`extended CONNECT <https://tools.ietf.org/html/draft-mcmanus-httpbis-h2-websockets>`_ support. The
38+
WebSocket request will be transformed into an HTTP/2 CONNECT stream, with :protocol header
39+
indicating the original upgrade, traverse the HTTP/2 hop, and be downgraded back into an HTTP/1
40+
WebSocket Upgrade. This same Upgrade-CONNECT-Upgrade transformation will be performed on any
41+
HTTP/2 hop, with the documented flaw that the HTTP/1.1 method is always assumed to be GET.
42+
Non-WebSocket upgrades are allowed to use any valid HTTP method (i.e. POST) and the current
43+
upgrade/downgrade mechanism will drop the original method and transform the Upgrade request to
44+
a GET method on the final Envoy-Upstream hop.
45+
2346
Old style WebSocket support
2447
===========================
2548

include/envoy/http/codec.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ struct Http2Settings {
208208
uint32_t max_concurrent_streams_{DEFAULT_MAX_CONCURRENT_STREAMS};
209209
uint32_t initial_stream_window_size_{DEFAULT_INITIAL_STREAM_WINDOW_SIZE};
210210
uint32_t initial_connection_window_size_{DEFAULT_INITIAL_CONNECTION_WINDOW_SIZE};
211+
bool allow_connect_{DEFAULT_ALLOW_CONNECT};
211212

212213
// disable HPACK compression
213214
static const uint32_t MIN_HPACK_TABLE_SIZE = 0;
@@ -241,6 +242,8 @@ struct Http2Settings {
241242
// our default connection-level window also equals to our stream-level
242243
static const uint32_t DEFAULT_INITIAL_CONNECTION_WINDOW_SIZE = 256 * 1024 * 1024;
243244
static const uint32_t MAX_INITIAL_CONNECTION_WINDOW_SIZE = (1U << 31) - 1;
245+
// By default both nghttp2 and Envoy do not allow CONNECT over H2.
246+
static const bool DEFAULT_ALLOW_CONNECT = false;
244247
};
245248

246249
/**

include/envoy/http/header_map.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ class HeaderEntry {
281281
HEADER_FUNC(Origin) \
282282
HEADER_FUNC(OtSpanContext) \
283283
HEADER_FUNC(Path) \
284+
HEADER_FUNC(Protocol) \
284285
HEADER_FUNC(ProxyConnection) \
285286
HEADER_FUNC(Referer) \
286287
HEADER_FUNC(RequestId) \

source/common/http/conn_manager_impl.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -580,7 +580,7 @@ void ConnectionManagerImpl::ActiveStream::decodeHeaders(HeaderMapPtr&& headers,
580580

581581
// Modify the downstream remote address depending on configuration and headers.
582582
request_info_.setDownstreamRemoteAddress(ConnectionManagerUtility::mutateRequestHeaders(
583-
*request_headers_, protocol, connection_manager_.read_callbacks_->connection(),
583+
*request_headers_, connection_manager_.read_callbacks_->connection(),
584584
connection_manager_.config_, *snapped_route_config_, connection_manager_.random_generator_,
585585
connection_manager_.runtime_, connection_manager_.local_info_));
586586
ASSERT(request_info_.downstreamRemoteAddress() != nullptr);

source/common/http/conn_manager_utility.cc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@ namespace Envoy {
1919
namespace Http {
2020

2121
Network::Address::InstanceConstSharedPtr ConnectionManagerUtility::mutateRequestHeaders(
22-
Http::HeaderMap& request_headers, Protocol protocol, Network::Connection& connection,
22+
Http::HeaderMap& request_headers, Network::Connection& connection,
2323
ConnectionManagerConfig& config, const Router::Config& route_config,
2424
Runtime::RandomGenerator& random, Runtime::Loader& runtime,
2525
const LocalInfo::LocalInfo& local_info) {
2626
// If this is a Upgrade request, do not remove the Connection and Upgrade headers,
2727
// as we forward them verbatim to the upstream hosts.
28-
if (protocol == Protocol::Http11 && Utility::isUpgrade(request_headers)) {
28+
if (Utility::isUpgrade(request_headers)) {
2929
// The current WebSocket implementation re-uses the HTTP1 codec to send upgrade headers to
3030
// the upstream host. This adds the "transfer-encoding: chunked" request header if the stream
3131
// has not ended and content-length does not exist. In HTTP1.1, if transfer-encoding and

source/common/http/conn_manager_utility.h

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@ class ConnectionManagerUtility {
2828
* existence of the x-forwarded-for header. Again see the method for more details.
2929
*/
3030
static Network::Address::InstanceConstSharedPtr
31-
mutateRequestHeaders(Http::HeaderMap& request_headers, Protocol protocol,
32-
Network::Connection& connection, ConnectionManagerConfig& config,
33-
const Router::Config& route_config, Runtime::RandomGenerator& random,
34-
Runtime::Loader& runtime, const LocalInfo::LocalInfo& local_info);
31+
mutateRequestHeaders(Http::HeaderMap& request_headers, Network::Connection& connection,
32+
ConnectionManagerConfig& config, const Router::Config& route_config,
33+
Runtime::RandomGenerator& random, Runtime::Loader& runtime,
34+
const LocalInfo::LocalInfo& local_info);
3535

3636
static void mutateResponseHeaders(Http::HeaderMap& response_headers,
3737
const Http::HeaderMap* request_headers, const std::string& via);

source/common/http/headers.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ class HeaderValues {
7676
const LowerCaseString Origin{"origin"};
7777
const LowerCaseString OtSpanContext{"x-ot-span-context"};
7878
const LowerCaseString Path{":path"};
79+
const LowerCaseString Protocol{":protocol"};
7980
const LowerCaseString ProxyConnection{"proxy-connection"};
8081
const LowerCaseString Referer{"referer"};
8182
const LowerCaseString RequestId{"x-request-id"};
@@ -158,6 +159,7 @@ class HeaderValues {
158159
} ExpectValues;
159160

160161
struct {
162+
const std::string Connect{"CONNECT"};
161163
const std::string Get{"GET"};
162164
const std::string Head{"HEAD"};
163165
const std::string Post{"POST"};

source/common/http/http2/codec_impl.cc

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
#include "common/http/codes.h"
1818
#include "common/http/exception.h"
1919
#include "common/http/headers.h"
20-
#include "common/http/utility.h"
2120

2221
namespace Envoy {
2322
namespace Http {
@@ -90,7 +89,15 @@ void ConnectionImpl::StreamImpl::encode100ContinueHeaders(const HeaderMap& heade
9089

9190
void ConnectionImpl::StreamImpl::encodeHeaders(const HeaderMap& headers, bool end_stream) {
9291
std::vector<nghttp2_nv> final_headers;
93-
buildHeaders(final_headers, headers);
92+
93+
Http::HeaderMapPtr modified_headers;
94+
if (Http::Utility::isUpgrade(headers)) {
95+
modified_headers = std::make_unique<Http::HeaderMapImpl>(headers);
96+
transformUpgradeFromH1toH2(*modified_headers);
97+
buildHeaders(final_headers, *modified_headers);
98+
} else {
99+
buildHeaders(final_headers, headers);
100+
}
94101

95102
nghttp2_data_provider provider;
96103
if (!end_stream) {
@@ -151,6 +158,11 @@ void ConnectionImpl::StreamImpl::pendingRecvBufferLowWatermark() {
151158
readDisable(false);
152159
}
153160

161+
void ConnectionImpl::StreamImpl::decodeHeaders() {
162+
maybeTransformUpgradeFromH2ToH1();
163+
decoder_->decodeHeaders(std::move(headers_), remote_end_stream_);
164+
}
165+
154166
void ConnectionImpl::StreamImpl::pendingSendBufferHighWatermark() {
155167
ENVOY_CONN_LOG(debug, "send buffer over limit ", parent_.connection_);
156168
ASSERT(!pending_send_buffer_high_watermark_called_);
@@ -366,13 +378,13 @@ int ConnectionImpl::onFrameReceived(const nghttp2_frame* frame) {
366378
ASSERT(!stream->remote_end_stream_);
367379
stream->decoder_->decode100ContinueHeaders(std::move(stream->headers_));
368380
} else {
369-
stream->decoder_->decodeHeaders(std::move(stream->headers_), stream->remote_end_stream_);
381+
stream->decodeHeaders();
370382
}
371383
break;
372384
}
373385

374386
case NGHTTP2_HCAT_REQUEST: {
375-
stream->decoder_->decodeHeaders(std::move(stream->headers_), stream->remote_end_stream_);
387+
stream->decodeHeaders();
376388
break;
377389
}
378390

@@ -401,7 +413,7 @@ int ConnectionImpl::onFrameReceived(const nghttp2_frame* frame) {
401413
// start out with. In this case, raise as headers. nghttp2 message checking guarantees
402414
// proper flow here.
403415
ASSERT(!stream->headers_->Status() || stream->headers_->Status()->value() != "100");
404-
stream->decoder_->decodeHeaders(std::move(stream->headers_), stream->remote_end_stream_);
416+
stream->decodeHeaders();
405417
}
406418
}
407419

@@ -734,6 +746,10 @@ ConnectionImpl::Http2Options::Http2Options(const Http2Settings& http2_settings)
734746
if (http2_settings.hpack_table_size_ != NGHTTP2_DEFAULT_HEADER_TABLE_SIZE) {
735747
nghttp2_option_set_max_deflate_dynamic_table_size(options_, http2_settings.hpack_table_size_);
736748
}
749+
if (http2_settings.allow_connect_) {
750+
// TODO(alyssawilk) change to ENABLE_CONNECT_PROTOCOL when it's available.
751+
nghttp2_option_set_no_http_messaging(options_, 1);
752+
}
737753
}
738754

739755
ConnectionImpl::Http2Options::~Http2Options() { nghttp2_option_del(options_); }

source/common/http/http2/codec_impl.h

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include "common/common/logger.h"
2020
#include "common/http/codec_helper.h"
2121
#include "common/http/header_map_impl.h"
22+
#include "common/http/utility.h"
2223

2324
#include "absl/types/optional.h"
2425
#include "nghttp2/nghttp2.h"
@@ -187,6 +188,13 @@ class ConnectionImpl : public virtual Connection, protected Logger::Loggable<Log
187188
// I don't fully understand.
188189
static const uint64_t MAX_HEADER_SIZE = 63 * 1024;
189190

191+
// Does any necessary WebSocket/Upgrade conversion, then passes the headers
192+
// to the decoder_.
193+
void decodeHeaders();
194+
195+
virtual void transformUpgradeFromH1toH2(HeaderMap& headers) PURE;
196+
virtual void maybeTransformUpgradeFromH2ToH1() PURE;
197+
190198
bool buffers_overrun() const { return read_disable_count_ > 0; }
191199

192200
ConnectionImpl& parent_;
@@ -224,6 +232,16 @@ class ConnectionImpl : public virtual Connection, protected Logger::Loggable<Log
224232
// StreamImpl
225233
void submitHeaders(const std::vector<nghttp2_nv>& final_headers,
226234
nghttp2_data_provider* provider) override;
235+
void transformUpgradeFromH1toH2(HeaderMap& headers) override {
236+
upgrade_type_ = headers.Upgrade()->value().c_str();
237+
Http::Utility::transformUpgradeRequestFromH1toH2(headers);
238+
}
239+
void maybeTransformUpgradeFromH2ToH1() override {
240+
if (!upgrade_type_.empty() && headers_->Status()) {
241+
Http::Utility::transformUpgradeResponseFromH2toH1(*headers_, upgrade_type_);
242+
}
243+
}
244+
std::string upgrade_type_;
227245
};
228246

229247
/**
@@ -235,6 +253,14 @@ class ConnectionImpl : public virtual Connection, protected Logger::Loggable<Log
235253
// StreamImpl
236254
void submitHeaders(const std::vector<nghttp2_nv>& final_headers,
237255
nghttp2_data_provider* provider) override;
256+
void transformUpgradeFromH1toH2(HeaderMap& headers) override {
257+
Http::Utility::transformUpgradeResponseFromH1toH2(headers);
258+
}
259+
void maybeTransformUpgradeFromH2ToH1() override {
260+
if (Http::Utility::isH2UpgradeRequest(*headers_)) {
261+
Http::Utility::transformUpgradeRequestFromH2toH1(*headers_);
262+
}
263+
}
238264
};
239265

240266
ConnectionImpl* base() { return this; }

0 commit comments

Comments
 (0)