diff --git a/internal/proxyattributes/proxyattributes.go b/internal/proxyattributes/proxyattributes.go
new file mode 100644
index 000000000000..d25d33efd373
--- /dev/null
+++ b/internal/proxyattributes/proxyattributes.go
@@ -0,0 +1,53 @@
+/*
+ *
+ * Copyright 2024 gRPC authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+// Package proxyattributes contains functions for getting and setting proxy
+// attributes like the CONNECT address and user info.
+package proxyattributes
+
+import (
+ "net/url"
+
+ "google.golang.org/grpc/resolver"
+)
+
+type keyType string
+
+const proxyOptionsKey = keyType("grpc.resolver.delegatingresolver.proxyOptions")
+
+// Options holds the proxy connection details needed during the CONNECT
+// handshake.
+type Options struct {
+ User url.Userinfo
+ ConnectAddr string
+}
+
+// Set returns a copy of addr with opts set in its attributes.
+func Set(addr resolver.Address, opts Options) resolver.Address {
+ addr.Attributes = addr.Attributes.WithValue(proxyOptionsKey, opts)
+ return addr
+}
+
+// Get returns the Options for the proxy [resolver.Address] and a boolean
+// value representing if the attribute is present or not.
+func Get(addr resolver.Address) (Options, bool) {
+ if a := addr.Attributes.Value(proxyOptionsKey); a != nil {
+ return a.(Options), true
+ }
+ return Options{}, false
+}
diff --git a/internal/proxyattributes/proxyattributes_test.go b/internal/proxyattributes/proxyattributes_test.go
new file mode 100644
index 000000000000..225b2919d5d9
--- /dev/null
+++ b/internal/proxyattributes/proxyattributes_test.go
@@ -0,0 +1,116 @@
+/*
+ *
+ * Copyright 2024 gRPC authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package proxyattributes
+
+import (
+ "net/url"
+ "testing"
+
+ "google.golang.org/grpc/attributes"
+ "google.golang.org/grpc/internal/grpctest"
+ "google.golang.org/grpc/resolver"
+)
+
+type s struct {
+ grpctest.Tester
+}
+
+func Test(t *testing.T) {
+ grpctest.RunSubTests(t, s{})
+}
+
+// Tests that Get returns a valid proxy attribute.
+func (s) TestGet(t *testing.T) {
+ user := url.UserPassword("username", "password")
+ tests := []struct {
+ name string
+ addr resolver.Address
+ wantConnectAddr string
+ wantUser url.Userinfo
+ wantAttrPresent bool
+ }{
+ {
+ name: "connect_address_in_attribute",
+ addr: resolver.Address{
+ Addr: "test-address",
+ Attributes: attributes.New(proxyOptionsKey, Options{
+ ConnectAddr: "proxy-address",
+ }),
+ },
+ wantConnectAddr: "proxy-address",
+ wantAttrPresent: true,
+ },
+ {
+ name: "user_in_attribute",
+ addr: resolver.Address{
+ Addr: "test-address",
+ Attributes: attributes.New(proxyOptionsKey, Options{
+ User: *user,
+ }),
+ },
+ wantUser: *user,
+ wantAttrPresent: true,
+ },
+ {
+ name: "no_attribute",
+ addr: resolver.Address{Addr: "test-address"},
+ wantAttrPresent: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ gotOption, attrPresent := Get(tt.addr)
+ if attrPresent != tt.wantAttrPresent {
+ t.Errorf("Get(%v) = %v, want %v", tt.addr, attrPresent, tt.wantAttrPresent)
+ }
+
+ if gotOption.ConnectAddr != tt.wantConnectAddr {
+ t.Errorf("ConnectAddr(%v) = %v, want %v", tt.addr, gotOption.ConnectAddr, tt.wantConnectAddr)
+ }
+
+ if gotOption.User != tt.wantUser {
+ t.Errorf("User(%v) = %v, want %v", tt.addr, gotOption.User, tt.wantUser)
+ }
+ })
+ }
+}
+
+// Tests that Set returns a copy of addr with attributes containing correct
+// user and connect address.
+func (s) TestSet(t *testing.T) {
+ addr := resolver.Address{Addr: "test-address"}
+ pOpts := Options{
+ User: *url.UserPassword("username", "password"),
+ ConnectAddr: "proxy-address",
+ }
+
+ // Call Set and validate attributes
+ populatedAddr := Set(addr, pOpts)
+ gotOption, attrPresent := Get(populatedAddr)
+ if !attrPresent {
+ t.Errorf("Get(%v) = %v, want %v ", populatedAddr, attrPresent, true)
+ }
+ if got, want := gotOption.ConnectAddr, pOpts.ConnectAddr; got != want {
+ t.Errorf("Unexpected ConnectAddr proxy atrribute = %v, want %v", got, want)
+ }
+ if got, want := gotOption.User, pOpts.User; got != want {
+ t.Errorf("unexpected User proxy attribute = %v, want %v", got, want)
+ }
+}
diff --git a/internal/resolver/delegatingresolver/delegatingresolver.go b/internal/resolver/delegatingresolver/delegatingresolver.go
new file mode 100644
index 000000000000..6050e3d055bb
--- /dev/null
+++ b/internal/resolver/delegatingresolver/delegatingresolver.go
@@ -0,0 +1,333 @@
+/*
+ *
+ * Copyright 2024 gRPC authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+// Package delegatingresolver implements a resolver capable of resolving both
+// target URIs and proxy addresses.
+package delegatingresolver
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+ "sync"
+
+ "google.golang.org/grpc/grpclog"
+ "google.golang.org/grpc/internal/proxyattributes"
+ "google.golang.org/grpc/resolver"
+ "google.golang.org/grpc/serviceconfig"
+)
+
+var (
+ logger = grpclog.Component("delegating-resolver")
+ // HTTPSProxyFromEnvironment will be overwritten in the tests
+ HTTPSProxyFromEnvironment = http.ProxyFromEnvironment
+)
+
+// delegatingResolver manages both target URI and proxy address resolution by
+// delegating these tasks to separate child resolvers. Essentially, it acts as
+// a intermediary between the gRPC ClientConn and the child resolvers.
+//
+// It implements the [resolver.Resolver] interface.
+type delegatingResolver struct {
+ target resolver.Target // parsed target URI to be resolved
+ cc resolver.ClientConn // gRPC ClientConn
+ targetResolver resolver.Resolver // resolver for the target URI, based on its scheme
+ proxyResolver resolver.Resolver // resolver for the proxy URI; nil if no proxy is configured
+ proxyURL *url.URL // proxy URL, derived from proxy environment and target
+
+ mu sync.Mutex // protects all the fields below
+ targetResolverState *resolver.State // state of the target resolver
+ proxyAddrs []resolver.Address // resolved proxy addresses; empty if no proxy is configured
+}
+
+// nopResolver is a resolver that does nothing.
+type nopResolver struct{}
+
+func (nopResolver) ResolveNow(resolver.ResolveNowOptions) {}
+
+func (nopResolver) Close() {}
+
+// proxyURLForTarget determines the proxy URL for the given address based on
+// the environment. It can return the following:
+// - nil URL, nil error: No proxy is configured or the address is excluded
+// using the `NO_PROXY` environment variable or if req.URL.Host is
+// "localhost" (with or without // a port number)
+// - nil URL, non-nil error: An error occurred while retrieving the proxy URL.
+// - non-nil URL, nil error: A proxy is configured, and the proxy URL was
+// retrieved successfully without any errors.
+func proxyURLForTarget(address string) (*url.URL, error) {
+ req := &http.Request{URL: &url.URL{
+ Scheme: "https",
+ Host: address,
+ }}
+ return HTTPSProxyFromEnvironment(req)
+}
+
+// New creates a new delegating resolver that can create up to two child
+// resolvers:
+// - one to resolve the proxy address specified using the supported
+// environment variables. This uses the registered resolver for the "dns"
+// scheme.
+// - one to resolve the target URI using the resolver specified by the scheme
+// in the target URI or specified by the user using the WithResolvers dial
+// option. As a special case, if the target URI's scheme is "dns" and a
+// proxy is specified using the supported environment variables, the target
+// URI's path portion is used as the resolved address unless target
+// resolution is enabled using the dial option.
+func New(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions, targetResolverBuilder resolver.Builder, targetResolutionEnabled bool) (resolver.Resolver, error) {
+ r := &delegatingResolver{
+ target: target,
+ cc: cc,
+ }
+
+ var err error
+ r.proxyURL, err = proxyURLForTarget(target.Endpoint())
+ if err != nil {
+ return nil, fmt.Errorf("delegating_resolver: failed to determine proxy URL for target %s: %v", target, err)
+ }
+
+ // proxy is not configured or proxy address excluded using `NO_PROXY` env
+ // var, so only target resolver is used.
+ if r.proxyURL == nil {
+ return targetResolverBuilder.Build(target, cc, opts)
+ }
+
+ if logger.V(2) {
+ logger.Infof("Proxy URL detected : %s", r.proxyURL)
+ }
+
+ // When the scheme is 'dns' and target resolution on client is not enabled,
+ // resolution should be handled by the proxy, not the client. Therefore, we
+ // bypass the target resolver and store the unresolved target address.
+ if target.URL.Scheme == "dns" && !targetResolutionEnabled {
+ state := resolver.State{
+ Addresses: []resolver.Address{{Addr: target.Endpoint()}},
+ Endpoints: []resolver.Endpoint{{Addresses: []resolver.Address{{Addr: target.Endpoint()}}}},
+ }
+ r.targetResolverState = &state
+ } else {
+ wcc := &wrappingClientConn{
+ stateListener: r.updateTargetResolverState,
+ parent: r,
+ }
+ if r.targetResolver, err = targetResolverBuilder.Build(target, wcc, opts); err != nil {
+ return nil, fmt.Errorf("delegating_resolver: unable to build the resolver for target %s: %v", target, err)
+ }
+ }
+
+ if r.proxyResolver, err = r.proxyURIResolver(opts); err != nil {
+ return nil, fmt.Errorf("delegating_resolver: failed to build resolver for proxy URL %q: %v", r.proxyURL, err)
+ }
+
+ if r.targetResolver == nil {
+ r.targetResolver = nopResolver{}
+ }
+ if r.proxyResolver == nil {
+ r.proxyResolver = nopResolver{}
+ }
+ return r, nil
+}
+
+// proxyURIResolver creates a resolver for resolving proxy URIs using the
+// "dns" scheme. It adjusts the proxyURL to conform to the "dns:///" format and
+// builds a resolver with a wrappingClientConn to capture resolved addresses.
+func (r *delegatingResolver) proxyURIResolver(opts resolver.BuildOptions) (resolver.Resolver, error) {
+ proxyBuilder := resolver.Get("dns")
+ if proxyBuilder == nil {
+ panic("delegating_resolver: resolver for proxy not found for scheme dns")
+ }
+ url := *r.proxyURL
+ url.Scheme = "dns"
+ url.Path = "/" + r.proxyURL.Host
+ url.Host = "" // Clear the Host field to conform to the "dns:///" format
+
+ proxyTarget := resolver.Target{URL: url}
+ wcc := &wrappingClientConn{
+ stateListener: r.updateProxyResolverState,
+ parent: r,
+ }
+ return proxyBuilder.Build(proxyTarget, wcc, opts)
+}
+
+func (r *delegatingResolver) ResolveNow(o resolver.ResolveNowOptions) {
+ r.targetResolver.ResolveNow(o)
+ r.proxyResolver.ResolveNow(o)
+}
+
+func (r *delegatingResolver) Close() {
+ r.targetResolver.Close()
+ r.targetResolver = nil
+
+ r.proxyResolver.Close()
+ r.proxyResolver = nil
+}
+
+// updateClientConnStateLocked creates a list of combined addresses by
+// pairing each proxy address with every target address. For each pair, it
+// generates a new [resolver.Address] using the proxy address, and adding the
+// target address as the attribute along with user info. It returns nil if
+// either resolver has not sent update even once and returns the error from
+// ClientConn update once both resolvers have sent update atleast once.
+func (r *delegatingResolver) updateClientConnStateLocked() error {
+ if r.targetResolverState == nil || r.proxyAddrs == nil {
+ return nil
+ }
+
+ curState := *r.targetResolverState
+ // If multiple resolved proxy addresses are present, we send only the
+ // unresolved proxy host and let net.Dial handle the proxy host name
+ // resolution when creating the transport. Sending all resolved addresses
+ // would increase the number of addresses passed to the ClientConn and
+ // subsequently to load balancing (LB) policies like Round Robin, leading
+ // to additional TCP connections. However, if there's only one resolved
+ // proxy address, we send it directly, as it doesn't affect the address
+ // count returned by the target resolver and the address count sent to the
+ // ClientConn.
+ var proxyAddr resolver.Address
+ if len(r.proxyAddrs) == 1 {
+ proxyAddr = r.proxyAddrs[0]
+ } else {
+ proxyAddr = resolver.Address{Addr: r.proxyURL.Host}
+ }
+ var addresses []resolver.Address
+ var user url.Userinfo
+ if r.proxyURL.User != nil {
+ user = *r.proxyURL.User
+ }
+ for _, targetAddr := range (*r.targetResolverState).Addresses {
+ addresses = append(addresses, proxyattributes.Set(proxyAddr, proxyattributes.Options{
+ User: user,
+ ConnectAddr: targetAddr.Addr,
+ }))
+ }
+
+ // Create a list of combined endpoints by pairing all proxy endpoints
+ // with every target endpoint. Each time, it constructs a new
+ // [resolver.Endpoint] using the all addresses from all the proxy endpoint
+ // and the target addresses from one endpoint. The target address and user
+ // information from the proxy URL are added as attributes to the proxy
+ // address.The resulting list of addresses is then grouped into endpoints,
+ // covering all combinations of proxy and target endpoints.
+ var endpoints []resolver.Endpoint
+ for _, endpt := range (*r.targetResolverState).Endpoints {
+ var addrs []resolver.Address
+ for _, proxyAddr := range r.proxyAddrs {
+ for _, targetAddr := range endpt.Addresses {
+ addrs = append(addrs, proxyattributes.Set(proxyAddr, proxyattributes.Options{
+ User: user,
+ ConnectAddr: targetAddr.Addr,
+ }))
+ }
+ }
+ endpoints = append(endpoints, resolver.Endpoint{Addresses: addrs})
+ }
+ // Use the targetResolverState for its service config and attributes
+ // contents. The state update is only sent after both the target and proxy
+ // resolvers have sent their updates, and curState has been updated with
+ // the combined addresses.
+ curState.Addresses = addresses
+ curState.Endpoints = endpoints
+ return r.cc.UpdateState(curState)
+}
+
+// updateProxyResolverState updates the proxy resolver state by storing proxy
+// addresses and endpoints, marking the resolver as ready, and triggering a
+// state update if both proxy and target resolvers are ready. If the ClientConn
+// returns a non-nil error, it calls `ResolveNow()` on the target resolver. It
+// is a StateListener function of wrappingClientConn passed to the proxy resolver.
+func (r *delegatingResolver) updateProxyResolverState(state resolver.State) error {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+ if logger.V(2) {
+ logger.Infof("Addresses received from proxy resolver: %s", state.Addresses)
+ }
+ if len(state.Endpoints) > 0 {
+ // We expect exactly one address per endpoint because the proxy
+ // resolver uses "dns" resolution.
+ r.proxyAddrs = make([]resolver.Address, 0, len(state.Endpoints))
+ for _, endpoint := range state.Endpoints {
+ r.proxyAddrs = append(r.proxyAddrs, endpoint.Addresses...)
+ }
+ } else if state.Addresses != nil {
+ r.proxyAddrs = state.Addresses
+ } else {
+ r.proxyAddrs = []resolver.Address{} // ensure proxyAddrs is non-nil to indicate an update has been received
+ }
+ err := r.updateClientConnStateLocked()
+ // Another possible approach was to block until updates are received from
+ // both resolvers. But this is not used because calling `New()` triggers
+ // `Build()` for the first resolver, which calls `UpdateState()`. And the
+ // second resolver hasn't sent an update yet, so it would cause `New()` to
+ // block indefinitely.
+ if err != nil {
+ r.targetResolver.ResolveNow(resolver.ResolveNowOptions{})
+ }
+ return err
+}
+
+// updateTargetResolverState updates the target resolver state by storing target
+// addresses, endpoints, and service config, marking the resolver as ready, and
+// triggering a state update if both resolvers are ready. If the ClientConn
+// returns a non-nil error, it calls `ResolveNow()` on the proxy resolver. It
+// is a StateListener function of wrappingClientConn passed to the target resolver.
+func (r *delegatingResolver) updateTargetResolverState(state resolver.State) error {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+
+ if logger.V(2) {
+ logger.Infof("Addresses received from target resolver: %v", state.Addresses)
+ }
+ r.targetResolverState = &state
+ err := r.updateClientConnStateLocked()
+ if err != nil {
+ r.proxyResolver.ResolveNow(resolver.ResolveNowOptions{})
+ }
+ return nil
+}
+
+// wrappingClientConn serves as an intermediary between the parent ClientConn
+// and the child resolvers created here. It implements the resolver.ClientConn
+// interface and is passed in that capacity to the child resolvers.
+type wrappingClientConn struct {
+ // Callback to deliver resolver state updates
+ stateListener func(state resolver.State) error
+ parent *delegatingResolver
+}
+
+// UpdateState receives resolver state updates and forwards them to the
+// appropriate listener function (either for the proxy or target resolver).
+func (wcc *wrappingClientConn) UpdateState(state resolver.State) error {
+ return wcc.stateListener(state)
+}
+
+// ReportError intercepts errors from the child resolvers and passes them to ClientConn.
+func (wcc *wrappingClientConn) ReportError(err error) {
+ wcc.parent.cc.ReportError(err)
+}
+
+// NewAddress intercepts the new resolved address from the child resolvers and
+// passes them to ClientConn.
+func (wcc *wrappingClientConn) NewAddress(addrs []resolver.Address) {
+ wcc.UpdateState(resolver.State{Addresses: addrs})
+}
+
+// ParseServiceConfig parses the provided service config and returns an
+// object that provides the parsed config.
+func (wcc *wrappingClientConn) ParseServiceConfig(serviceConfigJSON string) *serviceconfig.ParseResult {
+ return wcc.parent.cc.ParseServiceConfig(serviceConfigJSON)
+}
diff --git a/internal/resolver/delegatingresolver/delegatingresolver_ext_test.go b/internal/resolver/delegatingresolver/delegatingresolver_ext_test.go
new file mode 100644
index 000000000000..6f60153c7dd2
--- /dev/null
+++ b/internal/resolver/delegatingresolver/delegatingresolver_ext_test.go
@@ -0,0 +1,550 @@
+/*
+ *
+ * Copyright 2024 gRPC authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package delegatingresolver_test
+
+import (
+ "net/http"
+ "net/url"
+ "testing"
+ "time"
+
+ "github.com/google/go-cmp/cmp"
+ "google.golang.org/grpc/internal/grpctest"
+ "google.golang.org/grpc/internal/proxyattributes"
+ "google.golang.org/grpc/internal/resolver/delegatingresolver"
+ "google.golang.org/grpc/internal/testutils"
+ "google.golang.org/grpc/resolver"
+ "google.golang.org/grpc/resolver/manual"
+ "google.golang.org/grpc/serviceconfig"
+
+ _ "google.golang.org/grpc/resolver/dns" // To register dns resolver.
+)
+
+type s struct {
+ grpctest.Tester
+}
+
+func Test(t *testing.T) {
+ grpctest.RunSubTests(t, s{})
+}
+
+const (
+ defaultTestTimeout = 10 * time.Second
+ defaultTestShortTimeout = 10 * time.Millisecond
+)
+
+// createTestResolverClientConn initializes a [testutils.ResolverClientConn] and
+// returns it along with channels for resolver state updates and errors.
+func createTestResolverClientConn(t *testing.T) (*testutils.ResolverClientConn, chan resolver.State, chan error) {
+ t.Helper()
+ stateCh := make(chan resolver.State, 1)
+ errCh := make(chan error, 1)
+
+ tcc := &testutils.ResolverClientConn{
+ Logger: t,
+ UpdateStateF: func(s resolver.State) error { stateCh <- s; return nil },
+ ReportErrorF: func(err error) { errCh <- err },
+ }
+ return tcc, stateCh, errCh
+}
+
+// Tests the scenario where no proxy environment variables are set or proxying
+// is disabled by the `NO_PROXY` environment variable. The test verifies that
+// the delegating resolver creates only a target resolver and that the
+// addresses returned by the delegating resolver exactly match those returned
+// by the target resolver.
+func (s) TestDelegatingResolverNoProxyEnvVarsSet(t *testing.T) {
+ hpfe := func(req *http.Request) (*url.URL, error) { return nil, nil }
+ originalhpfe := delegatingresolver.HTTPSProxyFromEnvironment
+ delegatingresolver.HTTPSProxyFromEnvironment = hpfe
+ defer func() {
+ delegatingresolver.HTTPSProxyFromEnvironment = originalhpfe
+ }()
+
+ const (
+ targetTestAddr = "test.com"
+ resolvedTargetTestAddr1 = "1.1.1.1:8080"
+ resolvedTargetTestAddr2 = "2.2.2.2:8080"
+ )
+
+ // Set up a manual resolver to control the address resolution.
+ targetResolver := manual.NewBuilderWithScheme("test")
+ target := targetResolver.Scheme() + ":///" + targetTestAddr
+
+ // Create a delegating resolver with no proxy configuration
+ tcc, stateCh, _ := createTestResolverClientConn(t)
+ if _, err := delegatingresolver.New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, targetResolver, false); err != nil {
+ t.Fatalf("Failed to create delegating resolver: %v", err)
+ }
+
+ // Update the manual resolver with a test address.
+ targetResolver.UpdateState(resolver.State{
+ Addresses: []resolver.Address{
+ {Addr: resolvedTargetTestAddr1},
+ {Addr: resolvedTargetTestAddr2},
+ },
+ ServiceConfig: &serviceconfig.ParseResult{},
+ })
+
+ // Verify that the delegating resolver outputs the same addresses, as returned
+ // by the target resolver.
+ wantState := resolver.State{
+ Addresses: []resolver.Address{
+ {Addr: resolvedTargetTestAddr1},
+ {Addr: resolvedTargetTestAddr2},
+ },
+ ServiceConfig: &serviceconfig.ParseResult{},
+ }
+
+ var gotState resolver.State
+ select {
+ case gotState = <-stateCh:
+ case <-time.After(defaultTestTimeout):
+ t.Fatal("Timeout when waiting for a state update from the delegating resolver")
+ }
+
+ if diff := cmp.Diff(gotState, wantState); diff != "" {
+ t.Fatalf("Unexpected state from delegating resolver. Diff (-got +want):\n%v", diff)
+ }
+}
+
+// setupDNS registers a new manual resolver for the DNS scheme, effectively
+// overwriting the previously registered DNS resolver. This allows the test to
+// mock the DNS resolution for the proxy resolver. It also registers the
+// original DNS resolver after the test is done.
+func setupDNS(t *testing.T) *manual.Resolver {
+ t.Helper()
+ mr := manual.NewBuilderWithScheme("dns")
+
+ dnsResolverBuilder := resolver.Get("dns")
+ resolver.Register(mr)
+
+ t.Cleanup(func() { resolver.Register(dnsResolverBuilder) })
+ return mr
+}
+
+// proxyAddressWithTargetAttribute creates a resolver.Address for the proxy,
+// adding the target address as an attribute.
+func proxyAddressWithTargetAttribute(proxyAddr string, targetAddr string) resolver.Address {
+ addr := resolver.Address{Addr: proxyAddr}
+ addr = proxyattributes.Set(addr, proxyattributes.Options{ConnectAddr: targetAddr})
+ return addr
+}
+
+// Tests the scenario where proxy is configured and the target URI contains the
+// "dns" scheme and target resolution is enabled. The test verifies that the
+// addresses returned by the delegating resolver combines the addresses
+// returned by the proxy resolver and the target resolver.
+func (s) TestDelegatingResolverwithDNSAndProxyWithTargetResolution(t *testing.T) {
+ const (
+ targetTestAddr = "test.com"
+ resolvedTargetTestAddr1 = "1.1.1.1:8080"
+ resolvedTargetTestAddr2 = "2.2.2.2:8080"
+ envProxyAddr = "proxytest.com"
+ resolvedProxyTestAddr1 = "11.11.11.11:7687"
+ )
+ hpfe := func(req *http.Request) (*url.URL, error) {
+ if req.URL.Host == targetTestAddr {
+ return &url.URL{
+ Scheme: "https",
+ Host: envProxyAddr,
+ }, nil
+ }
+ t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, targetTestAddr)
+ return nil, nil
+ }
+ originalhpfe := delegatingresolver.HTTPSProxyFromEnvironment
+ delegatingresolver.HTTPSProxyFromEnvironment = hpfe
+ defer func() {
+ delegatingresolver.HTTPSProxyFromEnvironment = originalhpfe
+ }()
+ // Manual resolver to control the target resolution.
+ targetResolver := manual.NewBuilderWithScheme("dns")
+ target := targetResolver.Scheme() + ":///" + targetTestAddr
+ // Set up a manual DNS resolver to control the proxy address resolution.
+ proxyResolver := setupDNS(t)
+
+ tcc, stateCh, _ := createTestResolverClientConn(t)
+ if _, err := delegatingresolver.New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, targetResolver, true); err != nil {
+ t.Fatalf("Failed to create delegating resolver: %v", err)
+ }
+
+ proxyResolver.UpdateState(resolver.State{
+ Addresses: []resolver.Address{{Addr: resolvedProxyTestAddr1}},
+ ServiceConfig: &serviceconfig.ParseResult{},
+ })
+
+ select {
+ case <-stateCh:
+ t.Fatalf("Delegating resolver invoked UpdateState before both the proxy and target resolvers had updated their states.")
+ case <-time.After(defaultTestShortTimeout):
+ }
+
+ targetResolver.UpdateState(resolver.State{
+ Addresses: []resolver.Address{
+ {Addr: resolvedTargetTestAddr1},
+ {Addr: resolvedTargetTestAddr2},
+ },
+ ServiceConfig: &serviceconfig.ParseResult{},
+ })
+
+ // Verify that the delegating resolver outputs the expected address.
+ wantState := resolver.State{
+ Addresses: []resolver.Address{
+ proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, resolvedTargetTestAddr1),
+ proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, resolvedTargetTestAddr2),
+ },
+ ServiceConfig: &serviceconfig.ParseResult{},
+ }
+ var gotState resolver.State
+ select {
+ case gotState = <-stateCh:
+ case <-time.After(defaultTestTimeout):
+ t.Fatal("Timeout when waiting for a state update from the delegating resolver")
+ }
+
+ if diff := cmp.Diff(gotState, wantState); diff != "" {
+ t.Fatalf("Unexpected state from delegating resolver. Diff (-got +want):\n%v", diff)
+ }
+}
+
+// Tests the scenario where a proxy is configured, the target URI contains the
+// "dns" scheme, and target resolution is disabled(default behavior). The test
+// verifies that the addresses returned by the delegating resolver include the
+// proxy resolver's addresses, with the unresolved target URI as an attribute
+// of the proxy address.
+func (s) TestDelegatingResolverwithDNSAndProxyWithNoTargetResolution(t *testing.T) {
+ const (
+ targetTestAddr = "test.com"
+ envProxyAddr = "proxytest.com"
+ resolvedProxyTestAddr1 = "11.11.11.11:7687"
+ )
+ hpfe := func(req *http.Request) (*url.URL, error) {
+ if req.URL.Host == targetTestAddr {
+ return &url.URL{
+ Scheme: "https",
+ Host: envProxyAddr,
+ }, nil
+ }
+ t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, targetTestAddr)
+ return nil, nil
+ }
+ originalhpfe := delegatingresolver.HTTPSProxyFromEnvironment
+ delegatingresolver.HTTPSProxyFromEnvironment = hpfe
+ defer func() {
+ delegatingresolver.HTTPSProxyFromEnvironment = originalhpfe
+ }()
+
+ targetResolver := manual.NewBuilderWithScheme("dns")
+ target := targetResolver.Scheme() + ":///" + targetTestAddr
+ // Set up a manual DNS resolver to control the proxy address resolution.
+ proxyResolver := setupDNS(t)
+
+ tcc, stateCh, _ := createTestResolverClientConn(t)
+ if _, err := delegatingresolver.New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, targetResolver, false); err != nil {
+ t.Fatalf("Failed to create delegating resolver: %v", err)
+ }
+
+ proxyResolver.UpdateState(resolver.State{
+ Addresses: []resolver.Address{
+ {Addr: resolvedProxyTestAddr1},
+ },
+ })
+
+ wantState := resolver.State{
+ Addresses: []resolver.Address{proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, targetTestAddr)},
+ Endpoints: []resolver.Endpoint{{Addresses: []resolver.Address{proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, targetTestAddr)}}},
+ }
+
+ var gotState resolver.State
+ select {
+ case gotState = <-stateCh:
+ case <-time.After(defaultTestTimeout):
+ t.Fatal("Timeout when waiting for a state update from the delegating resolver")
+ }
+
+ if diff := cmp.Diff(gotState, wantState); diff != "" {
+ t.Fatalf("Unexpected state from delegating resolver. Diff (-got +want):\n%v", diff)
+ }
+}
+
+// Tests the scenario where a proxy is configured, and the target URI scheme is
+// not "dns". The test verifies that the addresses returned by the delegating
+// resolver include the resolved proxy address and the custom resolved target
+// address as attributes of the proxy address.
+func (s) TestDelegatingResolverwithCustomResolverAndProxy(t *testing.T) {
+ const (
+ targetTestAddr = "test.com"
+ resolvedTargetTestAddr1 = "1.1.1.1:8080"
+ resolvedTargetTestAddr2 = "2.2.2.2:8080"
+ envProxyAddr = "proxytest.com"
+ resolvedProxyTestAddr1 = "11.11.11.11:7687"
+ )
+ hpfe := func(req *http.Request) (*url.URL, error) {
+ if req.URL.Host == targetTestAddr {
+ return &url.URL{
+ Scheme: "https",
+ Host: envProxyAddr,
+ }, nil
+ }
+ t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, targetTestAddr)
+ return nil, nil
+ }
+ originalhpfe := delegatingresolver.HTTPSProxyFromEnvironment
+ delegatingresolver.HTTPSProxyFromEnvironment = hpfe
+ defer func() {
+ delegatingresolver.HTTPSProxyFromEnvironment = originalhpfe
+ }()
+
+ // Manual resolver to control the target resolution.
+ targetResolver := manual.NewBuilderWithScheme("test")
+ target := targetResolver.Scheme() + ":///" + targetTestAddr
+ // Set up a manual DNS resolver to control the proxy address resolution.
+ proxyResolver := setupDNS(t)
+
+ tcc, stateCh, _ := createTestResolverClientConn(t)
+ if _, err := delegatingresolver.New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, targetResolver, false); err != nil {
+ t.Fatalf("Failed to create delegating resolver: %v", err)
+ }
+
+ proxyResolver.UpdateState(resolver.State{
+ Addresses: []resolver.Address{{Addr: resolvedProxyTestAddr1}},
+ ServiceConfig: &serviceconfig.ParseResult{},
+ })
+
+ select {
+ case <-stateCh:
+ t.Fatalf("Delegating resolver invoked UpdateState before both the proxy and target resolvers had updated their states.")
+ case <-time.After(defaultTestShortTimeout):
+ }
+
+ targetResolver.UpdateState(resolver.State{
+ Addresses: []resolver.Address{
+ {Addr: resolvedTargetTestAddr1},
+ {Addr: resolvedTargetTestAddr2},
+ },
+ ServiceConfig: &serviceconfig.ParseResult{},
+ })
+
+ wantState := resolver.State{
+ Addresses: []resolver.Address{
+ proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, resolvedTargetTestAddr1),
+ proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, resolvedTargetTestAddr2),
+ },
+ ServiceConfig: &serviceconfig.ParseResult{},
+ }
+ var gotState resolver.State
+ select {
+ case gotState = <-stateCh:
+ case <-time.After(defaultTestTimeout):
+ t.Fatal("Timeout when waiting for a state update from the delegating resolver")
+ }
+
+ if diff := cmp.Diff(gotState, wantState); diff != "" {
+ t.Fatalf("Unexpected state from delegating resolver. Diff (-got +want):\n%v", diff)
+ }
+}
+
+// Tests the scenario where a proxy is configured, the target URI scheme is not
+// "dns," and both the proxy and target resolvers return endpoints. The test
+// verifies that the delegating resolver combines resolved proxy and target
+// addresses correctly, returning endpoints with the proxy address populated
+// and the target address included as an attribute of the proxy address for
+// each combination of proxy and target endpoints.
+func (s) TestDelegatingResolverForEndpointsWithProxy(t *testing.T) {
+ const (
+ targetTestAddr = "test.com"
+ resolvedTargetTestAddr1 = "1.1.1.1:8080"
+ resolvedTargetTestAddr2 = "2.2.2.2:8080"
+ resolvedTargetTestAddr3 = "3.3.3.3:8080"
+ resolvedTargetTestAddr4 = "4.4.4.4:8080"
+ envProxyAddr = "proxytest.com"
+ resolvedProxyTestAddr1 = "11.11.11.11:7687"
+ resolvedProxyTestAddr2 = "22.22.22.22:7687"
+ )
+ hpfe := func(req *http.Request) (*url.URL, error) {
+ if req.URL.Host == targetTestAddr {
+ return &url.URL{
+ Scheme: "https",
+ Host: envProxyAddr,
+ }, nil
+ }
+ t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, targetTestAddr)
+ return nil, nil
+ }
+ originalhpfe := delegatingresolver.HTTPSProxyFromEnvironment
+ delegatingresolver.HTTPSProxyFromEnvironment = hpfe
+ defer func() {
+ delegatingresolver.HTTPSProxyFromEnvironment = originalhpfe
+ }()
+
+ // Manual resolver to control the target resolution.
+ targetResolver := manual.NewBuilderWithScheme("test")
+ target := targetResolver.Scheme() + ":///" + targetTestAddr
+ // Set up a manual DNS resolver to control the proxy address resolution.
+ proxyResolver := setupDNS(t)
+
+ tcc, stateCh, _ := createTestResolverClientConn(t)
+ if _, err := delegatingresolver.New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, targetResolver, false); err != nil {
+ t.Fatalf("Failed to create delegating resolver: %v", err)
+ }
+
+ proxyResolver.UpdateState(resolver.State{
+ Endpoints: []resolver.Endpoint{
+ {Addresses: []resolver.Address{{Addr: resolvedProxyTestAddr1}}},
+ {Addresses: []resolver.Address{{Addr: resolvedProxyTestAddr2}}},
+ },
+ ServiceConfig: &serviceconfig.ParseResult{},
+ })
+
+ select {
+ case <-stateCh:
+ t.Fatalf("Delegating resolver invoked UpdateState before both the proxy and target resolvers had updated their states.")
+ case <-time.After(defaultTestShortTimeout):
+ }
+ targetResolver.UpdateState(resolver.State{
+ Endpoints: []resolver.Endpoint{
+ {
+ Addresses: []resolver.Address{
+ {Addr: resolvedTargetTestAddr1},
+ {Addr: resolvedTargetTestAddr2}},
+ },
+ {
+ Addresses: []resolver.Address{
+ {Addr: resolvedTargetTestAddr3},
+ {Addr: resolvedTargetTestAddr4}},
+ },
+ },
+ ServiceConfig: &serviceconfig.ParseResult{},
+ })
+
+ wantState := resolver.State{
+ Endpoints: []resolver.Endpoint{
+ {
+ Addresses: []resolver.Address{
+ proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, resolvedTargetTestAddr1),
+ proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, resolvedTargetTestAddr2),
+ proxyAddressWithTargetAttribute(resolvedProxyTestAddr2, resolvedTargetTestAddr1),
+ proxyAddressWithTargetAttribute(resolvedProxyTestAddr2, resolvedTargetTestAddr2),
+ },
+ },
+ {
+ Addresses: []resolver.Address{
+ proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, resolvedTargetTestAddr3),
+ proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, resolvedTargetTestAddr4),
+ proxyAddressWithTargetAttribute(resolvedProxyTestAddr2, resolvedTargetTestAddr3),
+ proxyAddressWithTargetAttribute(resolvedProxyTestAddr2, resolvedTargetTestAddr4),
+ },
+ },
+ },
+ ServiceConfig: &serviceconfig.ParseResult{},
+ }
+ var gotState resolver.State
+ select {
+ case gotState = <-stateCh:
+ case <-time.After(defaultTestTimeout):
+ t.Fatal("Timeout when waiting for a state update from the delegating resolver")
+ }
+
+ if diff := cmp.Diff(gotState, wantState); diff != "" {
+ t.Fatalf("Unexpected state from delegating resolver. Diff (-got +want):\n%v", diff)
+ }
+}
+
+// Tests the scenario where a proxy is configured, the target URI scheme is not
+// "dns," and both the proxy and target resolvers return multiple addresses.
+// The test verifies that the delegating resolver combines unresolved proxy
+// host and target addresses correctly, returning addresses with the proxy host
+// populated and the target address included as an attribute.
+func (s) TestDelegatingResolverForMutipleProxyAddress(t *testing.T) {
+ const (
+ targetTestAddr = "test.com"
+ resolvedTargetTestAddr1 = "1.1.1.1:8080"
+ resolvedTargetTestAddr2 = "2.2.2.2:8080"
+ envProxyAddr = "proxytest.com"
+ resolvedProxyTestAddr1 = "11.11.11.11:7687"
+ resolvedProxyTestAddr2 = "22.22.22.22:7687"
+ )
+ hpfe := func(req *http.Request) (*url.URL, error) {
+ if req.URL.Host == targetTestAddr {
+ return &url.URL{
+ Scheme: "https",
+ Host: envProxyAddr,
+ }, nil
+ }
+ t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, targetTestAddr)
+ return nil, nil
+ }
+ originalhpfe := delegatingresolver.HTTPSProxyFromEnvironment
+ delegatingresolver.HTTPSProxyFromEnvironment = hpfe
+ defer func() {
+ delegatingresolver.HTTPSProxyFromEnvironment = originalhpfe
+ }()
+
+ // Manual resolver to control the target resolution.
+ targetResolver := manual.NewBuilderWithScheme("test")
+ target := targetResolver.Scheme() + ":///" + targetTestAddr
+ // Set up a manual DNS resolver to control the proxy address resolution.
+ proxyResolver := setupDNS(t)
+
+ tcc, stateCh, _ := createTestResolverClientConn(t)
+ if _, err := delegatingresolver.New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, targetResolver, false); err != nil {
+ t.Fatalf("Failed to create delegating resolver: %v", err)
+ }
+
+ proxyResolver.UpdateState(resolver.State{
+ Addresses: []resolver.Address{
+ {Addr: resolvedProxyTestAddr1},
+ {Addr: resolvedProxyTestAddr2},
+ },
+ ServiceConfig: &serviceconfig.ParseResult{},
+ })
+
+ select {
+ case <-stateCh:
+ t.Fatalf("Delegating resolver invoked UpdateState before both the proxy and target resolvers had updated their states.")
+ case <-time.After(defaultTestShortTimeout):
+ }
+
+ targetResolver.UpdateState(resolver.State{
+ Addresses: []resolver.Address{
+ {Addr: resolvedTargetTestAddr1},
+ {Addr: resolvedTargetTestAddr2},
+ },
+ ServiceConfig: &serviceconfig.ParseResult{},
+ })
+
+ wantState := resolver.State{
+ Addresses: []resolver.Address{
+ proxyAddressWithTargetAttribute(envProxyAddr, resolvedTargetTestAddr1),
+ proxyAddressWithTargetAttribute(envProxyAddr, resolvedTargetTestAddr2),
+ },
+ ServiceConfig: &serviceconfig.ParseResult{},
+ }
+ var gotState resolver.State
+ select {
+ case gotState = <-stateCh:
+ case <-time.After(defaultTestTimeout):
+ t.Fatal("Timeout when waiting for a state update from the delegating resolver")
+ }
+
+ if diff := cmp.Diff(gotState, wantState); diff != "" {
+ t.Fatalf("Unexpected state from delegating resolver. Diff (-got +want):\n%v", diff)
+ }
+}
diff --git a/internal/resolver/delegatingresolver/delegatingresolver_test.go b/internal/resolver/delegatingresolver/delegatingresolver_test.go
new file mode 100644
index 000000000000..c99afff422e6
--- /dev/null
+++ b/internal/resolver/delegatingresolver/delegatingresolver_test.go
@@ -0,0 +1,110 @@
+/*
+ *
+ * Copyright 2024 gRPC authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package delegatingresolver
+
+import (
+ "errors"
+ "net/http"
+ "net/url"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "google.golang.org/grpc/internal/grpctest"
+)
+
+type s struct {
+ grpctest.Tester
+}
+
+func Test(t *testing.T) {
+ grpctest.RunSubTests(t, s{})
+}
+
+const (
+ targetTestAddr = "test.com"
+ envProxyAddr = "proxytest.com"
+)
+
+// overrideHTTPSProxyFromEnvironment function overwrites HTTPSProxyFromEnvironment and
+// returns a function to restore the default values.
+func overrideHTTPSProxyFromEnvironment(hpfe func(req *http.Request) (*url.URL, error)) func() {
+ HTTPSProxyFromEnvironment = hpfe
+ return func() {
+ HTTPSProxyFromEnvironment = nil
+ }
+}
+
+// Tests that the proxyURLForTarget function correctly resolves the proxy URL
+// for a given target address. Tests all the possible output cases.
+func (s) TestproxyURLForTargetEnv(t *testing.T) {
+ err := errors.New("invalid proxy url")
+ tests := []struct {
+ name string
+ hpfeFunc func(req *http.Request) (*url.URL, error)
+ wantURL *url.URL
+ wantErr error
+ }{
+ {
+ name: "valid_proxy_url_and_nil_error",
+ hpfeFunc: func(_ *http.Request) (*url.URL, error) {
+ return &url.URL{
+ Scheme: "https",
+ Host: "proxy.example.com",
+ }, nil
+ },
+ wantURL: &url.URL{
+ Scheme: "https",
+ Host: "proxy.example.com",
+ },
+ },
+ {
+ name: "invalid_proxy_url_and_non-nil_error",
+ hpfeFunc: func(_ *http.Request) (*url.URL, error) {
+ return &url.URL{
+ Scheme: "https",
+ Host: "notproxy.example.com",
+ }, err
+ },
+ wantURL: &url.URL{
+ Scheme: "https",
+ Host: "notproxy.example.com",
+ },
+ wantErr: err,
+ },
+ {
+ name: "nil_proxy_url_and_nil_error",
+ hpfeFunc: func(_ *http.Request) (*url.URL, error) {
+ return nil, nil
+ },
+ wantURL: nil,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ defer overrideHTTPSProxyFromEnvironment(tt.hpfeFunc)()
+ got, err := proxyURLForTarget(targetTestAddr)
+ if err != tt.wantErr {
+ t.Errorf("parsedProxyURLForProxy(%v) failed with error :%v, want %v\n", targetTestAddr, err, tt.wantErr)
+ }
+ if !cmp.Equal(got, tt.wantURL) {
+ t.Fatalf("parsedProxyURLForProxy(%v) = %v, want %v\n", targetTestAddr, got, tt.wantURL)
+ }
+ })
+ }
+}