Skip to content

Commit

Permalink
websocket: tunneling websockets (and upgrades in general) over H2 (#4188
Browse files Browse the repository at this point in the history
)

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.
#1630

Signed-off-by: Alyssa Wilk <[email protected]>
  • Loading branch information
alyssawilk authored Aug 28, 2018
1 parent b9dc5d9 commit cd171d9
Show file tree
Hide file tree
Showing 23 changed files with 482 additions and 68 deletions.
14 changes: 14 additions & 0 deletions api/envoy/api/v2/core/protocol.proto
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,20 @@ message Http2ProtocolOptions {
// window. Currently, this has the same minimum/maximum/default as *initial_stream_window_size*.
google.protobuf.UInt32Value initial_connection_window_size = 4
[(validate.rules).uint32 = {gte: 65535, lte: 2147483647}];

// [#not-implemented-hide:] Hiding until nghttp2 has native support.
//
// Allows proxying Websocket and other upgrades over H2 connect.
//
// THIS IS NOT SAFE TO USE IN PRODUCTION
//
// This currently works via disabling all HTTP sanity checks for H2 traffic
// which is a much larger hammer than we'd like to use. Eventually when
// https://github.com/nghttp2/nghttp2/issues/1181 is resolved, this will work
// with simply enabling CONNECT for H2. This may require some tweaks to the
// headers making pre-CONNECT-support proxying not backwards compatible with
// post-CONNECT-support proxying.
bool allow_connect = 5;
}

// [#not-implemented-hide:]
Expand Down
23 changes: 23 additions & 0 deletions docs/root/intro/arch_overview/websocket.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,29 @@ one can set up custom
for the given upgrade type, up to and including only using the router filter to send the WebSocket
data upstream.

Handling H2 hops (implementation in progress)
---------------------------------------------

Envoy currently has an alpha implementation of tunneling websockets over H2 streams for deployments
that prefer a uniform H2 mesh throughout, for example, for a deployment of the form:

[Client] ---- HTTP/1.1 ---- [Front Envoy] ---- HTTP/2 ---- [Sidecar Envoy ---- H1 ---- App]

In this case, if a client is for example using WebSocket, we want the Websocket to arive at the
upstream server functionally intact, which means it needs to traverse the HTTP/2 hop.

TODO(alyssawilk) copy the warnings from the config here, or just land the docs when we unhide.

This is accomplished via
`extended CONNECT <https://tools.ietf.org/html/draft-mcmanus-httpbis-h2-websockets>`_ support. The
WebSocket request will be transformed into an HTTP/2 CONNECT stream, with :protocol header
indicating the original upgrade, traverse the HTTP/2 hop, and be downgraded back into an HTTP/1
WebSocket Upgrade. This same Upgrade-CONNECT-Upgrade transformation will be performed on any
HTTP/2 hop, with the documented flaw that the HTTP/1.1 method is always assumed to be GET.
Non-WebSocket upgrades are allowed to use any valid HTTP method (i.e. POST) and the current
upgrade/downgrade mechanism will drop the original method and transform the Upgrade request to
a GET method on the final Envoy-Upstream hop.

Old style WebSocket support
===========================

