1/*
2 *
3 * Copyright 2024 gRPC authors.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 *
17 */
18
19// Package delegatingresolver implements a resolver capable of resolving both
20// target URIs and proxy addresses.
21package delegatingresolver
22
23import (
24 "fmt"
25 "net/http"
26 "net/url"
27 "sync"
28
29 "google.golang.org/grpc/grpclog"
30 "google.golang.org/grpc/internal/proxyattributes"
31 "google.golang.org/grpc/resolver"
32 "google.golang.org/grpc/serviceconfig"
33)
34
35var (
36 logger = grpclog.Component("delegating-resolver")
37 // HTTPSProxyFromEnvironment will be overwritten in the tests
38 HTTPSProxyFromEnvironment = http.ProxyFromEnvironment
39)
40
41// delegatingResolver manages both target URI and proxy address resolution by
42// delegating these tasks to separate child resolvers. Essentially, it acts as
43// a intermediary between the gRPC ClientConn and the child resolvers.
44//
45// It implements the [resolver.Resolver] interface.
46type delegatingResolver struct {
47 target resolver.Target // parsed target URI to be resolved
48 cc resolver.ClientConn // gRPC ClientConn
49 targetResolver resolver.Resolver // resolver for the target URI, based on its scheme
50 proxyResolver resolver.Resolver // resolver for the proxy URI; nil if no proxy is configured
51 proxyURL *url.URL // proxy URL, derived from proxy environment and target
52
53 mu sync.Mutex // protects all the fields below
54 targetResolverState *resolver.State // state of the target resolver
55 proxyAddrs []resolver.Address // resolved proxy addresses; empty if no proxy is configured
56}
57
58// nopResolver is a resolver that does nothing.
59type nopResolver struct{}
60
61func (nopResolver) ResolveNow(resolver.ResolveNowOptions) {}
62
63func (nopResolver) Close() {}
64
65// proxyURLForTarget determines the proxy URL for the given address based on
66// the environment. It can return the following:
67// - nil URL, nil error: No proxy is configured or the address is excluded
68// using the `NO_PROXY` environment variable or if req.URL.Host is
69// "localhost" (with or without // a port number)
70// - nil URL, non-nil error: An error occurred while retrieving the proxy URL.
71// - non-nil URL, nil error: A proxy is configured, and the proxy URL was
72// retrieved successfully without any errors.
73func proxyURLForTarget(address string) (*url.URL, error) {
74 req := &http.Request{URL: &url.URL{
75 Scheme: "https",
76 Host: address,
77 }}
78 return HTTPSProxyFromEnvironment(req)
79}
80
81// New creates a new delegating resolver that can create up to two child
82// resolvers:
83// - one to resolve the proxy address specified using the supported
84// environment variables. This uses the registered resolver for the "dns"
85// scheme.
86// - one to resolve the target URI using the resolver specified by the scheme
87// in the target URI or specified by the user using the WithResolvers dial
88// option. As a special case, if the target URI's scheme is "dns" and a
89// proxy is specified using the supported environment variables, the target
90// URI's path portion is used as the resolved address unless target
91// resolution is enabled using the dial option.
92func New(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions, targetResolverBuilder resolver.Builder, targetResolutionEnabled bool) (resolver.Resolver, error) {
93 r := &delegatingResolver{
94 target: target,
95 cc: cc,
96 }
97
98 var err error
99 r.proxyURL, err = proxyURLForTarget(target.Endpoint())
100 if err != nil {
101 return nil, fmt.Errorf("delegating_resolver: failed to determine proxy URL for target %s: %v", target, err)
102 }
103
104 // proxy is not configured or proxy address excluded using `NO_PROXY` env
105 // var, so only target resolver is used.
106 if r.proxyURL == nil {
107 return targetResolverBuilder.Build(target, cc, opts)
108 }
109
110 if logger.V(2) {
111 logger.Infof("Proxy URL detected : %s", r.proxyURL)
112 }
113
114 // When the scheme is 'dns' and target resolution on client is not enabled,
115 // resolution should be handled by the proxy, not the client. Therefore, we
116 // bypass the target resolver and store the unresolved target address.
117 if target.URL.Scheme == "dns" && !targetResolutionEnabled {
118 state := resolver.State{
119 Addresses: []resolver.Address{{Addr: target.Endpoint()}},
120 Endpoints: []resolver.Endpoint{{Addresses: []resolver.Address{{Addr: target.Endpoint()}}}},
121 }
122 r.targetResolverState = &state
123 } else {
124 wcc := &wrappingClientConn{
125 stateListener: r.updateTargetResolverState,
126 parent: r,
127 }
128 if r.targetResolver, err = targetResolverBuilder.Build(target, wcc, opts); err != nil {
129 return nil, fmt.Errorf("delegating_resolver: unable to build the resolver for target %s: %v", target, err)
130 }
131 }
132
133 if r.proxyResolver, err = r.proxyURIResolver(opts); err != nil {
134 return nil, fmt.Errorf("delegating_resolver: failed to build resolver for proxy URL %q: %v", r.proxyURL, err)
135 }
136
137 if r.targetResolver == nil {
138 r.targetResolver = nopResolver{}
139 }
140 if r.proxyResolver == nil {
141 r.proxyResolver = nopResolver{}
142 }
143 return r, nil
144}
145
146// proxyURIResolver creates a resolver for resolving proxy URIs using the
147// "dns" scheme. It adjusts the proxyURL to conform to the "dns:///" format and
148// builds a resolver with a wrappingClientConn to capture resolved addresses.
149func (r *delegatingResolver) proxyURIResolver(opts resolver.BuildOptions) (resolver.Resolver, error) {
150 proxyBuilder := resolver.Get("dns")
151 if proxyBuilder == nil {
152 panic("delegating_resolver: resolver for proxy not found for scheme dns")
153 }
154 url := *r.proxyURL
155 url.Scheme = "dns"
156 url.Path = "/" + r.proxyURL.Host
157 url.Host = "" // Clear the Host field to conform to the "dns:///" format
158
159 proxyTarget := resolver.Target{URL: url}
160 wcc := &wrappingClientConn{
161 stateListener: r.updateProxyResolverState,
162 parent: r,
163 }
164 return proxyBuilder.Build(proxyTarget, wcc, opts)
165}
166
167func (r *delegatingResolver) ResolveNow(o resolver.ResolveNowOptions) {
168 r.targetResolver.ResolveNow(o)
169 r.proxyResolver.ResolveNow(o)
170}
171
172func (r *delegatingResolver) Close() {
173 r.targetResolver.Close()
174 r.targetResolver = nil
175
176 r.proxyResolver.Close()
177 r.proxyResolver = nil
178}
179
180// updateClientConnStateLocked creates a list of combined addresses by
181// pairing each proxy address with every target address. For each pair, it
182// generates a new [resolver.Address] using the proxy address, and adding the
183// target address as the attribute along with user info. It returns nil if
184// either resolver has not sent update even once and returns the error from
185// ClientConn update once both resolvers have sent update atleast once.
186func (r *delegatingResolver) updateClientConnStateLocked() error {
187 if r.targetResolverState == nil || r.proxyAddrs == nil {
188 return nil
189 }
190
191 curState := *r.targetResolverState
192 // If multiple resolved proxy addresses are present, we send only the
193 // unresolved proxy host and let net.Dial handle the proxy host name
194 // resolution when creating the transport. Sending all resolved addresses
195 // would increase the number of addresses passed to the ClientConn and
196 // subsequently to load balancing (LB) policies like Round Robin, leading
197 // to additional TCP connections. However, if there's only one resolved
198 // proxy address, we send it directly, as it doesn't affect the address
199 // count returned by the target resolver and the address count sent to the
200 // ClientConn.
201 var proxyAddr resolver.Address
202 if len(r.proxyAddrs) == 1 {
203 proxyAddr = r.proxyAddrs[0]
204 } else {
205 proxyAddr = resolver.Address{Addr: r.proxyURL.Host}
206 }
207 var addresses []resolver.Address
208 for _, targetAddr := range (*r.targetResolverState).Addresses {
209 addresses = append(addresses, proxyattributes.Set(proxyAddr, proxyattributes.Options{
210 User: r.proxyURL.User,
211 ConnectAddr: targetAddr.Addr,
212 }))
213 }
214
215 // Create a list of combined endpoints by pairing all proxy endpoints
216 // with every target endpoint. Each time, it constructs a new
217 // [resolver.Endpoint] using the all addresses from all the proxy endpoint
218 // and the target addresses from one endpoint. The target address and user
219 // information from the proxy URL are added as attributes to the proxy
220 // address.The resulting list of addresses is then grouped into endpoints,
221 // covering all combinations of proxy and target endpoints.
222 var endpoints []resolver.Endpoint
223 for _, endpt := range (*r.targetResolverState).Endpoints {
224 var addrs []resolver.Address
225 for _, proxyAddr := range r.proxyAddrs {
226 for _, targetAddr := range endpt.Addresses {
227 addrs = append(addrs, proxyattributes.Set(proxyAddr, proxyattributes.Options{
228 User: r.proxyURL.User,
229 ConnectAddr: targetAddr.Addr,
230 }))
231 }
232 }
233 endpoints = append(endpoints, resolver.Endpoint{Addresses: addrs})
234 }
235 // Use the targetResolverState for its service config and attributes
236 // contents. The state update is only sent after both the target and proxy
237 // resolvers have sent their updates, and curState has been updated with
238 // the combined addresses.
239 curState.Addresses = addresses
240 curState.Endpoints = endpoints
241 return r.cc.UpdateState(curState)
242}
243
244// updateProxyResolverState updates the proxy resolver state by storing proxy
245// addresses and endpoints, marking the resolver as ready, and triggering a
246// state update if both proxy and target resolvers are ready. If the ClientConn
247// returns a non-nil error, it calls `ResolveNow()` on the target resolver. It
248// is a StateListener function of wrappingClientConn passed to the proxy resolver.
249func (r *delegatingResolver) updateProxyResolverState(state resolver.State) error {
250 r.mu.Lock()
251 defer r.mu.Unlock()
252 if logger.V(2) {
253 logger.Infof("Addresses received from proxy resolver: %s", state.Addresses)
254 }
255 if len(state.Endpoints) > 0 {
256 // We expect exactly one address per endpoint because the proxy
257 // resolver uses "dns" resolution.
258 r.proxyAddrs = make([]resolver.Address, 0, len(state.Endpoints))
259 for _, endpoint := range state.Endpoints {
260 r.proxyAddrs = append(r.proxyAddrs, endpoint.Addresses...)
261 }
262 } else if state.Addresses != nil {
263 r.proxyAddrs = state.Addresses
264 } else {
265 r.proxyAddrs = []resolver.Address{} // ensure proxyAddrs is non-nil to indicate an update has been received
266 }
267 err := r.updateClientConnStateLocked()
268 // Another possible approach was to block until updates are received from
269 // both resolvers. But this is not used because calling `New()` triggers
270 // `Build()` for the first resolver, which calls `UpdateState()`. And the
271 // second resolver hasn't sent an update yet, so it would cause `New()` to
272 // block indefinitely.
273 if err != nil {
274 r.targetResolver.ResolveNow(resolver.ResolveNowOptions{})
275 }
276 return err
277}
278
279// updateTargetResolverState updates the target resolver state by storing target
280// addresses, endpoints, and service config, marking the resolver as ready, and
281// triggering a state update if both resolvers are ready. If the ClientConn
282// returns a non-nil error, it calls `ResolveNow()` on the proxy resolver. It
283// is a StateListener function of wrappingClientConn passed to the target resolver.
284func (r *delegatingResolver) updateTargetResolverState(state resolver.State) error {
285 r.mu.Lock()
286 defer r.mu.Unlock()
287
288 if logger.V(2) {
289 logger.Infof("Addresses received from target resolver: %v", state.Addresses)
290 }
291 r.targetResolverState = &state
292 err := r.updateClientConnStateLocked()
293 if err != nil {
294 r.proxyResolver.ResolveNow(resolver.ResolveNowOptions{})
295 }
296 return nil
297}
298
299// wrappingClientConn serves as an intermediary between the parent ClientConn
300// and the child resolvers created here. It implements the resolver.ClientConn
301// interface and is passed in that capacity to the child resolvers.
302type wrappingClientConn struct {
303 // Callback to deliver resolver state updates
304 stateListener func(state resolver.State) error
305 parent *delegatingResolver
306}
307
308// UpdateState receives resolver state updates and forwards them to the
309// appropriate listener function (either for the proxy or target resolver).
310func (wcc *wrappingClientConn) UpdateState(state resolver.State) error {
311 return wcc.stateListener(state)
312}
313
314// ReportError intercepts errors from the child resolvers and passes them to ClientConn.
315func (wcc *wrappingClientConn) ReportError(err error) {
316 wcc.parent.cc.ReportError(err)
317}
318
319// NewAddress intercepts the new resolved address from the child resolvers and
320// passes them to ClientConn.
321func (wcc *wrappingClientConn) NewAddress(addrs []resolver.Address) {
322 wcc.UpdateState(resolver.State{Addresses: addrs})
323}
324
325// ParseServiceConfig parses the provided service config and returns an
326// object that provides the parsed config.
327func (wcc *wrappingClientConn) ParseServiceConfig(serviceConfigJSON string) *serviceconfig.ParseResult {
328 return wcc.parent.cc.ParseServiceConfig(serviceConfigJSON)
329}