Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

http: downstream connect support #10720

Merged
merged 14 commits into from
Apr 20, 2020

Conversation

alyssawilk
Copy link
Contributor

@alyssawilk alyssawilk commented Apr 9, 2020

Using the upgrade path for CONNECT requests to allow proxying CONNECT payload.
This results in functionally correct proxying of CONNECT requests e2e, though it remains hidden and should not be used yet, as there are WIP improvements to security not yet landed.
Various tweaks to HTTP/1 and HTTP/2 codecs to get this working e2e
Changing the 400 for unsupported connects to 403, which seems more accurate.

Risk Level: Medium (mostly config guarded)
Testing: new unit tests, e2e tests for CONNECT proxying.
Docs Changes: in #10623, need more plus config examples
Release Notes: no
Part of #1630 #1451

Signed-off-by: Alyssa Wilk <alyssar@chromium.org>
Signed-off-by: Alyssa Wilk <alyssar@chromium.org>
Signed-off-by: Alyssa Wilk <alyssar@chromium.org>
Signed-off-by: Alyssa Wilk <alyssar@chromium.org>
@alyssawilk
Copy link
Contributor Author

I believe the clang-tidy error is unrelated - PTAL?

Copy link
Member

@mattklein123 mattklein123 left a comment

Choose a reason for hiding this comment

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

This is awesome. Very excited to see this taking shape. Generally looks great with a few small comments and questions. Thank you!

/wait

