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

extensions: add grpc_http1_reverse_bridge filter #5315

Merged
merged 32 commits into from
Jan 4, 2019

Conversation

snowp
Copy link
Contributor

@snowp snowp commented Dec 15, 2018

Adds a filter that allows converting a gRPC request into an HTTP/1.1
request with a custom content-type. Allows a vanilla HTTP/1.1 upstream
to handle incoming requests by reading and responding with protobuf
messages in binary octet format.

For now this shields the upstream from any gRPC association: the filter
removes the gRPC specific message prefix and manages the conversion of
the HTTP status code into grpc-status.

Signed-off-by: Snow Pettersen [email protected]

Risk Level: Low, new optional filter
Testing: UTs and integration tests
Docs Changes: n/a for now, will add comprehensive docs
Release Notes: n/a
Fixes #5300

Adds a filter that allows converting a gRPC request into an HTTP/1.1
request with a custom content-type. Allows a vanilla HTTP/1.1 upstream
to handle incoming requests by reading and responding with protobuf
messages in binary octet format.

For now this shields the upstream from any gRPC association: the filter
removes the gRPC specific message prefix and manages the conversion of
the HTTP status code into grpc-status.

Signed-off-by: Snow Pettersen <[email protected]>
@snowp
Copy link
Contributor Author

snowp commented Dec 15, 2018

Open for suggesting on naming. grpc_shim is what we refer to it internally, but I'm not super happy about it. I wanted to call it a bridge, but with the existing bridge filter it made it rather confusing to have both a grpc_http_bridge and http_grpc_bridge filter.

We've been running a variation of this filter internally for a few months now, and it's been working just fine.

Signed-off-by: Snow Pettersen <[email protected]>
Signed-off-by: Snow Pettersen <[email protected]>
@ramaraochavali
Copy link
Contributor

Should we call this envoy.http1_grpc_bridge (or envoy.grpc_http1_reverse_bridge if it causes confusion - actually it would have been nice if the http1->gRPC was called envoy.http1_grpc_bridge which would have explained source->target conversion well) similar to how we call the http1->gRPC bridge envoy.grpc_http1_bridge?

Signed-off-by: Snow Pettersen <[email protected]>
Signed-off-by: Snow Pettersen <[email protected]>
// was unsuccessful. Since we're guaranteed at this point to have a valid response
// (unless upstream lied in content-type) we attempt to return a well-formed gRPC
// response body.
const auto length = htonl(buffer.length() + buffer_.length());
Copy link
Member

Choose a reason for hiding this comment

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

use Grpc::Encoder

