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

aysnc_client: use proper scheme header #37588

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

meierfra-ergon
Copy link

@meierfra-ergon meierfra-ergon commented Dec 9, 2024

Commit Message:
derive scheme header of async client request from upstream config instead of non-existing downstream.

Additional Description:
I saw that the "router decoding headers:" log message in router.cc stated that the :scheme header is always 'http' regardless of the upstream protocol used.
The reason for this is, the stream_info of a async client request does not have a downstream and the :scheme header is by default derived from the downstream (FilterUtility::setUpstreamScheme() call in in router.cc Filter::decodeHeaders() is called with use_upstream=false since callbacks_->streamInfo().shouldSchemeMatchUpstream() is false).

The Fix for this is to set setShouldSchemeMatchUpstream(true) on the async clients stream_info. Then setUpstreamScheme() call will derive the :scheme header from the upstream config.

I`m not sure if this is a real problem, since the async client by default uses http11 and the :scheme header is not sent to the server. Perhaps if the server triggers an http2 upgrade this might be an issue.

Risk Level: low

Testing:

  • Manual testing (with JWT Authentication with JwtProvider remote_jwks endpoint configured)
  • AsyncClientImplTest::UpstreamTlsScheme test added

Docs Changes: -

Release Notes: Set async client :scheme header depending on upstream protocol

Platform Specific Features: -

Copy link

Hi @meierfra-ergon, welcome and thank you for your contribution.

We will try to review your Pull Request as quickly as possible.

In the meantime, please take a look at the contribution guidelines if you have not done so already.

🐱

Caused by: #37588 was opened by meierfra-ergon.

see: more, trace.

@meierfra-ergon meierfra-ergon force-pushed the fix_async_client_upstream_scheme_header branch from d0bac9b to b834c02 Compare December 10, 2024 13:29
wbpcode
wbpcode previously approved these changes Dec 11, 2024
@wbpcode
Copy link
Member

wbpcode commented Dec 11, 2024

considering the async client request has no downstream, it is reasonable to always use the upstream protocol to set the schema.

@wbpcode
Copy link
Member

wbpcode commented Dec 11, 2024

cc @adisuissa because @adisuissa merged the shouldSchemeMatchUpstream() feature.

@adisuissa
Copy link
Contributor

My main concern is that this change seems to be impacting all HTTP async clients, and I'm not sure this is the intended outcome of this PR.
Question: should this be configurable for an async client?
If it doesn't need to be configurable, and seems more like a bug fix, I suggest having this under a runtime-guard as it may impact current behavior in an unexpected way.

@meierfra-ergon
Copy link
Author

In my opinion this is a bug fix and the scheme should not be configurable. The :scheme pseudo header is directly tied to the scheme of the URI which defines if 'https' or 'http' is being used. So I cannot imagine someone wants to configure this. I even speculate, that a HTTP/2 requests might fail if this :scheme pseudo header is sett to 'http' while the transport is https. I only used the asysnc client with HTTP/1.1 where it does not matter, since the pseudo header is not included in the request.
If you agree I can add a runtime-guard, but to be honest I can not imagine, someone is relying on the behavior that is IMHO plain wrong. But of course you never know for sure.

@adisuissa
Copy link
Contributor

In my opinion this is a bug fix and the scheme should not be configurable. The :scheme pseudo header is directly tied to the scheme of the URI which defines if 'https' or 'http' is being used. So I cannot imagine someone wants to configure this. I even speculate, that a HTTP/2 requests might fail if this :scheme pseudo header is sett to 'http' while the transport is https. I only used the asysnc client with HTTP/1.1 where it does not matter, since the pseudo header is not included in the request.

I tend to agree with your view here.

If you agree I can add a runtime-guard, but to be honest I can not imagine, someone is relying on the behavior that is IMHO plain wrong. But of course you never know for sure.

The intention of the runtime guard is to let users migrate "slowly", i.e., in case one finds that it breaks them, they can disable the change, and ensure that the upstreams behave correctly, before turning the flag on again. I think we had once a case where something that we thought was a bug-fix was broken due to a change, and then the flag was changed to a config-knob.

@adisuissa
Copy link
Contributor

Please also add a release note.

Copy link

CC @envoyproxy/runtime-guard-changes: FYI only for changes made to (source/common/runtime/runtime_features.cc).

🐱

Caused by: #37588 was synchronize by meierfra-ergon.

see: more, trace.

@meierfra-ergon meierfra-ergon force-pushed the fix_async_client_upstream_scheme_header branch from f49767f to 2d1e5bb Compare December 13, 2024 16:59
@meierfra-ergon
Copy link
Author

ok, I added a runtime guard and a changelog entry. I hope I`ve done it correctly.

On another note, I was able to 'force' the jwks fetch request (remote_jwks) to use http/2 (by setting http2_protocol_options: {} in the cluster config). With wireshark I could prove the :scheme header really is wrong. My test server (apache) did process the request without a problem. But I can imagine some other servers might not accept a http/2 request like this.

adisuissa
adisuissa previously approved these changes Dec 17, 2024
Copy link
Contributor