source/common/http/conn_manager_impl.cc Show resolved Hide resolved
@@ -2004,6 +2014,12 @@ bool ConnectionManagerImpl::ActiveStream::createFilterChain() {
}
bool upgrade_rejected = false;
auto upgrade = request_headers_ ? request_headers_->Upgrade() : nullptr;
// Treat CONNECT requests as a special upgrade case.
if (!upgrade && request_headers_ && request_headers_->Method() &&
Copy link
Member

Choose a reason for hiding this comment

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

Is it possible to have headers but not method?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not unless a filter removes the method for some reason, but given it's constant time I mildly prefer erring on the side of caution. I can take it out (and or revert the utility it landed in) if you 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'm fine either way. I thought this was before filters run but it's not a big deal either way.

@@ -2004,6 +2014,12 @@ bool ConnectionManagerImpl::ActiveStream::createFilterChain() {
}
bool upgrade_rejected = false;
auto upgrade = request_headers_ ? request_headers_->Upgrade() : nullptr;
Copy link
Member

Choose a reason for hiding this comment

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

nit: can you make this a non-auto type? It's a little confusing to read through and see what type this is.

source/common/http/http1/codec_impl.cc Show resolved Hide resolved
@@ -72,6 +72,7 @@ class StreamEncoderImpl : public virtual StreamEncoder,
const Network::Address::InstanceConstSharedPtr& connectionLocalAddress() override;

void isResponseToHeadRequest(bool value) { is_response_to_head_request_ = value; }
void isResponseToConnectRequest(bool value) { is_response_to_connect_request_ = 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: I would probably do s/is/set in the method name and change the head request one also but up to you.

@@ -149,6 +149,10 @@ void ConnectionImpl::ClientStreamImpl::encodeHeaders(const RequestHeaderMap& hea
upgrade_type_ = std::string(headers.Upgrade()->value().getStringView());
Http::Utility::transformUpgradeRequestFromH1toH2(*modified_headers);
buildHeaders(final_headers, *modified_headers);
} else if (headers.Method() && headers.Method()->value() == "CONNECT") {
modified_headers = createHeaderMap<RequestHeaderMapImpl>(headers);
modified_headers->setProtocol("bytestream");
Copy link
Member

Choose a reason for hiding this comment

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

nit: "bytestream" is constant in headers.h? If possible for some of the more arcane parts of this would it be also possible to comment with the RFC section to reference?

NiceMock<MockRequestDecoder> decoder;
EXPECT_CALL(callbacks_, newStream(_, _)).WillOnce(ReturnRef(decoder));

EXPECT_CALL(decoder, decodeHeaders_(_, false)).Times(1);
Copy link
Member

Choose a reason for hiding this comment

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

nit: Times(1) is redundant here and elsewhere.

EXPECT_CALL(callbacks_, newStream(_, _)).WillOnce(ReturnRef(decoder));

EXPECT_CALL(decoder, decodeHeaders_(_, false)).Times(1);
Buffer::OwnedImpl buffer("CONNECT / HTTP/1.1\r\n\r\n");
Copy link
Member

Choose a reason for hiding this comment

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

I'm not very familiar with CONNECT, but according to https://tools.ietf.org/html/rfc7231#section-4.3.6 it says that CONNECT requires the authority form. Should we be allowing a request like this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, we shouldn't. It gets by because of the if(is_connect) in ServerConnectionImpl::handlePath.
Fixing that requires fixing the URL parser to handle connect requests, and the audit of all the Path() calls in the code base. Ok with this and the updated TODO or would you prefer to land this code and the Path() work together?

Copy link
Member

Choose a reason for hiding this comment

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

I was thinking we could at least fix the codec in this PR and then put in the synthetic \ before dispatch into the HCM? WDYT?

Signed-off-by: Alyssa Wilk <alyssar@chromium.org>
@alyssawilk
Copy link
Contributor Author

Fair compromise :-)

@dio this means I'm going to have to use the is_connect field in the http parser library. Can you make sure #10599 will handle the new functionality? Happy to brainstorm options with you.

Signed-off-by: Alyssa Wilk <alyssar@chromium.org>
Signed-off-by: Alyssa Wilk <alyssar@chromium.org>
Signed-off-by: Alyssa Wilk <alyssar@chromium.org>
Copy link
Member

@mattklein123 mattklein123 left a comment

Choose a reason for hiding this comment

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

Thanks this is great. Some small comments. The codec changes are kind of scary. Is it possible to run the codec fuzzer locally a little bit to see if there are any obvious errors? I'm not sure if we should seed a CONNECT corpus? cc @asraa

/wait

@@ -760,6 +760,12 @@ void ConnectionManagerImpl::ActiveStream::decodeHeaders(RequestHeaderMapPtr&& he
connection_manager_.read_callbacks_->connection().dispatcher());
request_headers_ = std::move(headers);

// TODO(alyssawilk) remove this synthetic path in a follow-up PR, including
// auditing of empty path headers.
if (HeaderUtility::isConnect(*request_headers_) && !request_headers_->Path()) {
Copy link
Member

Choose a reason for hiding this comment

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

Isn't path guaranteed by the RFC to be empty? Should we just universally set it and ASSERT that it's empty?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

Can you comment about the h2 thing?

if (upgrade_rejected) {
// Do not allow upgrades if the route does not support it.
if (hasCachedRoute()) {
if (upgrade_rejected) { // Do not allow upgrades if the route does not support it.
Copy link
Member

Choose a reason for hiding this comment

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

nit: move comment to next line.

Copy link
Member

Choose a reason for hiding this comment

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

ping

Comment on lines 364 to 366
if (is_connect && !host) {
throw CodecClientException("Host must be specified for CONNECT requests");
} else if (!method || (!path && !is_connect)) {
Copy link
Member

Choose a reason for hiding this comment

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

This came up in @asraa's exception removal PR, but these exceptions have no purpose. I don't recall the history of why they are here but they are programming errors and will crash the server anyway, so perhaps just switch them to RELEASE_ASSERT or ASSERT now to avoid @asraa more hassle later? Up to you.

Copy link
Member

Choose a reason for hiding this comment

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

What did we decide here? Do you want to just ASSERT or RELEASE_ASSERT the new case vs. throw an exception?

@@ -101,7 +101,7 @@ namespace Utility {
*/
class Url {
public:
bool initialize(absl::string_view absolute_url);
bool initialize(absl::string_view absolute_url, bool is_connect_request = false);
Copy link
Member

Choose a reason for hiding this comment

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

nit: I would probably have this be a non-default param given how much it changes the behavior, and possibly even an enum for clarity, but up to you.

Comment on lines +1595 to +1596
// Connect with body is technically illegal, but Envoy does not inspect the
// body to see if there is a non-zero byte chunk. It will instead pass it
Copy link
Member

Choose a reason for hiding this comment

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

Should we eventually block this? TODO? WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We'd have to basically peek at connect payload and reject if it's not an empty chunk. I'm happy to TODO if you think it's worth it

Copy link
Member

Choose a reason for hiding this comment

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

I'm fine either way. I don't know enough about the RFC here to have an opinion. cc @PiotrSikora

Signed-off-by: Alyssa Wilk <alyssar@chromium.org>
Signed-off-by: Alyssa Wilk <alyssar@chromium.org>
@dio
Copy link
Member

dio commented Apr 16, 2020

@alyssawilk sure, so is_connect > 0 means to have the URL to be strictly checked. If there is no port in the URL (since CONNECT can only contain "hostname:port"), the schema will cause the http_parser_parse_url method to fail. Is that correct?

@alyssawilk
Copy link
Contributor Author

alyssawilk commented Apr 16, 2020 via email

Signed-off-by: Alyssa Wilk <alyssar@chromium.org>
Signed-off-by: Alyssa Wilk <alyssar@chromium.org>
Copy link
Member

@mattklein123 mattklein123 left a comment

Choose a reason for hiding this comment

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

Thanks, awesome, just pinging a few comments from before. Thank you!

/wait

@@ -760,6 +760,12 @@ void ConnectionManagerImpl::ActiveStream::decodeHeaders(RequestHeaderMapPtr&& he
connection_manager_.read_callbacks_->connection().dispatcher());
request_headers_ = std::move(headers);

// TODO(alyssawilk) remove this synthetic path in a follow-up PR, including
// auditing of empty path headers.
if (HeaderUtility::isConnect(*request_headers_) && !request_headers_->Path()) {
Copy link
Member

Choose a reason for hiding this comment

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

Can you comment about the h2 thing?

if (upgrade_rejected) {
// Do not allow upgrades if the route does not support it.
if (hasCachedRoute()) {
if (upgrade_rejected) { // Do not allow upgrades if the route does not support it.
Copy link
Member

Choose a reason for hiding this comment

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

ping

Comment on lines 364 to 366
if (is_connect && !host) {
throw CodecClientException("Host must be specified for CONNECT requests");
} else if (!method || (!path && !is_connect)) {
Copy link
Member

Choose a reason for hiding this comment

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

What did we decide here? Do you want to just ASSERT or RELEASE_ASSERT the new case vs. throw an exception?

Signed-off-by: Alyssa Wilk <alyssar@chromium.org>
Signed-off-by: Alyssa Wilk <alyssar@chromium.org>
@alyssawilk
Copy link
Contributor Author

CI is happy and I think I addressed comments - PTAL!

Copy link
Member

@mattklein123 mattklein123 left a comment

Choose a reason for hiding this comment

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

Nice!

@mattklein123 mattklein123 merged commit b3fe5f7 into envoyproxy:master Apr 20, 2020
alyssawilk added a commit that referenced this pull request Apr 22, 2020
This should result in all Path() calls not altered in #10720 being safe for path-less CONNECT.
The major change for this PR is that requests without a path will not be considered gRPC requests. They're still currently rejected at the HCM, but when they are allowed through they will simply not be gRPC rather than causing crashes.

Risk Level: medium (L7 code refactor)
Testing: new unit tests
Docs Changes: n/a
Release Notes: n/a
Part of #1630 #1451

Signed-off-by: Alyssa Wilk <alyssar@chromium.org>
penguingao pushed a commit to penguingao/envoy that referenced this pull request Apr 22, 2020
Signed-off-by: Alyssa Wilk <alyssar@chromium.org>
Signed-off-by: pengg <pengg@google.com>
penguingao pushed a commit to penguingao/envoy that referenced this pull request Apr 22, 2020
…oxy#10851)

This should result in all Path() calls not altered in envoyproxy#10720 being safe for path-less CONNECT.
The major change for this PR is that requests without a path will not be considered gRPC requests. They're still currently rejected at the HCM, but when they are allowed through they will simply not be gRPC rather than causing crashes.

Risk Level: medium (L7 code refactor)
Testing: new unit tests
Docs Changes: n/a
Release Notes: n/a
Part of envoyproxy#1630 envoyproxy#1451

Signed-off-by: Alyssa Wilk <alyssar@chromium.org>
Signed-off-by: pengg <pengg@google.com>
@alyssawilk alyssawilk deleted the downstream_connect branch August 27, 2020 16:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants