internal_upstream: reverse-propagate filter state across the internal listener boundary at close#45237
Conversation
|
Hi @MyUmmaGumma, 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. |
|
CC @envoyproxy/api-shepherds: Your approval is needed for changes made to |
… listener boundary at close Adds StreamSharingMayImpactPooling::SharedWithDownstreamConnectionOnClose, PassthroughState::captureReverse/mergeReverse, and IoHandle::addOnPreCloseCallback. The internal listener now copies marked filter state objects from the inner (upstream) connection back to the outer (downstream) connection at upstream close. tcp_proxy.propagateResponseHeaders/propagateResponseTrailers now set their filter state with the new flag, so TunnelResponseHeaders/Trailers captured via propagate_response_headers: true on an inner-side tcp_proxy are reverse- propagated to the outer connection without further config. Reverse propagation must not overwrite an existing name on the recipient. Refs envoyproxy#43977. Risk Level: Low (opt-in; default behavior unchanged). Testing: unit tests for filter_state, passthrough_state, and InternalSocket close. Docs Changes: tcp_proxy.proto note + changelogs/current.yaml entries. Release Notes: added under filter_state and tcp_proxy areas. Platform Specific Features: N/A. Signed-off-by: Keerti Narayan <keerti2882@gmail.com>
e35635d to
1ff2d25
Compare
|
/assign @yanjunxiang-google |
|
/assign @kyessenov |
|
@MyUmmaGumma before I going into the details of the PR, I saw #44157 is also fixing #43977 and merged. Could you please share some background about this PR and and #44157? |
Before #44157: When an upstream returned a non-2xx CONNECT response, that status was discarded on the inner-side (router) unless This PR: In the internal-listener tunneling topology (outer L7 → internal-listener pair → inner tcp_proxy → upstream CONNECT), the captured response info lives in the inner upstream-connection's filter state. The outer L7 — where the original request was received and where a follow-up consumer would act — does not see it. Filter state propagates outward ( The PRs are related to a similar issue but independent. #45237 does NOT consume #44157's new On our internal deployment the shape that this takes is that auth denials on CONNECT currently surface to the caller as 503s though the access logs on the internal listener can record the 403. With this PR and a follow up the caller can actually see the 403.
|
|
@yanjunxiang-google @kyessenov - please let me know if anything needs work/explanation. This will be a very useful fix on our internal deployments. |

internal_upstream: reverse-propagate filter state across the internal listener boundary at close
Commit Message
internal_upstream: reverse-propagate filter state across the internal listener boundary at close
Additional Description
When an internal listener is used (cluster
transport_socket: envoy.transport_sockets.internal_upstream→tunneling_encap_listener→ tcp_proxy), the userspace IoHandle pair currently propagates dynamic metadata and filter state objects forward (downstream → internal-listener stream) at connection creation viaPassthroughState::initialize/mergeInto. There is no reverse path: filter state produced on the inner side never crosses back to the outer (downstream) side.This is the first piece of a fix for #43977 — propagating a non-2xx CONNECT response status (e.g. a
tcp_proxyupstream tunnel that returns 403) from the inner side back to the downstream HCM, so operators see the real upstream status instead of a canonical 503.This change is the primitive only. It introduces:
StreamSharingMayImpactPooling::SharedWithDownstreamConnectionOnCloseenum variant, symmetric to the existingSharedWithUpstreamConnection. Marks filter state objects that should flow upstream → downstream at upstream close.FilterState::objectsSharedWithDownstreamConnectionOnClose()accessor (parent-chain walk identical toobjectsSharedWithUpstreamConnection).PassthroughState::captureReverse(FilterState&)andPassthroughState::mergeReverse(FilterState&).captureReverseis called on the inner side at connection close to harvest marked objects into the sharedPassthroughState.mergeReverseis called on the outer side at its own close to deposit them into its connection's filter state. Already-set names on the recipient are skipped (reverse propagation must not overwrite).IoHandle::addOnPreCloseCallback.IoHandleImpl::close()fires registered callbacks exactly once, before notifying the peer or tearing down the file event. This is the hook the inner side uses to callcaptureReversewhile its connection's StreamInfo is still queryable.ActiveInternalListener::newActiveConnectionregisters a pre-close callback that callscaptureReverse(connection.streamInfo().filterState()), capturing theFilterStateSharedPtrby value so the callback is safe even if it fires from the IoHandle destructor (i.e. after the Connection is gone).InternalSocket::closeSocketcallsmergeReverse(connection.streamInfo().filterState())before delegating to the basePassthroughSocket::closeSocket.TunnelingConfigHelperImpl::propagateResponseHeadersandpropagateResponseTrailersnow set their filter state objects withSharedWithDownstreamConnectionOnClose. Withpropagate_response_headers: trueon the innertcp_proxy, the captured response headers/trailers are now reverse-propagated to the downstream connection's filter state automatically.The primitive is general: any inner-side network or HTTP filter can mark a filter state object with the new flag to opt into reverse propagation.
What's not in this PR
Router::UpstreamRequest::onPoolFailure(currently it only flows throughonPoolReadyviasetUpstreamFilterState). On the failure path,streamInfo:upstreamFilterState()and%UPSTREAM_FILTER_STATE%still see nothing. That fix is intended as a separate follow-up PR — it widens the connection pool callback API and is structurally bigger than this primitive.%UPSTREAM_FILTER_STATE(envoy.tcp_proxy.propagate_response_headers:TYPED)%continues to work on the success path; the failure-path fix is in the followup.tcp_proxyitself (CONNECT semantics, reset behavior, retries).Risk Level
Low.
IoHandle::addOnPreCloseCallbackinterface is new and has zero existing registrations outside this PR's wiring; the close path's behavior is unchanged when the callback list is empty.propagateResponseHeaders/propagateResponseTrailersrequirespropagate_response_headers: true(already opt-in) to fire at all. With the option off, the call is a no-op as before. With the option on, the only observable difference is that the capturedTunnelResponseHeadersfilter state object additionally appears on the downstream connection's filter state at upstream close — operators get more information, not different information.Testing
propagate_response_headersintegration tests cover the inner-side access-log path; the change topropagateResponseHeaders/propagateResponseTrailersonly adds an opt-in sharing flag tosetDataand does not alter the data placed on the inner-side filter state.test/extensions/io_socket/user_space/io_handle_impl_test.ccexercisingcaptureReverse→mergeReversewith a marked filter state object.test/extensions/transport_sockets/internal_upstream/internal_upstream_test.ccverifyingInternalSocket::closeSocketinvokesmergeReverseexactly once and passes the right filter state.tcp_proxyCONNECT receiving a 403 from its upstream now lands aTunnelResponseHeadersfilter state object (with the propagated:status) on the downstream connection's filter state. Without the patch, the downstream connection's filter state is empty for this case.Docs Changes
api/envoy/extensions/filters/network/tcp_proxy/v3/tcp_proxy.protoupdated to note that whenpropagate_response_headers/propagate_response_trailersare true and the tcp_proxy is on the inner side of an internal-listener boundary, the resulting filter state objects also become available on the downstream (outer-side) connection's filter state at upstream close. No new config fields.Release Notes
Added under
new_featuresinchangelogs/current.yaml:And an additional entry for the generic primitive:
Platform Specific Features
N/A.
Fixes
#43977