Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0cb59af
router: adding CONNECT support
alyssawilk Apr 1, 2020
7f134d1
decapsulating *is* a word
alyssawilk Apr 2, 2020
f1fe955
sigh
alyssawilk Apr 2, 2020
6ddcb00
reviewer comments
alyssawilk Apr 6, 2020
21986a9
hidden docs
alyssawilk Apr 8, 2020
e8de80b
Merge branch 'master' into upstream_connect
alyssawilk Apr 8, 2020
0729b25
Merge branch 'master' into upstream_connect
alyssawilk Apr 9, 2020
500c4e5
Merge branch 'master' into upstream_connect
alyssawilk Apr 13, 2020
0286031
Matcher API?
alyssawilk Apr 13, 2020
771c1a6
WIP matcher API use
alyssawilk Apr 13, 2020
b83f9bd
API WIP
alyssawilk Apr 15, 2020
f56e2f0
cleanup
alyssawilk Apr 16, 2020
1f2e9e5
Merge branch 'master' into upstream_connect
alyssawilk Apr 16, 2020
9587888
Merge branch 'master' into upstream_connect
alyssawilk Apr 16, 2020
67a753d
cleanup and tests of new API
alyssawilk Apr 16, 2020
795231f
finally got the config impl test working
alyssawilk Apr 16, 2020
a847f91
default return
alyssawilk Apr 20, 2020
ac38de1
fixing build rule
alyssawilk Apr 20, 2020
9a82f12
tidier
alyssawilk Apr 20, 2020
c3193d2
Merge branch 'master' into upstream_connect
alyssawilk Apr 21, 2020
92d833d
integration tests!
alyssawilk Apr 21, 2020
4434593
Merge branch 'refs/heads/master' into upstream_connect
alyssawilk Apr 21, 2020
8363d5a
reviewer comments
alyssawilk Apr 22, 2020
394c387
Merge branch 'master' into upstream_connect
alyssawilk Apr 22, 2020
72b292a
docs rewording
alyssawilk Apr 22, 2020
e44bfbe
Merge branch 'master' into upstream_connect
alyssawilk Apr 27, 2020
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
2 changes: 1 addition & 1 deletion docs/root/intro/arch_overview/http/http.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ HTTP
http_connection_management
http_filters
http_routing
websocket
upgrades
http_proxy
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
.. _arch_overview_websocket:

WebSocket and HTTP upgrades
HTTP upgrades
===========================

Envoy Upgrade support is intended mainly for WebSocket but may be used for non-WebSocket
upgrades as well. Upgrades pass both the HTTP headers and the upgrade payload
Envoy Upgrade support is intended mainly for WebSocket and CONNECT support, but may be used for
arbitrary upgrades as well. Upgrades pass both the HTTP headers and the upgrade payload
through an HTTP filter chain. One may configure the
:ref:`upgrade_configs <envoy_api_field_config.filter.network.http_connection_manager.v2.HttpConnectionManager.upgrade_configs>`
with or without custom filter chains. If only the
:ref:`upgrade_type <envoy_api_field_config.filter.network.http_connection_manager.v2.HttpConnectionManager.UpgradeConfig.upgrade_type>`
is specified, both the upgrade headers, any request and response body, and WebSocket payload will
is specified, both the upgrade headers, any request and response body, and HTTP data payload will
pass through the default HTTP filter chain. To avoid the use of HTTP-only filters for upgrade payload,
one can set up custom
:ref:`filters <envoy_api_field_config.filter.network.http_connection_manager.v2.HttpConnectionManager.UpgradeConfig.filters>`
for the given upgrade type, up to and including only using the router filter to send the WebSocket
for the given upgrade type, up to and including only using the router filter to send the HTTP
data upstream.

Upgrades can be enabled or disabled on a :ref:`per-route <envoy_api_field_route.RouteAction.upgrade_configs>` basis.
Expand All @@ -32,12 +32,12 @@ laid out below, but custom filter chains can only be configured on a per-HttpCon
| F | F | F |
+-----------------------+-------------------------+-------------------+

Note that the statistics for upgrades are all bundled together so WebSocket
Note that the statistics for upgrades are all bundled together so WebSocket and other upgrades
:ref:`statistics <config_http_conn_man_stats>` are tracked by stats such as
downstream_cx_upgrades_total and downstream_cx_upgrades_active

Handling HTTP/2 hops
^^^^^^^^^^^^^^^^^^^^
Websocket over HTTP/2 hops
^^^^^^^^^^^^^^^^^^^^^^^^^^

