Skip to content

Commit 619dcd4

Browse files
Change proxy behaviour
1 parent 274830d commit 619dcd4

12 files changed

+1417
-335
lines changed

clientconn.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ func Dial(target string, opts ...DialOption) (*ClientConn, error) {
225225
func DialContext(ctx context.Context, target string, opts ...DialOption) (conn *ClientConn, err error) {
226226
// At the end of this method, we kick the channel out of idle, rather than
227227
// waiting for the first rpc.
228-
opts = append([]DialOption{withDefaultScheme("passthrough")}, opts...)
228+
opts = append([]DialOption{withDefaultScheme("passthrough"), WithTargetResolutionEnabled()}, opts...)
229229
cc, err := NewClient(target, opts...)
230230
if err != nil {
231231
return nil, err

dialoptions.go

+20-6
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ type dialOptions struct {
9494
idleTimeout time.Duration
9595
defaultScheme string
9696
maxCallAttempts int
97+
// TargetResolutionEnabled specifies if the target resolution is enabled even
98+
// when proxy is enabled.
99+
TargetResolutionEnabled bool
100+
// UseProxy specifies if a proxy should be used.
101+
UseProxy bool
97102
}
98103

99104
// DialOption configures how we set up the connection.
@@ -377,7 +382,15 @@ func WithInsecure() DialOption {
377382
// later release.
378383
func WithNoProxy() DialOption {
379384
return newFuncDialOption(func(o *dialOptions) {
380-
o.copts.UseProxy = false
385+
o.UseProxy = false
386+
})
387+
}
388+
389+
// WithTargetResolutionEnabled returns a DialOption which enables target
390+
// resolution on client.
391+
func WithTargetResolutionEnabled() DialOption {
392+
return newFuncDialOption(func(o *dialOptions) {
393+
o.TargetResolutionEnabled = true
381394
})
382395
}
383396

@@ -662,14 +675,15 @@ func defaultDialOptions() dialOptions {
662675
copts: transport.ConnectOptions{
663676
ReadBufferSize: defaultReadBufSize,
664677
WriteBufferSize: defaultWriteBufSize,
665-
UseProxy: true,
666678
UserAgent: grpcUA,
667679
BufferPool: mem.DefaultBufferPool(),
668680
},
669-
bs: internalbackoff.DefaultExponential,
670-
idleTimeout: 30 * time.Minute,
671-
defaultScheme: "dns",
672-
maxCallAttempts: defaultMaxCallAttempts,
681+
bs: internalbackoff.DefaultExponential,
682+
idleTimeout: 30 * time.Minute,
683+
defaultScheme: "dns",
684+
maxCallAttempts: defaultMaxCallAttempts,
685+
UseProxy: true,
686+
TargetResolutionEnabled: false,
673687
}
674688
}
675689

internal/attributes/attributes.go

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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 attributes contains functions for getting and setting attributes.
20+
package attributes
21+
22+
import (
23+
"net/url"
24+
25+
"google.golang.org/grpc/resolver"
26+
)
27+
28+
type keyType string
29+
30+
const userAndConnectAddrKey = keyType("grpc.resolver.delegatingresolver.userAndConnectAddr")
31+
32+
type attr struct {
33+
user *url.Userinfo
34+
addr string
35+
}
36+
37+
// SetUserAndConnectAddr returns a copy of the provided resolver.Address with
38+
// attributes containing address to be sent in connect request to proxy and the
39+
// user info. It's data should not be mutated after calling SetConnectAddr.
40+
func SetUserAndConnectAddr(resAddr resolver.Address, user *url.Userinfo, addr string) resolver.Address {
41+
resAddr.Attributes = resAddr.Attributes.WithValue(userAndConnectAddrKey, attr{user: user, addr: addr})
42+
return resAddr
43+
}
44+
45+
// ProxyConnectAddr returns the proxy connect address in resolver.Address, or nil
46+
// if not present. The returned data should not be mutated.
47+
func ProxyConnectAddr(addr resolver.Address) string {
48+
attribute := addr.Attributes.Value(userAndConnectAddrKey)
49+
if attribute != nil {
50+
return attribute.(attr).addr
51+
}
52+
return ""
53+
}
54+
55+
// User returns the user info in the resolver.Address, or nil if not present.
56+
// The returned data should not be mutated.
57+
func User(addr resolver.Address) *url.Userinfo {
58+
attribute := addr.Attributes.Value(userAndConnectAddrKey)
59+
if attribute != nil {
60+
return attribute.(attr).user
61+
}
62+
return nil
63+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
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 defines a resolver that can handle both target URI
20+
// and proxy address resolution, unless:
21+
// - A custom dialer is set using WithContextDialer dialoption.
22+
// - Proxy usage is explicitly disabled using WithNoProxy dialoption.
23+
// - Client-side resolution is explicitly enforced using WithTargetResolutionEnabled.
24+
package delegatingresolver
25+
26+
import (
27+
"fmt"
28+
"net/http"
29+
"net/url"
30+
"sync"
31+
32+
"google.golang.org/grpc/grpclog"
33+
"google.golang.org/grpc/internal/attributes"
34+
"google.golang.org/grpc/resolver"
35+
"google.golang.org/grpc/serviceconfig"
36+
)
37+
38+
var (
39+
// HTTPSProxyFromEnvironment will be used and overwritten in the tests.
40+
HTTPSProxyFromEnvironment = http.ProxyFromEnvironment
41+
// ProxyScheme will be ovwewritten in tests
42+
ProxyScheme = "dns"
43+
logger = grpclog.Component("delegating-resolver")
44+
)
45+
46+
// delegatingResolver implements the `resolver.Resolver` interface. It uses child
47+
// resolvers for the target and proxy resolution. It acts as an intermediatery
48+
// between the child resolvers and the gRPC ClientConn.
49+
type delegatingResolver struct {
50+
target resolver.Target // parsed target URI to be resolved
51+
cc resolver.ClientConn // gRPC ClientConn
52+
targetResolver resolver.Resolver // resolver for the target URI, based on its scheme
53+
proxyResolver resolver.Resolver // resolver for the proxy URI; nil if no proxy is configured
54+
55+
mu sync.Mutex // protects access to the resolver state and addresses during updates
56+
targetAddrs []resolver.Address // resolved or unresolved target addresses, depending on proxy configuration
57+
proxyAddrs []resolver.Address // resolved proxy addresses; empty if no proxy is configured
58+
proxyURL *url.URL // proxy URL, derived from proxy environment and target
59+
60+
targetResolverReady bool // indicates if an update from the target resolver has been received
61+
proxyResolverReady bool // indicates if an update from the proxy resolver has been received
62+
}
63+
64+
func parsedURLForProxy(address string) (*url.URL, error) {
65+
req := &http.Request{URL: &url.URL{Scheme: "https", Host: address}}
66+
url, err := HTTPSProxyFromEnvironment(req)
67+
if err != nil {
68+
return nil, err
69+
}
70+
return url, nil
71+
}
72+
73+
// OnClientResolution is a no-op function in non-test code. In tests, it can
74+
// be overwritten to send a signal to a channel, indicating that client-side
75+
// name resolution was triggered. This enables tests to verify that resolution
76+
// is bypassed when a proxy is in use.
77+
var OnClientResolution = func(int) { /* no-op */ }
78+
79+
// New creates a new delegating resolver that is used to call the target and
80+
// proxy child resolver. If proxy is configured and target endpoint points
81+
// correctly points to proxy, both proxy and target resolvers are used else
82+
// only target resolver is used.
83+
//
84+
// For target resolver, if scheme is dns and target resolution is not enabled,
85+
// it stores unresolved target address, bypassing target resolution at the
86+
// client and resolution happens at the proxy server otherwise it resolves
87+
// and store the resolved address.
88+
//
89+
// It returns error if proxy is configured but proxy target doesn't parse to
90+
// correct url or if target resolution at client fails.
91+
func New(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions, targetResolverBuilder resolver.Builder, targetResolutionEnabled bool) (resolver.Resolver, error) {
92+
r := &delegatingResolver{target: target, cc: cc}
93+
94+
var err error
95+
r.proxyURL, err = parsedURLForProxy(target.Endpoint())
96+
// if proxy is configured but proxy target is wrong, so return early with error.
97+
if err != nil {
98+
return nil, fmt.Errorf("failed to determine proxy URL for %v target endpoint: %v", target.Endpoint(), err)
99+
}
100+
101+
// proxy is not configured or proxy address excluded using `NO_PROXY` env var,
102+
// so only target resolver is used.
103+
if r.proxyURL == nil {
104+
OnClientResolution(1)
105+
return targetResolverBuilder.Build(target, cc, opts)
106+
}
107+
108+
if logger.V(2) {
109+
logger.Info("Proxy URL detected : %+v", r.proxyURL)
110+
}
111+
// When the scheme is 'dns' and target resolution on client is not enabled,
112+
// resolution should be handled by the proxy, not the client. Therefore, we
113+
// bypass the target resolver and store the unresolved target address.
114+
if target.URL.Scheme == "dns" && !targetResolutionEnabled {
115+
r.targetAddrs = []resolver.Address{{Addr: target.Endpoint()}}
116+
r.targetResolverReady = true
117+
} else {
118+
OnClientResolution(1)
119+
if r.targetResolver, err = targetResolverBuilder.Build(target, &wrappingClientConn{parent: r, resolverType: targetResolverType}, opts); err != nil {
120+
return nil, fmt.Errorf("delegating_resolver: unable to build the resolver for target %v : %v", target, err)
121+
}
122+
}
123+
124+
if r.proxyResolver, err = r.proxyURIResolver(opts); err != nil {
125+
return nil, err
126+
}
127+
return r, nil
128+
}
129+
130+
func (r *delegatingResolver) proxyURIResolver(opts resolver.BuildOptions) (resolver.Resolver, error) {
131+
proxyBuilder := resolver.Get(ProxyScheme)
132+
if proxyBuilder == nil {
133+
panic(fmt.Sprintln("delegating_resolver: resolver for proxy not found for scheme dns"))
134+
}
135+
r.proxyURL.Scheme = "dns"
136+
r.proxyURL.Path = "/" + r.proxyURL.Host
137+
r.proxyURL.Host = "" // Clear the Host field to conform to the "dns:///" format
138+
proxyTarget := resolver.Target{URL: *r.proxyURL}
139+
return proxyBuilder.Build(proxyTarget, &wrappingClientConn{parent: r, resolverType: proxyResolverType}, opts)
140+
}
141+
142+
func (r *delegatingResolver) ResolveNow(o resolver.ResolveNowOptions) {
143+
if r.targetResolver != nil {
144+
r.targetResolver.ResolveNow(o)
145+
}
146+
if r.proxyResolver != nil {
147+
r.proxyResolver.ResolveNow(o)
148+
}
149+
}
150+
151+
func (r *delegatingResolver) Close() {
152+
if r.targetResolver != nil {
153+
r.targetResolver.Close()
154+
}
155+
if r.proxyResolver != nil {
156+
r.proxyResolver.Close()
157+
}
158+
}
159+
160+
func (r *delegatingResolver) updateState() []resolver.Address {
161+
var addresses []resolver.Address
162+
for _, proxyAddr := range r.proxyAddrs {
163+
for _, targetAddr := range r.targetAddrs {
164+
newAddr := resolver.Address{Addr: proxyAddr.Addr}
165+
newAddr = attributes.SetUserAndConnectAddr(newAddr, r.proxyURL.User, targetAddr.Addr)
166+
addresses = append(addresses, newAddr)
167+
}
168+
}
169+
// return the combined addresses.
170+
return addresses
171+
}
172+
173+
// resolverType is an enum representing the type of resolver (target or proxy).
174+
type resolverType int
175+
176+
const (
177+
targetResolverType resolverType = iota
178+
proxyResolverType
179+
)
180+
181+
type wrappingClientConn struct {
182+
parent *delegatingResolver
183+
resolverType resolverType // represents the type of resolver (target or proxy)
184+
}
185+
186+
// UpdateState intercepts state updates from the target and proxy resolvers.
187+
func (wcc *wrappingClientConn) UpdateState(state resolver.State) error {
188+
wcc.parent.mu.Lock()
189+
defer wcc.parent.mu.Unlock()
190+
var curState resolver.State
191+
if wcc.resolverType == targetResolverType {
192+
wcc.parent.targetAddrs = state.Addresses
193+
logger.Infof("%v addresses received from target resolver", len(wcc.parent.targetAddrs))
194+
wcc.parent.targetResolverReady = true
195+
curState = state
196+
}
197+
if wcc.resolverType == proxyResolverType {
198+
wcc.parent.proxyAddrs = state.Addresses
199+
logger.Infof("%v addresses received from proxy resolver", len(wcc.parent.proxyAddrs))
200+
wcc.parent.proxyResolverReady = true
201+
}
202+
203+
// Proceed only if updates from both resolvers have been received.
204+
if !wcc.parent.targetResolverReady || !wcc.parent.proxyResolverReady {
205+
return nil
206+
}
207+
curState.Addresses = wcc.parent.updateState()
208+
return wcc.parent.cc.UpdateState(curState)
209+
}
210+
211+
// ReportError intercepts errors from the child resolvers and pass to ClientConn.
212+
func (wcc *wrappingClientConn) ReportError(err error) {
213+
wcc.parent.cc.ReportError(err)
214+
}
215+
216+
// NewAddress intercepts the new resolved address from the child resolvers and
217+
// pass to ClientConn.
218+
func (wcc *wrappingClientConn) NewAddress(addrs []resolver.Address) {
219+
wcc.UpdateState(resolver.State{Addresses: addrs})
220+
}
221+
222+
// ParseServiceConfig parses the provided service config and returns an
223+
// object that provides the parsed config.
224+
func (wcc *wrappingClientConn) ParseServiceConfig(serviceConfigJSON string) *serviceconfig.ParseResult {
225+
return wcc.parent.cc.ParseServiceConfig(serviceConfigJSON)
226+
}

0 commit comments

Comments
 (0)