@adisuissa adisuissa left a comment

Choose a reason for hiding this comment

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

LGTM, thanks!
Assigning @yanjunxiang-google who recently worked on this domain, and @yanavlasov for senior-maintainer pass.
/assign @yanjunxiang-google @yanavlasov

@meierfra-ergon meierfra-ergon force-pushed the fix_async_client_upstream_scheme_header branch 2 times, most recently from 1b964b6 to 845b015 Compare January 6, 2025 16:41
@meierfra-ergon meierfra-ergon force-pushed the fix_async_client_upstream_scheme_header branch from 845b015 to 6633f4d Compare January 6, 2025 17:45
Copy link
Contributor

@adisuissa adisuissa left a comment

Choose a reason for hiding this comment

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

LGTM, thanks!
/assign-from @envoyproxy/senior-maintainers

@@ -2405,5 +2409,34 @@ TEST_F(AsyncClientImplUnitTest, NullVirtualHost) {
EXPECT_EQ(std::numeric_limits<uint32_t>::max(), vhost_.retryShadowBufferLimit());
}

TEST_F(AsyncClientImplTest, UpstreamTlsScheme) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Please add a one-line comment describing what this test is validating.

Copy link

@envoyproxy/senior-maintainers assignee is @alyssawilk

🐱

Caused by: a #37588 (review) was submitted by @adisuissa.

see: more, trace.

@yanjunxiang-google
Copy link
Contributor

LGTM

@@ -139,6 +139,9 @@ AsyncStreamImpl::AsyncStreamImpl(AsyncClientImpl& parent, AsyncClient::StreamCal
route_ = std::move(*route_or_error);
stream_info_.dynamicMetadata().MergeFrom(options.metadata);
stream_info_.setIsShadow(options.is_shadow);
if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.async_client_scheme_header_fix")) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this makes sense for some (all?) envoy-initiated requests but I'm pretty sure we don't want to do this unilaterally.

For example, if an edge proxy is getting https requests from a user, but happens to not use TLS internally, the main request will retain the https scheme and shadowed requests will as well. I don't think we want the shadow/mirror request to have a different scheme than the primary.

What use case are you trying to address? Maybe this could be a configurable option for async streams and we can set it at those call sites?

Copy link
Author

@meierfra-ergon meierfra-ergon Jan 8, 2025

Choose a reason for hiding this comment

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

I'm not sure, how the shadowed requests are implemented exactly, but if they just use this AsyncClientImpl, then (without my patch) the ':scheme' header will always be 'http' (since there is no downstream reference in the stream_info (see PR description for explanation)). If I`m correct, this will also lead to the behavior that the :scheme header of the shadow request is not the same as the main request (if the main request uses TLS).

In my understanding the :scheme header value is tightly bound to connection of the performed request. If the request is proxied from a 'http' downstream to a 'https' upstream (or vice versa), it is correct that the header is changed, since it belongs to the connection. So I would argue this should also be the case for mirroring. But I see this could be argued differently.

In my "use-case", in a setup with jwt_authn filter and remote_jwks configured, the request fetching the jwks should have the correct :scheme header set. This is not the case when the request is using http/2 (with TLS). I think the same issue exists with other filters using the async client like the oauth2 filter.

In general I think, this this PR changes a hard-coded default (scheme header always 'http') to something adaptive to the (upstream) environment. I think this is a better default. If in some cases the :scheme header should be set to another value than that default I suggest a separate feature.

In any case if this change breaks anything, the runtime guard is still available to disable the new behavior.

Copy link
Author

@meierfra-ergon meierfra-ergon Jan 8, 2025

Choose a reason for hiding this comment

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

I have to revise some of my points of the former comment:
If a Http::RequestMessagePtr given to the httpAsyncClientImpl::send() includes a :scheme header, then the headers value is used in the request, otherwise the default is 'http' (due to non-existing downstream). With my patch an existing :scheme` header is ignored, since the upstream scheme is enforced, what could be problematic change in behavior :-/.

I checked shadowWriterImpl::shadow() where a RequestMessagePtr argument is taken.

void ShadowWriterImpl::shadow(const std::string& cluster, Http::RequestMessagePtr&& request,

So if this holds the original RequestMessage including a :scheme header (which is probable), the resulting shadow request will have the same :scheme header. For me the question remains if this is correct. But I see, that my PR introduces a change I have not anticipated.

Copy link
Author

Choose a reason for hiding this comment

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

A different approach for 'my use-case' would be to not derive the ':scheme' header from the upstream cluster but extract the scheme from the uri.
In case of the jwks fetcher (JwksFetcherImpl::fetch()) the request message is prepared with the remote_jwks.uri

Http::RequestMessagePtr message = Http::Utility::prepareHeaders(remote_jwks_.http_uri());

unfortunately prepareHeaders() only sets :host and :path if there the :scheme also would be set, my problem would also be resolved I guess. For me the question remains if the source of the :scheme header primary be the upstream protocol (from the cluster config) or should it be set by other means.

Please let me know if you would prefer an approach in this direction. Or if the original approach is ok or should be refined. Or if you see a completely different approach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants