[proxy]: Resolve upstreams per request from namespace and metadata#81
Merged
Conversation
There was a problem hiding this comment.
Pull request overview
This PR extends the proxy’s upstream-routing model to support per-request upstream resolution (e.g., per-namespace/per-metadata) by introducing an internal namespace-metadata contract, a per-call resolving grpc.ClientConnInterface, and a connection pool keyed by a logical cache key (not just the dial target). This enables templated upstream host/port and TLS server name resolution without re-parsing request payloads.
Changes:
- Added
internal/transport/metato stamp/extract the router-derived namespace via internal gRPC metadata and wired the router to stamp it on forwarded calls. - Introduced
internal/transport/connect.Conn+Resolverabstraction and updated the connectionPoolto key by logical key (supporting multiple conns to the same target with different TLS identities). - Added
internal/proxy.DynamicResolverto render hostPort/serverName per request from namespace + outgoing metadata, and updated proxy fx wiring/server construction accordingly (including splittingListenfromStart).
Reviewed changes
Copilot reviewed 23 out of 23 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/transport/meta/meta.go | Defines internal namespace metadata header and helpers to set/get it on outgoing contexts. |
| internal/transport/meta/meta_test.go | Adds unit tests for namespace header stamping and overwrite behavior. |
| internal/transport/creds/tls.go | Adds client TLS server-name override support (SNI + verification). |
| internal/transport/creds/tls_test.go | Expands tests to cover client TLS with/without server name. |
| internal/transport/connect/pool.go | Changes pool keying from host to logical key and adds ConnOrCreate(key,target,...). |
| internal/transport/connect/pool_test.go | Updates/extends pool tests for new key/target semantics and concurrency behavior. |
| internal/transport/connect/doc.go | Updates package docs and introduces the per-call resolving Conn concept. |
| internal/transport/connect/conn.go | Adds Conn wrapper implementing grpc.ClientConnInterface with per-call resolution. |
| internal/transport/connect/conn_test.go | Adds tests for static vs dynamic resolver behavior and error propagation. |
| internal/router/handler.go | Stamps extracted namespace into outgoing metadata before forwarding upstream. |
| internal/router/handler_test.go | Adds an integration-style test proving spoofed namespace headers are overwritten. |
| internal/router/fx.go | Updates upstream socket dialing to use Pool.ConnOrCreate. |
| internal/proxy/server.go | Refactors proxy server to accept a grpc.ClientConnInterface, and splits Listen from Start. |
| internal/proxy/server_test.go | Updates tests for new New(hostPort, cc) API and Listen/Start split. |
| internal/proxy/translation_test.go | Adapts translation tests to new proxy construction and listener lifecycle. |
| internal/proxy/resolver.go | Adds DynamicResolver for per-request hostPort/serverName + dial option resolution. |
| internal/proxy/resolver_test.go | Adds tests for template parsing, metadata behavior, invalid addresses, and option-factory errors. |
| internal/proxy/fx.go | Wires pool-backed resolving conns into upstream proxies; adds resolver selection logic. |
| internal/proxy/fx_test.go | Updates fx tests to accept templated upstreams and requires connect.Module. |
| internal/config/upstream.go | Treats templated TLS server name as making the upstream templated. |
| internal/config/config_test.go | Extends IsTemplated tests to cover templated/static TLS server name behavior. |
| e2e/templated_upstream_test.go | Adds an e2e test proving per-request rendered-address routing reaches the correct upstream. |
| e2e/harness_test.go | Updates harness wiring to include connect.Module and reflects Listen behavior in docs. |
pseudomuto
force-pushed
the
templates-upstreams
branch
from
July 22, 2026 15:54
09c42d9 to
a570175
Compare
The proxy could route to multiple named upstreams but only at fixed addresses. Migrations and multi-region setups may require one proxy to reach a different endpoint per namespace (e.g. Cloud per-namespace endpoints), where the dial address and TLS server name depend on the request's namespace and metadata. The router stamps the namespace it already extracts onto an internal metadata header (internal/transport/meta) so the typed per-upstream hop resolves without re-parsing payloads. connect.Conn wraps the connection pool as a grpc.ClientConnInterface whose target is chosen per call by a Resolver: a StaticResolver for fixed upstreams (dialed eagerly) or a DynamicResolver that renders hostPort/serverName and rebuilds credentials per request. The pool is now keyed by a logical key distinct from the dial target, so two connections to one address with different TLS server names don't collapse. A rendered address that is empty or malformed fails the request loudly instead of dialing garbage. For a templated mTLS upstream only the rendered server name varies per request, so rebuilding credentials would otherwise re-read and re-parse the certificate and CA files on every request. A shared creds.CertLoader loads that material once and caches it (safe to share concurrently, as it is immutable after the first load), so per-request resolution builds the dial options without touching disk. Fixed-address upstreams load once as before, so a rotated cert is picked up only on restart either way. This finalizes the upstream-routing RFC proposed in #32.
pseudomuto
force-pushed
the
templates-upstreams
branch
from
July 22, 2026 15:59
a570175 to
7126782
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The proxy could route to multiple named upstreams but only at fixed addresses. Migrations and multi-region setups may require one proxy to reach a different endpoint per namespace (e.g. Cloud per-namespace endpoints), where the dial address and TLS server name depend on the request's namespace and metadata.
The router stamps the namespace it already extracts onto an internal metadata header (internal/transport/meta) so the typed per-upstream hop resolves without re-parsing payloads. connect.Conn wraps the connection pool as a grpc.ClientConnInterface whose target is chosen per call by a Resolver: a StaticResolver for fixed upstreams (dialed eagerly) or a DynamicResolver that renders hostPort/serverName and rebuilds credentials per request. The pool is now keyed by a logical key distinct from the dial target, so two connections to one address with different TLS server names don't collapse. A rendered address that is empty or malformed fails the request loudly instead of dialing garbage.
This finalizes the upstream-routing RFC proposed in #32.