/*
 *
 * 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 ( ) 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 // an 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 proxyURL *url.URL // proxy URL, derived from proxy environment and target // We do not hold both mu and childMu in the same goroutine. Avoid holding // both locks when calling into the child, as the child resolver may // synchronously callback into the channel. 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 // childMu serializes calls into child resolvers. It also protects access to // the following fields. childMu sync.Mutex 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 } // nopResolver is a resolver that does nothing. type nopResolver struct{} func (nopResolver) (resolver.ResolveNowOptions) {} func (nopResolver) () {} // 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 ( string) (*url.URL, error) { := &http.Request{URL: &url.URL{ Scheme: "https", Host: , }} return HTTPSProxyFromEnvironment() } // 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. It is lazily built when a target resolver update contains at least // one TCP address. // - 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 ( resolver.Target, resolver.ClientConn, resolver.BuildOptions, resolver.Builder, bool) (resolver.Resolver, error) { := &delegatingResolver{ target: , cc: , proxyResolver: nopResolver{}, targetResolver: nopResolver{}, } var error .proxyURL, = proxyURLForTarget(.Endpoint()) if != nil { return nil, fmt.Errorf("delegating_resolver: failed to determine proxy URL for target %s: %v", , ) } // proxy is not configured or proxy address excluded using `NO_PROXY` env // var, so only target resolver is used. if .proxyURL == nil { return .Build(, , ) } if logger.V(2) { logger.Infof("Proxy URL detected : %s", .proxyURL) } // Resolver updates from one child may trigger calls into the other. Block // updates until the children are initialized. .childMu.Lock() defer .childMu.Unlock() // 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 .URL.Scheme == "dns" && ! { .targetResolverState = &resolver.State{ Addresses: []resolver.Address{{Addr: .Endpoint()}}, Endpoints: []resolver.Endpoint{{Addresses: []resolver.Address{{Addr: .Endpoint()}}}}, } .updateTargetResolverState(*.targetResolverState) return , nil } := &wrappingClientConn{ stateListener: .updateTargetResolverState, parent: , } if .targetResolver, = .Build(, , ); != nil { return nil, fmt.Errorf("delegating_resolver: unable to build the resolver for target %s: %v", , ) } return , 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 ( *delegatingResolver) ( resolver.BuildOptions) (resolver.Resolver, error) { := resolver.Get("dns") if == nil { panic("delegating_resolver: resolver for proxy not found for scheme dns") } := *.proxyURL .Scheme = "dns" .Path = "/" + .proxyURL.Host .Host = "" // Clear the Host field to conform to the "dns:///" format := resolver.Target{URL: } := &wrappingClientConn{ stateListener: .updateProxyResolverState, parent: , } return .Build(, , ) } func ( *delegatingResolver) ( resolver.ResolveNowOptions) { .childMu.Lock() defer .childMu.Unlock() .targetResolver.ResolveNow() .proxyResolver.ResolveNow() } func ( *delegatingResolver) () { .childMu.Lock() defer .childMu.Unlock() .targetResolver.Close() .targetResolver = nil .proxyResolver.Close() .proxyResolver = nil } func ( *resolver.State) bool { for , := range .Addresses { if !skipProxy() { return true } } for , := range .Endpoints { for , := range .Addresses { if !skipProxy() { return true } } } return false } func ( resolver.Address) bool { // Avoid proxy when network is not tcp. , := networktype.Get() if ! { , _ = transport.ParseDialTarget(.Addr) } if != "tcp" { return true } := &http.Request{URL: &url.URL{ Scheme: "https", Host: .Addr, }} // Avoid proxy when address included in `NO_PROXY` environment variable or // fails to get the proxy address. , := HTTPSProxyFromEnvironment() if != nil || == nil { return true } return false } // updateClientConnStateLocked constructs a combined list of addresses by // pairing each proxy address with every target address of type TCP. For each // pair, it creates a new [resolver.Address] using the proxy address and // attaches the corresponding target address and user info as attributes. Target // addresses that are not of type TCP are appended to the list as-is. The // function returns nil if either resolver has not yet provided an update, and // returns the result of ClientConn.UpdateState once both resolvers have // provided at least one update. func ( *delegatingResolver) () error { if .targetResolverState == nil || .proxyAddrs == nil { return nil } // 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 resolver.Address if len(.proxyAddrs) == 1 { = .proxyAddrs[0] } else { = resolver.Address{Addr: .proxyURL.Host} } var []resolver.Address for , := range (*.targetResolverState).Addresses { if skipProxy() { = append(, ) continue } = append(, proxyattributes.Set(, proxyattributes.Options{ User: .proxyURL.User, ConnectAddr: .Addr, })) } // For each target endpoint, construct a new [resolver.Endpoint] that // includes all addresses from all proxy endpoints and the addresses from // that target endpoint, preserving the number of target endpoints. var []resolver.Endpoint for , := range (*.targetResolverState).Endpoints { var []resolver.Address for , := range .Addresses { // Avoid proxy when network is not tcp. if skipProxy() { = append(, ) continue } for , := range .proxyAddrs { = append(, proxyattributes.Set(, proxyattributes.Options{ User: .proxyURL.User, ConnectAddr: .Addr, })) } } = append(, resolver.Endpoint{Addresses: }) } // 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. := *.targetResolverState .Addresses = .Endpoints = return .cc.UpdateState() } // 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 ( *delegatingResolver) ( resolver.State) error { .mu.Lock() defer .mu.Unlock() if logger.V(2) { logger.Infof("Addresses received from proxy resolver: %s", .Addresses) } if len(.Endpoints) > 0 { // We expect exactly one address per endpoint because the proxy resolver // uses "dns" resolution. .proxyAddrs = make([]resolver.Address, 0, len(.Endpoints)) for , := range .Endpoints { .proxyAddrs = append(.proxyAddrs, .Addresses...) } } else if .Addresses != nil { .proxyAddrs = .Addresses } else { .proxyAddrs = []resolver.Address{} // ensure proxyAddrs is non-nil to indicate an update has been received } := .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 != nil { go func() { .childMu.Lock() defer .childMu.Unlock() if .targetResolver != nil { .targetResolver.ResolveNow(resolver.ResolveNowOptions{}) } }() } return } // updateTargetResolverState is the StateListener function provided to the // target resolver via wrappingClientConn. It updates the resolver state and // marks the target resolver as ready. If the update includes at least one TCP // address and the proxy resolver has not yet been constructed, it initializes // the proxy resolver. A combined state update is triggered once both resolvers // are ready. If all addresses are non-TCP, it proceeds without waiting for the // proxy resolver. If ClientConn.UpdateState returns a non-nil error, // ResolveNow() is called on the proxy resolver. func ( *delegatingResolver) ( resolver.State) error { .mu.Lock() defer .mu.Unlock() if logger.V(2) { logger.Infof("Addresses received from target resolver: %v", .Addresses) } .targetResolverState = & // If all addresses returned by the target resolver have a non-TCP network // type, or are listed in the `NO_PROXY` environment variable, do not wait // for proxy update. if !needsProxyResolver(.targetResolverState) { return .cc.UpdateState(*.targetResolverState) } // The proxy resolver may be rebuilt multiple times, specifically each time // the target resolver sends an update, even if the target resolver is built // successfully but building the proxy resolver fails. if len(.proxyAddrs) == 0 { go func() { .childMu.Lock() defer .childMu.Unlock() if , := .proxyResolver.(nopResolver); ! { return } , := .proxyURIResolver(resolver.BuildOptions{}) if != nil { .cc.ReportError(fmt.Errorf("delegating_resolver: unable to build the proxy resolver: %v", )) return } .proxyResolver = }() } := .updateClientConnStateLocked() if != nil { go func() { .childMu.Lock() defer .childMu.Unlock() if .proxyResolver != nil { .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 ( *wrappingClientConn) ( resolver.State) error { return .stateListener() } // ReportError intercepts errors from the child resolvers and passes them to // ClientConn. func ( *wrappingClientConn) ( error) { .parent.cc.ReportError() } // NewAddress intercepts the new resolved address from the child resolvers and // passes them to ClientConn. func ( *wrappingClientConn) ( []resolver.Address) { .UpdateState(resolver.State{Addresses: }) } // ParseServiceConfig parses the provided service config and returns an object // that provides the parsed config. func ( *wrappingClientConn) ( string) *serviceconfig.ParseResult { return .parent.cc.ParseServiceConfig() }