/* * * 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 delegatingresolverimport ()var (logger = grpclog.Component("delegating-resolver")// HTTPSProxyFromEnvironment will be overwritten in the testsHTTPSProxyFromEnvironment = 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.typedelegatingResolverstruct {targetresolver.Target// parsed target URI to be resolvedccresolver.ClientConn// gRPC ClientConnproxyURL *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.musync.Mutex// protects all the fields belowtargetResolverState *resolver.State// state of the target resolverproxyAddrs []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.childMusync.MutextargetResolverresolver.Resolver// resolver for the target URI, based on its schemeproxyResolverresolver.Resolver// resolver for the proxy URI; nil if no proxy is configured}// nopResolver is a resolver that does nothing.typenopResolverstruct{}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: , }}returnHTTPSProxyFromEnvironment()}// 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{}, }varerror .proxyURL, = proxyURLForTarget(.Endpoint())if != nil {returnnil, 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(, , ) }iflogger.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 {returnnil, 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() {returntrue } }for , := range .Endpoints {for , := range .Addresses {if !skipProxy() {returntrue } } }returnfalse}func ( resolver.Address) bool {// Avoid proxy when network is not tcp. , := networktype.Get()if ! { , _ = transport.ParseDialTarget(.Addr) }if != "tcp" {returntrue } := &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 {returntrue }returnfalse}// 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 {returnnil }// 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.varresolver.Addressiflen(.proxyAddrs) == 1 { = .proxyAddrs[0] } else { = resolver.Address{Addr: .proxyURL.Host} }var []resolver.Addressfor , := range (*.targetResolverState).Addresses {ifskipProxy() { = 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.Endpointfor , := range (*.targetResolverState).Endpoints {var []resolver.Addressfor , := range .Addresses {// Avoid proxy when network is not tcp.ifskipProxy() { = 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()iflogger.V(2) {logger.Infof("Addresses received from proxy resolver: %s", .Addresses) }iflen(.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...) } } elseif .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 {gofunc() { .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()iflogger.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.iflen(.proxyAddrs) == 0 {gofunc() { .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 {gofunc() { .childMu.Lock()defer .childMu.Unlock()if .proxyResolver != nil { .proxyResolver.ResolveNow(resolver.ResolveNowOptions{}) } }() }returnnil}// 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.typewrappingClientConnstruct {// Callback to deliver resolver state updatesstateListenerfunc(state resolver.State) errorparent *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()}
The pages are generated with Goldsv0.7.6. (GOOS=linux GOARCH=amd64)