While HTTP/2 support for WebSockets is off by default, Envoy does support tunneling WebSockets over
HTTP/2 streams for deployments that prefer a uniform HTTP/2 mesh throughout; this enables, for example,
Expand All @@ -61,3 +61,31 @@ a GET method on the final Envoy-Upstream hop.

Note that the HTTP/2 upgrade path has very strict HTTP/1.1 compliance, so will not proxy WebSocket
upgrade requests or responses with bodies.

.. TODO(alyssawilk) unhide this when unhiding config
.. CONNECT support
.. ^^^^^^^^^^^^^^^

.. Envoy CONNECT support is off by default (Envoy will send an internally generated 403 in response to
.. CONNECT requests). CONNECT support can be enabled via the upgrade options described above, setting
.. the upgrade value to the special keyword "CONNECT".

.. While for HTTP/2, CONNECT request may have a path, in general and for HTTP/1.1 CONNECT requests do
.. not have a path, and can only be matched using a
.. :ref:`connect_matcher <envoy_api_field_route.RouteMatch.connect_matcher>`
..
.. Envoy can handle CONNECT in one of two ways, either proxying the CONNECT headers through as if they
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For my own understanding, if we proxy CONNECT through, this is really no different from WebSocket, is that right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, in that case it's just a blind upgrade where we proxy payload. Want me to comment something to that effect?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From reading this it sounds to me like if we proxy CONNECT we don't do anything special and just treat it like any other request? I think it could be clearer about what happens after the CONNECT is proxied/established.

I think it might also be useful to explain whether we support terminating the CONNECT and dropping down to L4 proxying. I don't think we do (?) but I think it's a use case people might be interested in and it might be worth calling out what our support is there if we don't already.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think some clarifications would be helpful to explain the differences between WS/CONNECT and termination vs. pass through. With this change terminating WS would be pretty trivial so that could be a future enhancement for someone (might be worth tracking in an issue).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I may have been steeped in the code too long but I'm not sure how I could make this more clear, but I think it's clear from Snow's comment that it's not as clear as I want it to be.

For connect we can basically treat it like normal L7 request, forwarding request headers and payload through, or we can terminate, strip connect headers, and forward the CONNECT payload on a raw TCP connection. I don't know how to explain that more clearly than I am doing so - I'll try for a little rewording in my next push but I'd totally appreciate any help making it clear what the options are.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is fine and we can ship and iterate.

.. were any other request, and letting the upstream terminate the CONNECT request, or by terminating the
.. CONNECT request, and forwarding the payload as raw TCP data. When CONNECT upgrade configuration is
.. set up, the default behavior is to proxy the CONNECT request, treating it like any other request using
.. the upgrade path.
.. If termination is desired, this can be accomplished by setting
.. :ref:`connect_config <envoy_api_field_config.filter.network.http_connection_manager.v2.HttpConnectionManager.UpgradeConfig.connect_config>`
.. If it that message is present for CONNECT requests, the router filter will strip the request headers,
.. and forward the HTTP payload upstream. On receipt of initial TCP data from upstream, the router
.. will synthesize 200 response headers, and then forward the TCP data as the HTTP response body.

.. .. warning::
.. This mode of CONNECT support can create major security holes if configured correctly, as the upstream
.. will be forwarded *unsanitized* headers if they are in the body payload. Please use with caution

63 changes: 49 additions & 14 deletions source/common/router/router.cc
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,16 @@ bool convertRequestHeadersForInternalRedirect(Http::RequestHeaderMap& downstream

constexpr uint64_t TimeoutPrecisionFactor = 100;

Http::ConnectionPool::Instance*
httpPool(absl::variant<Http::ConnectionPool::Instance*, Tcp::ConnectionPool::Instance*> pool) {
return absl::get<Http::ConnectionPool::Instance*>(pool);
}

Tcp::ConnectionPool::Instance*
tcpPool(absl::variant<Http::ConnectionPool::Instance*, Tcp::ConnectionPool::Instance*> pool) {
return absl::get<Tcp::ConnectionPool::Instance*>(pool);
}

const absl::string_view getPath(const Http::RequestHeaderMap& headers) {
return headers.Path() ? headers.Path()->value().getStringView() : "";
}
Expand Down Expand Up @@ -549,12 +559,10 @@ Http::FilterHeadersStatus Filter::decodeHeaders(Http::RequestHeaderMap& headers,
}
}

Http::ConnectionPool::Instance* http_pool = getHttpConnPool();
Upstream::HostDescriptionConstSharedPtr host;
Filter::HttpOrTcpPool conn_pool = createConnPool(host);