Expand Down
3 changes: 3 additions & 0 deletions include/envoy/http/codec.h
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ struct Http2Settings {
uint32_t max_concurrent_streams_{DEFAULT_MAX_CONCURRENT_STREAMS};
uint32_t initial_stream_window_size_{DEFAULT_INITIAL_STREAM_WINDOW_SIZE};
uint32_t initial_connection_window_size_{DEFAULT_INITIAL_CONNECTION_WINDOW_SIZE};
bool allow_connect_{DEFAULT_ALLOW_CONNECT};

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

/**
Expand Down
1 change: 1 addition & 0 deletions include/envoy/http/header_map.h
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ class HeaderEntry {
HEADER_FUNC(Origin) \
HEADER_FUNC(OtSpanContext) \
HEADER_FUNC(Path) \
HEADER_FUNC(Protocol) \
HEADER_FUNC(ProxyConnection) \
HEADER_FUNC(Referer) \
HEADER_FUNC(RequestId) \
Expand Down
2 changes: 1 addition & 1 deletion source/common/http/conn_manager_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -580,7 +580,7 @@ void ConnectionManagerImpl::ActiveStream::decodeHeaders(HeaderMapPtr&& headers,

// Modify the downstream remote address depending on configuration and headers.
request_info_.setDownstreamRemoteAddress(ConnectionManagerUtility::mutateRequestHeaders(
*request_headers_, protocol, connection_manager_.read_callbacks_->connection(),
*request_headers_, connection_manager_.read_callbacks_->connection(),
connection_manager_.config_, *snapped_route_config_, connection_manager_.random_generator_,
connection_manager_.runtime_, connection_manager_.local_info_));
ASSERT(request_info_.downstreamRemoteAddress() != nullptr);
Expand Down
4 changes: 2 additions & 2 deletions source/common/http/conn_manager_utility.cc
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ namespace Envoy {
namespace Http {

Network::Address::InstanceConstSharedPtr ConnectionManagerUtility::mutateRequestHeaders(
Http::HeaderMap& request_headers, Protocol protocol, Network::Connection& connection,
Http::HeaderMap& request_headers, Network::Connection& connection,
ConnectionManagerConfig& config, const Router::Config& route_config,
Runtime::RandomGenerator& random, Runtime::Loader& runtime,
const LocalInfo::LocalInfo& local_info) {
// If this is a Upgrade request, do not remove the Connection and Upgrade headers,
// as we forward them verbatim to the upstream hosts.
if (protocol == Protocol::Http11 && Utility::isUpgrade(request_headers)) {
if (Utility::isUpgrade(request_headers)) {
// The current WebSocket implementation re-uses the HTTP1 codec to send upgrade headers to
// the upstream host. This adds the "transfer-encoding: chunked" request header if the stream
// has not ended and content-length does not exist. In HTTP1.1, if transfer-encoding and
Expand Down
8 changes: 4 additions & 4 deletions source/common/http/conn_manager_utility.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ class ConnectionManagerUtility {
* existence of the x-forwarded-for header. Again see the method for more details.
*/
static Network::Address::InstanceConstSharedPtr
mutateRequestHeaders(Http::HeaderMap& request_headers, Protocol protocol,
Network::Connection& connection, ConnectionManagerConfig& config,
const Router::Config& route_config, Runtime::RandomGenerator& random,
Runtime::Loader& runtime, const LocalInfo::LocalInfo& local_info);
mutateRequestHeaders(Http::HeaderMap& request_headers, Network::Connection& connection,
ConnectionManagerConfig& config, const Router::Config& route_config,
Runtime::RandomGenerator& random, Runtime::Loader& runtime,
const LocalInfo::LocalInfo& local_info);

static void mutateResponseHeaders(Http::HeaderMap& response_headers,
const Http::HeaderMap* request_headers, const std::string& via);
Expand Down
2 changes: 2 additions & 0 deletions source/common/http/headers.h
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ class HeaderValues {
const LowerCaseString Origin{"origin"};
const LowerCaseString OtSpanContext{"x-ot-span-context"};
const LowerCaseString Path{":path"};
const LowerCaseString Protocol{":protocol"};
const LowerCaseString ProxyConnection{"proxy-connection"};
const LowerCaseString Referer{"referer"};
const LowerCaseString RequestId{"x-request-id"};
Expand Down Expand Up @@ -158,6 +159,7 @@ class HeaderValues {
} ExpectValues;

struct {
const std::string Connect{"CONNECT"};
const std::string Get{"GET"};
const std::string Head{"HEAD"};
const std::string Post{"POST"};
Expand Down
26 changes: 21 additions & 5 deletions source/common/http/http2/codec_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
#include "common/http/codes.h"
#include "common/http/exception.h"
#include "common/http/headers.h"
#include "common/http/utility.h"

namespace Envoy {
namespace Http {
Expand Down Expand Up @@ -90,7 +89,15 @@ void ConnectionImpl::StreamImpl::encode100ContinueHeaders(const HeaderMap& heade

void ConnectionImpl::StreamImpl::encodeHeaders(const HeaderMap& headers, bool end_stream) {
std::vector<nghttp2_nv> final_headers;
buildHeaders(final_headers, headers);

Http::HeaderMapPtr modified_headers;
if (Http::Utility::isUpgrade(headers)) {
modified_headers = std::make_unique<Http::HeaderMapImpl>(headers);
transformUpgradeFromH1toH2(*modified_headers);
buildHeaders(final_headers, *modified_headers);
} else {
buildHeaders(final_headers, headers);
}

nghttp2_data_provider provider;
if (!end_stream) {
Expand Down Expand Up @@ -151,6 +158,11 @@ void ConnectionImpl::StreamImpl::pendingRecvBufferLowWatermark() {
readDisable(false);
}

void ConnectionImpl::StreamImpl::decodeHeaders() {
maybeTransformUpgradeFromH2ToH1();
decoder_->decodeHeaders(std::move(headers_), remote_end_stream_);
}

void ConnectionImpl::StreamImpl::pendingSendBufferHighWatermark() {
ENVOY_CONN_LOG(debug, "send buffer over limit ", parent_.connection_);
ASSERT(!pending_send_buffer_high_watermark_called_);
Expand Down Expand Up @@ -366,13 +378,13 @@ int ConnectionImpl::onFrameReceived(const nghttp2_frame* frame) {
ASSERT(!stream->remote_end_stream_);
stream->decoder_->decode100ContinueHeaders(std::move(stream->headers_));
} else {
stream->decoder_->decodeHeaders(std::move(stream->headers_), stream->remote_end_stream_);
stream->decodeHeaders();
}
break;
}

case NGHTTP2_HCAT_REQUEST: {
stream->decoder_->decodeHeaders(std::move(stream->headers_), stream->remote_end_stream_);
stream->decodeHeaders();
break;
}

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

Expand Down Expand Up @@ -734,6 +746,10 @@ ConnectionImpl::Http2Options::Http2Options(const Http2Settings& http2_settings)
if (http2_settings.hpack_table_size_ != NGHTTP2_DEFAULT_HEADER_TABLE_SIZE) {
nghttp2_option_set_max_deflate_dynamic_table_size(options_, http2_settings.hpack_table_size_);
}
if (http2_settings.allow_connect_) {
// TODO(alyssawilk) change to ENABLE_CONNECT_PROTOCOL when it's available.
nghttp2_option_set_no_http_messaging(options_, 1);
}
}

ConnectionImpl::Http2Options::~Http2Options() { nghttp2_option_del(options_); }
Expand Down
26 changes: 26 additions & 0 deletions source/common/http/http2/codec_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include "common/common/logger.h"
#include "common/http/codec_helper.h"
#include "common/http/header_map_impl.h"
#include "common/http/utility.h"

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

// Does any necessary WebSocket/Upgrade conversion, then passes the headers
// to the decoder_.
void decodeHeaders();

virtual void transformUpgradeFromH1toH2(HeaderMap& headers) PURE;
virtual void maybeTransformUpgradeFromH2ToH1() PURE;

bool buffers_overrun() const { return read_disable_count_ > 0; }

ConnectionImpl& parent_;
Expand Down Expand Up @@ -224,6 +232,16 @@ class ConnectionImpl : public virtual Connection, protected Logger::Loggable<Log
// StreamImpl
void submitHeaders(const std::vector<nghttp2_nv>& final_headers,
nghttp2_data_provider* provider) override;
void transformUpgradeFromH1toH2(HeaderMap& headers) override {
upgrade_type_ = headers.Upgrade()->value().c_str();
Http::Utility::transformUpgradeRequestFromH1toH2(headers);
}
void maybeTransformUpgradeFromH2ToH1() override {
if (!upgrade_type_.empty() && headers_->Status()) {
Http::Utility::transformUpgradeResponseFromH2toH1(*headers_, upgrade_type_);
}
}
std::string upgrade_type_;
};

/**
Expand All @@ -235,6 +253,14 @@ class ConnectionImpl : public virtual Connection, protected Logger::Loggable<Log
// StreamImpl
void submitHeaders(const std::vector<nghttp2_nv>& final_headers,
nghttp2_data_provider* provider) override;
void transformUpgradeFromH1toH2(HeaderMap& headers) override {
Http::Utility::transformUpgradeResponseFromH1toH2(headers);
}
void maybeTransformUpgradeFromH2ToH1() override {
if (Http::Utility::isH2UpgradeRequest(*headers_)) {
Http::Utility::transformUpgradeRequestFromH2toH1(*headers_);
}
}
};

ConnectionImpl* base() { return this; }
Expand Down
51 changes: 51 additions & 0 deletions source/common/http/utility.cc
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,12 @@ bool Utility::isUpgrade(const HeaderMap& headers) {
Http::Headers::get().ConnectionValues.Upgrade.c_str()));
}

bool Utility::isH2UpgradeRequest(const HeaderMap& headers) {
return headers.Method() &&
headers.Method()->value().c_str() == Http::Headers::get().MethodValues.Connect &&
headers.Protocol() && !headers.Protocol()->value().empty();
}

bool Utility::isWebSocketUpgradeRequest(const HeaderMap& headers) {
return (isUpgrade(headers) && (0 == StringUtil::caseInsensitiveCompare(
headers.Upgrade()->value().c_str(),
Expand All @@ -227,6 +233,7 @@ Utility::parseHttp2Settings(const envoy::api::v2::core::Http2ProtocolOptions& co
ret.initial_connection_window_size_ =
PROTOBUF_GET_WRAPPED_OR_DEFAULT(config, initial_connection_window_size,
Http::Http2Settings::DEFAULT_INITIAL_CONNECTION_WINDOW_SIZE);
ret.allow_connect_ = config.allow_connect();
return ret;
}

Expand Down Expand Up @@ -398,5 +405,49 @@ std::string Utility::queryParamsToString(const QueryParams& params) {
return out;
}

void Utility::transformUpgradeRequestFromH1toH2(HeaderMap& headers) {
ASSERT(Utility::isUpgrade(headers));

const HeaderString& upgrade = headers.Upgrade()->value();
headers.insertMethod().value().setReference(Http::Headers::get().MethodValues.Connect);
headers.insertProtocol().value().setCopy(upgrade.c_str(), upgrade.size());
headers.removeUpgrade();
headers.removeConnection();
}

void Utility::transformUpgradeResponseFromH1toH2(HeaderMap& headers) {
if (getResponseStatus(headers) == 101) {
headers.insertStatus().value().setInteger(200);
}
headers.removeUpgrade();
headers.removeConnection();
}

void Utility::transformUpgradeRequestFromH2toH1(HeaderMap& headers) {
ASSERT(Utility::isH2UpgradeRequest(headers));

const HeaderString& protocol = headers.Protocol()->value();
headers.insertMethod().value().setReference(Http::Headers::get().MethodValues.Get);
headers.insertUpgrade().value().setCopy(protocol.c_str(), protocol.size());
headers.insertConnection().value().setReference(Http::Headers::get().ConnectionValues.Upgrade);
headers.removeProtocol();
if (headers.ContentLength() == nullptr) {
headers.insertTransferEncoding().value().setReference(
Http::Headers::get().TransferEncodingValues.Chunked);
}
}

void Utility::transformUpgradeResponseFromH2toH1(HeaderMap& headers, absl::string_view upgrade) {
if (getResponseStatus(headers) == 200) {
headers.insertUpgrade().value().setCopy(upgrade.data(), upgrade.size());
headers.insertConnection().value().setReference(Http::Headers::get().ConnectionValues.Upgrade);
if (headers.ContentLength() == nullptr) {
headers.insertTransferEncoding().value().setReference(
Http::Headers::get().TransferEncodingValues.Chunked);
}
headers.insertStatus().value().setInteger(101);
}
}

} // namespace Http
} // namespace Envoy
33 changes: 33 additions & 0 deletions source/common/http/utility.h
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ uint64_t getResponseStatus(const HeaderMap& headers);
*/
bool isUpgrade(const HeaderMap& headers);

/**
* @return true if this is a CONNECT request with a :protocol header present, false otherwise.
*/
bool isH2UpgradeRequest(const HeaderMap& headers);

/**
* Determine whether this is a WebSocket Upgrade request.
* This function returns true if the following HTTP headers and values are present:
Expand Down Expand Up @@ -200,6 +205,34 @@ MessagePtr prepareHeaders(const ::envoy::api::v2::core::HttpUri& http_uri);
*/
std::string queryParamsToString(const QueryParams& query_params);

/**
* Transforms the supplied headers from an HTTP/1 Upgrade request to an H2 style upgrade.
* Changes the method to connection, moves the Upgrade to a :protocol header,
* @param headers the headers to convert.
*/
void transformUpgradeRequestFromH1toH2(HeaderMap& headers);

/**
* Transforms the supplied headers from an HTTP/1 Upgrade response to an H2 style upgrade response.
* Changes the 101 upgrade response to a 200 for the CONNECT response.
* @param headers the headers to convert.
*/
void transformUpgradeResponseFromH1toH2(HeaderMap& headers);

/**
* Transforms the supplied headers from an H2 "CONNECT"-with-:protocol-header to an HTTP/1 style
* Upgrade response.
* @param headers the headers to convert.
*/
void transformUpgradeRequestFromH2toH1(HeaderMap& headers);

/**
* Transforms the supplied headers from an H2 "CONNECT success" to an HTTP/1 style Upgrade response.
* The caller is responsible for ensuring this only happens on upgraded streams.
* @param headers the headers to convert.
*/
void transformUpgradeResponseFromH2toH1(HeaderMap& headers, absl::string_view upgrade);

} // namespace Utility
} // namespace Http
} // namespace Envoy
Loading

0 comments on commit cd171d9

Please sign in to comment.