-
Notifications
You must be signed in to change notification settings - Fork 4.9k
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
Initial support for upstream HTTP/1.1 tunneling #13293
Initial support for upstream HTTP/1.1 tunneling #13293
Conversation
This is still WIP I have the following points to address:
|
Sweet, so excited to see progress on this! lmk when you want me to take a pass! |
Thx @alyssawilk! I'll let you know. I think I'll have time to continue on this tomorrow or Friday on this. I have just a question. For HTTP2 you ensured that no data is sent before the response to the CONNECT is received by the upstream #10974. From what I observed the HTTP1 flow is different and the same logic does not work. Do you confirm that? Do you have an idea where I could implement this logic? |
I think this is less about HTTP/2 and HTTP/1 and more about Enovy proxying CONNECT vs Envoy originating CONNECT. Either way I think it'd be great to fix. I think to do that we could defer onPoolReadyBase from Filter::onPoolReady and instead call it when HttpUpstream::isValidBytestreamResponse is confirmed. That could be done as a standalone PR or with this one - whatever you prefer! |
Ok, you're right. I did not look carefully enough into this. I will leave it for another PR in this case as it is not exclusively related to HTTP/1.1 tunneling. |
cb6ca56
to
5bb021d
Compare
Hey @alyssawilk, I found the time to progress a bit on this and I have two issues related to the integration tests I added so far. I would like to have your opinion on those:
|
Hm, the end stream issue is an interesting one. for waitForReset you may need to wait for a connection close with HTTP/1.1 since that's how stream resets are communicated. |
d57a897
to
cd7eb4e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looking better and better. :-)
one drive by comment but otherwise I'm going to wait for you to tell me I should do another pass. Just ping me when you're ready!
source/common/tcp_proxy/tcp_proxy.cc
Outdated
upstream_ = std::make_unique<HttpUpstream>(*upstream_callbacks_, | ||
config_->tunnelingConfig()->hostname()); | ||
if ((cluster->info()->features() & Upstream::ClusterInfo::Features::HTTP2) == 0) { | ||
// TODO(irozzo): How to detect HTTP3 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can replace the old early return-false with features() & HTTP3 != 0
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately, as we discussed offline the only feature flag I see at the moment is about HTTP2: https://github.com/envoyproxy/envoy/blob/master/include/envoy/upstream/upstream.h#L718.
Anyway, HTTP3 upstreams do not seem to be supported yet.
84fa458
to
c6800fd
Compare
I'm summarizing here the issues are blocking me at the moment. The first issue I met is that, the downstream client closing the connection (i.e. sending FIN) was not resulting in the downstream connection being closed (this is the test for this scenario). This is because with http1 upstream calling encodeData on the encoder with After discussing this with @alyssawilk, we decided not to implement half-closed semantics for the moment and to reset the encoder on doneWriting. Unfortunately this has a the side-effect of closing the upstream connection without flushing (TcpProxyUpstreamFlush integration test is revealing this issue). @mattklein123 @snowp, do you have any suggestions on how to proceed? |
Sorry can you clarify what you are asking for? What are the current problems? I see the text above about half closed but I don't think I have enough context to help without more info. |
Sure. Hope this clarifies. Description of the scenarioWhen a tcp client connect with envoy acting as a TCP proxy with http1 tunneling config, a HTTP/1.1 CONNECT request is generated and sent to the upstream. If the upstream respond with a 2xx return code the connection between the envoy and the upstream proxy is hijacked for streaming the TCP data in both directions. When the downstream client closes its end of the socket (the write end), I expect Envoy to flush all the pending data and close its write end with the upstream proxy. What is happeningThis is what happens when the downstream client finishes writing and closes it's end of the socket:
The problem is that this is not triggering the termination of the connection with the upstream. What I tried so far, as I mentioned above, is to reset the stream when What I'm asking is basically some guidance on how to approach this scenario as I'm not familiar yet with the codebase. e.g. naively I thought I could simply convey somehow the
|
Do you mean upstream? Also, in the description above when you say "close" do you mean "half close?" Can you clarify in terms of full/half close semantics what you want the behavior to be being extremely careful about using downstream and upstream accurately? |
Yeah, basically for HTTP/2 we can "end stream" by sending a FIN frame. For HTTP/1 we want to signal the stream is ended but closing the connection, and there's no way to flush-close upstream. My suggestion was that we add an allow-frame-by-connection-close to the HTTP/1.1 protocol options and if it's true, there's no content length or transfer encoding, the end stream bool would result in a Network:::Connection half close. |
+1. It's possible we don't need a config option and can do something like how I dealt witht he hystrix disable chunk encoding setting? envoy/include/envoy/http/codec.h Line 59 in d043a1d
|
Yes, you're right I meant upstream. I fixed this in the original comment.
To be as precise as I can, the diagram below represents what I expect to happen from an L4 standpoint. But what I got so far is either: |
ah yeah, that'd work too. Basically I think we want it "off by default" since we don't normally allow frame by close for requests and it'd limit the risk of the change. |
I think there 2 different things being requested here though:
I think (2) is easy and can be done the way @alyssawilk and I are suggesting (modulo some minor details). If we need to support (1) I think that will be a larger and more scary change. Is (2) sufficient for now? |
I think you have difficulty inverted. |
Quite possible. :)
True, though I wonder if we could get away with no delay initially? For supporting the delay, TCP proxy already supports a "flush write connection holder." Given that the connection pools now share a lot of code I wonder if we could just make this generic and then opt-in for this case?
Yeah, I just worry about the small details here and how many things might need to be fixed given that the HTTP code has never worked with half-close. It might just work. I'm guessing it won't. :) |
yeah, I was looking at the TCP drain code but it takes raw connection pointer stuff and I think hooking that up would be a mess |
05e3529
to
d0501f4
Compare
Signed-off-by: Iacopo Rozzo <iacopo@kubermatic.com>
Thx for your review @antoniovicente ! I think I addressed the points you raised, apart from: #13293 (comment) where I would like also to have @alyssawilk opinion. |
Signed-off-by: Iacopo Rozzo <iacopo@kubermatic.com>
@@ -26,10 +26,11 @@ static_resources: | |||
stat_prefix: tcp_stats | |||
cluster: "cluster_0" | |||
tunneling_config: | |||
hostname: host.com |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sorry, why 10002 when it's connecting to port 10001?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My idea was to show that the destination port can be different from the port used by the upstream proxy, but maybe using 443
makes more sense to be consistent with terminate_connect.yaml
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sending CONNECT foo.com:1234 to foo.com:1235 seems odd to me, but if you think it's worth explicitly testing for that how about a comment so other folks don't think it's just an off by one error :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just to be sure we are on the same page, in this scenario, the CONNECT is sent to the upstream that is 127.0.0.1:10001
targeting host.com:10002
. IMO it should be quite common to have the upstream proxy listening on a port and receiving CONNECT requests targeting a different one. What could be considered a bit odd maybe, is that the L2 proxy is receiving CONNECT host.com:10002
and it is connecting instead to www.google.com:443
. WDYT?
Signed-off-by: Iacopo Rozzo <iacopo@kubermatic.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@snowp did you want to take a pass? if not I'll merge tomorrow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good for the most part, just a couple comments
configs/configgen.py
Outdated
shutil.copy(os.path.join(SCRIPT_DIR, 'encapsulate_in_connect.yaml'), OUT_DIR) | ||
shutil.copy(os.path.join(SCRIPT_DIR, 'encapsulate_in_http2_connect.yaml'), OUT_DIR) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should the new HTTP1 example be included here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure about this, WDYT @alyssawilk ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, might as well for completeness.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
|
||
HTTP/1.1 CONNECT can be used to have TCP client connecting to its own | ||
destination passing through an HTTP proxy server (e.g. corporate proxy): | ||
|
||
[HTTP Server] --- raw HTTP --- [Upstream HTTP Proxy] --- HTTP tunneled over HTTP/1.1 --- [Envoy] --- raw HTTP --- [HTTP Client] | ||
|
||
Examples of such a set up can be found in the Envoy example config :repo:`directory <configs/>` | ||
If you run `bazel-bin/source/exe/envoy-static --config-path configs/encapsulate_in_http1_connect.yaml --base-id 1` | ||
you will be running Envoy listening for TCP traffic on port 10000 and encapsulating it in an HTTP/1.1 | ||
CONNECT addressed to an HTTP proxy running on localhost and listenig on port | ||
10001, having as a final destination host.com on port 443. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a first time reader this seems a bit confusing: which proxy are we configuring with the provided config? Both? Just the one labeled 'Envoy'?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this scenario, I made the assumption that the Upstream HTTP Proxy
is not an Envoy instance, that's why I only labeled the proxy that is tunneling the traffic with Envoy
and just provided one configuration configs/encapsulate_in_http1_connect.yaml
.
The main driver of this PR is to support use cases where the upstream HTTP proxy is supporting HTTP/1 only e.g. #11308
Do you think I should make this more clear?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having re-read this a few times i realize my confusion stems from the fact that the flow is server -> client, not client -> server, and I was originally thinking of the encapsulation flow as originating from the client.
I think I would still have this example make use of two Envoy instances (assuming we support this) and then mention that this can be used when the upstream is another proxy that does not support HTTP/2 (to justify why you would ever use HTTP/1.1). I think this would be clearer and would make the example complete, as its not relying on the reader knowing how to set up another local proxy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yep, this makes sense. If it's not a problem for you I could address this with a follow-up PR as I still have some work to do on this feature.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did this already as I had to do another change.
Also this should probably have a release note |
I thought to add a release note in a follow-up PR after address two remaining issues. Find some context here: #13293 (comment) |
Signed-off-by: Iacopo Rozzo <iacopo@kubermatic.com>
eecf55c
Signed-off-by: Iacopo Rozzo <iacopo@kubermatic.com>
Thx for reviewing @snowp, I think I addressed your comments ;-) |
@alyssawilk @antoniovicente @snowp Could you please let me know if you have any points left? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, thanks!
Commit Message: Additional Description: Risk Level: Low Testing: unit test, integration, manual testing Docs Changes: Added documentation on how to configure Envoy for tunneling TCP over HTTP/1 Release Notes: n/a (still hidden) Part of envoyproxy#11308 Signed-off-by: Iacopo Rozzo <iacopo@kubermatic.com> Signed-off-by: Qin Qin <qqin@google.com>
Signed-off-by: Iacopo Rozzo iacopo@kubermatic.com
For an explanation of how to fill out the fields, please see the relevant section
in PULL_REQUESTS.md
Commit Message:
Additional Description:
Risk Level: Low
Testing: unit test, integration, manual testing
Docs Changes: Added documentation on how to configure Envoy for tunneling TCP over HTTP/1
Release Notes: n/a (still hidden)
Fixes #11308