if (http_pool) {
host = http_pool->host();
} else {
if (!host) {
sendNoHealthyUpstreamResponse();
return Http::FilterHeadersStatus::StopIteration;
}
Expand Down Expand Up @@ -644,8 +652,7 @@ Http::FilterHeadersStatus Filter::decodeHeaders(Http::RequestHeaderMap& headers,
// Hang onto the modify_headers function for later use in handling upstream responses.
modify_headers_ = modify_headers;

UpstreamRequestPtr upstream_request =
std::make_unique<UpstreamRequest>(*this, std::make_unique<HttpConnPool>(*http_pool));
UpstreamRequestPtr upstream_request = createUpstreamRequest(conn_pool);
upstream_request->moveIntoList(std::move(upstream_request), upstream_requests_);
upstream_requests_.front()->encodeHeaders(end_stream);
if (end_stream) {
Expand All @@ -655,6 +662,38 @@ Http::FilterHeadersStatus Filter::decodeHeaders(Http::RequestHeaderMap& headers,
return Http::FilterHeadersStatus::StopIteration;
}

Filter::HttpOrTcpPool Filter::createConnPool(Upstream::HostDescriptionConstSharedPtr& host) {
Filter::HttpOrTcpPool conn_pool;
bool should_tcp_proxy = route_entry_->connectConfig().has_value() &&
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: const

downstream_headers_->Method()->value().getStringView() ==
Http::Headers::get().MethodValues.Connect;

if (!should_tcp_proxy) {
conn_pool = getHttpConnPool();
if (httpPool(conn_pool)) {
host = httpPool(conn_pool)->host();
}
} else {
transport_socket_options_ = Network::TransportSocketOptionsUtility::fromFilterState(
*callbacks_->streamInfo().filterState());
conn_pool = config_.cm_.tcpConnPoolForCluster(route_entry_->clusterName(),
Upstream::ResourcePriority::Default, this);
if (tcpPool(conn_pool)) {
host = tcpPool(conn_pool)->host();
}
}
return conn_pool;
}

UpstreamRequestPtr Filter::createUpstreamRequest(Filter::HttpOrTcpPool conn_pool) {
if (absl::holds_alternative<Http::ConnectionPool::Instance*>(conn_pool)) {
return std::make_unique<UpstreamRequest>(*this,
std::make_unique<HttpConnPool>(*httpPool(conn_pool)));
}
return std::make_unique<UpstreamRequest>(*this,
std::make_unique<TcpConnPool>(tcpPool(conn_pool)));
}

Http::ConnectionPool::Instance* Filter::getHttpConnPool() {
// Choose protocol based on cluster configuration and downstream connection
// Note: Cluster may downgrade HTTP2 to HTTP1 based on runtime configuration.
Expand Down Expand Up @@ -1454,19 +1493,15 @@ void Filter::doRetry() {
attempt_count_++;
ASSERT(pending_retries_ > 0);
pending_retries_--;
UpstreamRequestPtr upstream_request;

Http::ConnectionPool::Instance* conn_pool = getHttpConnPool();
if (conn_pool) {
upstream_request =
std::make_unique<UpstreamRequest>(*this, std::make_unique<HttpConnPool>(*conn_pool));
}

if (!upstream_request) {
Upstream::HostDescriptionConstSharedPtr host;
Filter::HttpOrTcpPool conn_pool = createConnPool(host);
if (!host) {
sendNoHealthyUpstreamResponse();
cleanup();
return;
}
UpstreamRequestPtr upstream_request = createUpstreamRequest(conn_pool);

if (include_attempt_count_in_request_) {
downstream_headers_->setEnvoyAttemptCount(attempt_count_);
Expand Down
6 changes: 6 additions & 0 deletions source/common/router/router.h
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,12 @@ class Filter : Logger::Loggable<Logger::Id::router>,
const Upstream::ClusterInfo& cluster, const VirtualCluster* vcluster,
Runtime::Loader& runtime, Runtime::RandomGenerator& random,
Event::Dispatcher& dispatcher, Upstream::ResourcePriority priority) PURE;

using HttpOrTcpPool =
absl::variant<Http::ConnectionPool::Instance*, Tcp::ConnectionPool::Instance*>;
HttpOrTcpPool createConnPool(Upstream::HostDescriptionConstSharedPtr& host);
UpstreamRequestPtr createUpstreamRequest(Filter::HttpOrTcpPool conn_pool);

Http::ConnectionPool::Instance* getHttpConnPool();
void maybeDoShadowing();
bool maybeRetryReset(Http::StreamResetReason reset_reason, UpstreamRequest& upstream_request);
Expand Down
73 changes: 73 additions & 0 deletions source/common/router/upstream_request.cc
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,16 @@ void HttpConnPool::newStream(GenericConnectionPoolCallbacks* callbacks) {
}
}

void TcpConnPool::onPoolReady(Tcp::ConnectionPool::ConnectionDataPtr&& conn_data,
Upstream::HostDescriptionConstSharedPtr host) {
upstream_handle_ = nullptr;
Network::Connection& latched_conn = conn_data->connection();
auto upstream =
std::make_unique<TcpUpstream>(callbacks_->upstreamRequest(), std::move(conn_data));
callbacks_->onPoolReady(std::move(upstream), host, latched_conn.localAddress(),
latched_conn.streamInfo());
}

bool HttpConnPool::cancelAnyPendingRequest() {
if (conn_pool_stream_handle_) {
conn_pool_stream_handle_->cancel();
Expand All @@ -516,5 +526,68 @@ void HttpConnPool::onPoolReady(Http::RequestEncoder& request_encoder,
request_encoder.getStream().connectionLocalAddress(), info);
}

TcpUpstream::TcpUpstream(UpstreamRequest* upstream_request,
Tcp::ConnectionPool::ConnectionDataPtr&& upstream)
: upstream_request_(upstream_request), upstream_conn_data_(std::move(upstream)) {
upstream_conn_data_->connection().enableHalfClose(true);
upstream_conn_data_->addUpstreamCallbacks(*this);
}

void TcpUpstream::encodeData(Buffer::Instance& data, bool end_stream) {
upstream_conn_data_->connection().write(data, end_stream);
}

void TcpUpstream::encodeHeaders(const Http::RequestHeaderMap&, bool end_stream) {
if (end_stream) {
Buffer::OwnedImpl data;
upstream_conn_data_->connection().write(data, true);
}
}

void TcpUpstream::encodeTrailers(const Http::RequestTrailerMap&) {
Buffer::OwnedImpl data;
upstream_conn_data_->connection().write(data, true);
}

void TcpUpstream::readDisable(bool disable) {
if (upstream_conn_data_->connection().state() != Network::Connection::State::Open) {
return;
}
upstream_conn_data_->connection().readDisable(disable);
}

void TcpUpstream::resetStream() {
upstream_request_ = nullptr;
upstream_conn_data_->connection().close(Network::ConnectionCloseType::NoFlush);
}

void TcpUpstream::onUpstreamData(Buffer::Instance& data, bool end_stream) {
if (!sent_headers_) {
Http::ResponseHeaderMapPtr headers{
Http::createHeaderMap<Http::ResponseHeaderMapImpl>({{Http::Headers::get().Status, "200"}})};
upstream_request_->decodeHeaders(std::move(headers), false);
sent_headers_ = true;
}
upstream_request_->decodeData(data, end_stream);
}

void TcpUpstream::onEvent(Network::ConnectionEvent event) {
if (event != Network::ConnectionEvent::Connected && upstream_request_) {
upstream_request_->onResetStream(Http::StreamResetReason::ConnectionTermination, "");
}
}

void TcpUpstream::onAboveWriteBufferHighWatermark() {
if (upstream_request_) {
upstream_request_->disableDataFromDownstreamForFlowControl();
}
}

void TcpUpstream::onBelowWriteBufferLowWatermark() {
if (upstream_request_) {
upstream_request_->enableDataFromDownstreamForFlowControl();
}
}

} // namespace Router
} // namespace Envoy
59 changes: 59 additions & 0 deletions source/common/router/upstream_request.h
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,41 @@ class HttpConnPool : public GenericConnPool, public Http::ConnectionPool::Callba
GenericConnectionPoolCallbacks* callbacks_{};
};

class TcpConnPool : public GenericConnPool, public Tcp::ConnectionPool::Callbacks {
public:
TcpConnPool(Tcp::ConnectionPool::Instance* conn_pool) : conn_pool_(conn_pool) {}

void newStream(GenericConnectionPoolCallbacks* callbacks) override {
callbacks_ = callbacks;
upstream_handle_ = conn_pool_->newConnection(*this);
}

bool cancelAnyPendingRequest() override {
if (upstream_handle_) {
upstream_handle_->cancel(Tcp::ConnectionPool::CancelPolicy::Default);
upstream_handle_ = nullptr;
return true;
}
return false;
}
absl::optional<Http::Protocol> protocol() const override { return absl::nullopt; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this used for logging? Should we populate this? I guess there is the question of upstream access logging for terminated connect requests? Maybe a TODO for this? cc @kyessenov

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, it goes in the streaminfo. IMO if we're doing raw L4 proxying upstream the HTTP based protocol should not be set, since we're not doing HTTP/1 or HTTP/2. Thoughts?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would recommend briefly chatting with @kyessenov about this. He has been trying to normalize all of this so we support other protocols including TCP, etc. so I think we should probably have some indication in the logs of what is happening here. Definitely fine to TODO/issue after you determine the best future course of action.


// Tcp::ConnectionPool::Callbacks
void onPoolFailure(ConnectionPool::PoolFailureReason reason,
Upstream::HostDescriptionConstSharedPtr host) override {
upstream_handle_ = nullptr;
callbacks_->onPoolFailure(reason, "", host);
}

void onPoolReady(Tcp::ConnectionPool::ConnectionDataPtr&& conn_data,
Upstream::HostDescriptionConstSharedPtr host) override;

private:
Tcp::ConnectionPool::Instance* conn_pool_;
Tcp::ConnectionPool::Cancellable* upstream_handle_{};
GenericConnectionPoolCallbacks* callbacks_{};
};

// A generic API which covers common functionality between HTTP and TCP upstreams.
class GenericUpstream {
public:
Expand Down Expand Up @@ -261,5 +296,29 @@ class HttpUpstream : public GenericUpstream, public Http::StreamCallbacks {
Http::RequestEncoder* request_encoder_{};
};

class TcpUpstream : public GenericUpstream, public Tcp::ConnectionPool::UpstreamCallbacks {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which timeouts come into play with a CONNECT/WS session? I guess the downstream idle timeouts? Anything else? I think we will need to make sure that #10531 works with non-pass through CONNECT also? WDYT? cc @Shikugawa

I haven't reviewed the tests yet but it would be great to make sure we have good integration coverage over all of the various timeout scenarios.

Copy link
Contributor Author

@alyssawilk alyssawilk Apr 22, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All HTTP timeouts may apply - we're 100% reusing the classes which do timeout logic.
Presumably one would want larger (or no) overall timeouts on WS / CONNECT routes.
Given the code reuse I did one timeout test to make sure that disconnects worked. I can do the full suite if you'd prefer?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you have to do them all I just wanted to make sure we are covered. @Shikugawa can make sure to cover CONNECT when the linked PR is finished for idle timeouts.

public:
TcpUpstream(UpstreamRequest* upstream_request, Tcp::ConnectionPool::ConnectionDataPtr&& upstream);

// GenericUpstream
void encodeData(Buffer::Instance& data, bool end_stream) override;
void encodeMetadata(const Http::MetadataMapVector&) override {}
void encodeHeaders(const Http::RequestHeaderMap&, bool end_stream) override;
void encodeTrailers(const Http::RequestTrailerMap&) override;
void readDisable(bool disable) override;
void resetStream() override;

// Tcp::ConnectionPool::UpstreamCallbacks
void onUpstreamData(Buffer::Instance& data, bool end_stream) override;
void onEvent(Network::ConnectionEvent event) override;
void onAboveWriteBufferHighWatermark() override;
void onBelowWriteBufferLowWatermark() override;

private:
UpstreamRequest* upstream_request_;
Tcp::ConnectionPool::ConnectionDataPtr upstream_conn_data_;
bool sent_headers_{};
};

} // namespace Router
} // namespace Envoy
20 changes: 20 additions & 0 deletions test/common/router/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,26 @@ envoy_cc_test(
],
)

envoy_cc_test(
name = "upstream_request_test",
srcs = ["upstream_request_test.cc"],
deps = [
"//source/common/buffer:buffer_lib",
"//source/common/router:router_lib",
"//source/common/upstream:upstream_includes",
"//source/common/upstream:upstream_lib",
"//test/common/http:common_lib",
"//test/mocks/http:http_mocks",
"//test/mocks/network:network_mocks",
"//test/mocks/router:router_mocks",
"//test/mocks/server:server_mocks",
"//test/mocks/upstream:upstream_mocks",
"//test/test_common:environment_lib",
"//test/test_common:simulated_time_system_lib",
"//test/test_common:utility_lib",
],
)

envoy_cc_test(
name = "header_formatter_test",
srcs = ["header_formatter_test.cc"],
Expand Down
Loading