// If this is a gRPC request and this is the start of DATA, remove the first 5
// bytes. These correspond to the gRPC data header.
if (enabled_ && !prefix_stripped_) {
buffer.drain(5);
Copy link
Member

Choose a reason for hiding this comment

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

This will crash with malicious downstream when buffer is < 5 bytes length.

Btw, aren't this messing up Content-Length in header?

Copy link
Contributor Author

@snowp snowp Dec 17, 2018

Choose a reason for hiding this comment

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

Ah yeah, you're right. Should be easy enough to +/- 5 the content size and to fail the request if the request body size is < 5.

EXPECT_EQ(17, buffer.length());
EXPECT_THAT(trailers, HeaderValueOf(Http::Headers::get().GrpcStatus, "0"));

char null;
Copy link
Member

Choose a reason for hiding this comment

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

use Grpc::Decoder and test against Grpc::Frame?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's so much better than my manual byte fiddling! I'll switch over that

@@ -31,6 +31,7 @@ EXTENSIONS = {
"envoy.filters.http.grpc_http1_bridge": "//source/extensions/filters/http/grpc_http1_bridge:config",
"envoy.filters.http.grpc_json_transcoder": "//source/extensions/filters/http/grpc_json_transcoder:config",
"envoy.filters.http.grpc_web": "//source/extensions/filters/http/grpc_web:config",
"envoy.filters.http.grpc_shim": "//source/extensions/filters/http/grpc_shim:config",
Copy link
Member

Choose a reason for hiding this comment

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

Also an entry in CODEOWNERS.

Copy link
Member

@lizan lizan left a comment

Choose a reason for hiding this comment

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

As this is basically doing the opposite of grpc_http1_bridge, can we have an integration test against that so make sure they interoperates?

const auto length = htonl(buffer.length() + buffer_.length());

std::array<uint8_t, Grpc::GRPC_FRAME_HEADER_SIZE> frame;
Grpc::Encoder().newFrame(Grpc::GRPC_FH_DEFAULT, htonl(length), frame);
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 need htonl here and above. newFrame takes care of that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh boy, I had just length here and it caused the tests to fail, didn't realize that I was already htonling on L146. At this point I'm just round tripping back to host 🤦‍♂️

@@ -13,6 +13,8 @@ const uint8_t GRPC_FH_DEFAULT = 0b0u;
// Last bit for a compressed message.
const uint8_t GRPC_FH_COMPRESSED = 0b1u;

const uint64_t GRPC_FRAME_HEADER_SIZE = sizeof(uint8_t) + sizeof(uint32_t);
Copy link
Member

Choose a reason for hiding this comment

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

nit: constexpr


#include "absl/strings/str_cat.h"

namespace {
Copy link
Member

Choose a reason for hiding this comment

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

you can put this inside namespace GrpcShim so you don't need to prefix everything with Envoy::


// Buffer the response in a mutable buffer: we need to determine the size of the response
// and modify it later on.
buffer_.move(buffer);
Copy link
Member

Choose a reason for hiding this comment

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

Seems this is basically buffer everything in the filter, so there will be no flow control here, would it make more sense just use content-length and pass through data without buffering?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The problem I ran into with using content-type was that large responses wouldn't have the content-type set due to being chunked.

Wondering if it makes sense to make the filter buffer mutable? That way we could use the standard buffering mechanism to get flow control but also be able to inject the frame header at the end when we know the size of it?

@lizan lizan added the waiting label Dec 17, 2018
@snowp
Copy link
Contributor Author

snowp commented Dec 18, 2018

I want to make sure they interoperate but they don't at the moment: grpc1_http_bridge doesn't manage the frame header. That said, maybe I should guard the frame header bits behind a config flag so that the default for both filters is the same, then test the interop on that. Later on I intend to make grpc1_http_bridge also optionally manage the frame header, at which point a similar interoperability test can be added.

EDIT: Actually seems like this isn't an issue, since we're going through a valid gRPC format between the filters in either case. I've added the option now so I'll just test both cases.

@snowp
Copy link
Contributor Author

snowp commented Dec 18, 2018

Unfortunately I think the two filters are not interoperable within the same filter chain at the moment, due to how addEncodedTrailers works: the trailers won't be applied to the subsequent filters until the onData chain has completed, which will never happen with the bridge filter because it buffers the data until it receives trailers.

These will still be interoperable between envoy processes (which is how I intend to use them together), since now the trailers will come in through the http2 codec which is able to trigger onTrailers while the onData chain has been stopped.

It might make sense to make this work within a filter chain (maybe addEncodedTrailers should trigger onTrailers using the dispatcher instead of relying on the filter chain to end?) but that's definitely out of the scope of this PR.

Signed-off-by: Snow Pettersen <[email protected]>
Signed-off-by: Snow Pettersen <[email protected]>
Signed-off-by: Snow Pettersen <[email protected]>
Signed-off-by: Snow Pettersen <[email protected]>
// strip the gRPC frame from the request, and add it back in to the response. This will
// hide the gRPC semantics from the upstream, allowing it to receive and respond with a
// simple binary encoded protobuf.
bool withhold_grpc_frames = 2;
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 like having this defaulted to false because it means that both the http1 bridge and this leaves the frame header alone by default.

@lizan
Copy link
Member

lizan commented Dec 18, 2018

I think you can make the interop test using two listener / HCMs within same process of Envoy, no?

@snowp
Copy link
Contributor Author

snowp commented Dec 18, 2018

Oh that's a great idea, I'll give that a go.

Copy link
Member

@lizan lizan 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 looks better modulo a few nits, and test. Are we OK with the grpc_shim name or any other suggestion?

@@ -0,0 +1,449 @@
#include <netinet/in.h>
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 needed?

@@ -0,0 +1,182 @@
#include "extensions/filters/http/grpc_shim/grpc_shim.h"

#include <netinet/in.h>
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 needed?


#include <netinet/in.h>

#include "envoy/common/platform.h"
Copy link
Member

Choose a reason for hiding this comment

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

ditto


#include "extensions/filters/http/well_known_names.h"

#include "absl/strings/str_cat.h"
Copy link
Member

Choose a reason for hiding this comment

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

not using?

@lizan lizan added the waiting label Dec 19, 2018
@snowp
Copy link
Contributor Author

snowp commented Dec 19, 2018

@lizan I'm thinking of switching to @ramaraochavali's suggestion of grpc_http1_reverse_bridge to indicate it's relationship with the other bridge. Does that sound reasonable to you?

Signed-off-by: Snow Pettersen <[email protected]>
lizan
lizan previously approved these changes Dec 27, 2018
Copy link
Member

@lizan lizan left a comment

Choose a reason for hiding this comment

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

Thanks!

/retest

@repokitteh-read-only
Copy link

🔨 rebuilding ci/circleci: asan (failed build)

🐱

Caused by: a #5315 (review) was submitted by @lizan.

see: more, trace.

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.

Can we get more complete docs for this before merging? We should add an overview page here: https://www.envoyproxy.io/docs/envoy/latest/configuration/http_filters/http_filters and probably also describe the different conversion options here https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/grpc? Thank you!

/wait

Signed-off-by: Snow Pettersen <[email protected]>
snowp added 4 commits January 2, 2019 15:58
Signed-off-by: Snow Pettersen <[email protected]>
Signed-off-by: Snow Pettersen <[email protected]>
Signed-off-by: Snow Pettersen <[email protected]>
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 for adding the extra docs. Very cool!

gRPC frame header management
----------------------------

By setting the withhold_grpc_frame option, the filer will assume that the upstream does not understand any gRPC semantics
Copy link
Member

Choose a reason for hiding this comment

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

typo "filer"

super nit: as long as you are in here again can you try to flow doc lines to ~100 col if possible? Same in other files.

@@ -4,6 +4,7 @@ Version history
1.10.0 (pending)
================
* http: added new grpc_http1_reverse_bridge filter for converting gRPC requests into HTTP/1.1 requests.
* access log: added a new flag for upstream retry count exceeded.
Copy link
Member

Choose a reason for hiding this comment

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

nit: alpha order

Signed-off-by: Snow Pettersen <[email protected]>
@mattklein123
Copy link
Member

@snowp LGTM, sorry, you lost the merge race again. Can you merge master one more time?

/wait

mattklein123
mattklein123 previously approved these changes Jan 3, 2019
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.

Thank you!

Signed-off-by: Snow Pettersen <[email protected]>
@snowp
Copy link
Contributor Author

snowp commented Jan 3, 2019

/retest

@repokitteh-read-only
Copy link

🔨 rebuilding ci/circleci: compile_time_options (failed build)

🐱

Caused by: a #5315 (comment) was created by @snowp.

see: more, trace.

@mattklein123 mattklein123 merged commit a97e138 into envoyproxy:master Jan 4, 2019
fredlas pushed a commit to fredlas/envoy that referenced this pull request Mar 5, 2019
Adds a filter that allows converting a gRPC request into an HTTP/1.1
request with a custom content-type. Allows a vanilla HTTP/1.1 upstream
to handle incoming requests by reading and responding with protobuf
messages in binary octet format.

For now this shields the upstream from any gRPC association: the filter
removes the gRPC specific message prefix and manages the conversion of
the HTTP status code into grpc-status.

Signed-off-by: Snow Pettersen <[email protected]>
Signed-off-by: Fred Douglas <[email protected]>
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.

grpc -> http/1.1 filter
4 participants