Skip to content

Commit 2fd426d

Browse files
transport,grpc: Integrate delegating resolver and introduce dial options for target host resolution (#7881)
* Change proxy behaviour
1 parent 66f6471 commit 2fd426d

File tree

12 files changed

+748
-274
lines changed

12 files changed

+748
-274
lines changed

clientconn.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,12 @@ 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+
//
229+
// WithLocalDNSResolution dial option in `grpc.Dial` ensures that it
230+
// preserves behavior: when default scheme passthrough is used, skip
231+
// hostname resolution, when "dns" is used for resolution, perform
232+
// resolution on the client.
233+
opts = append([]DialOption{withDefaultScheme("passthrough"), WithLocalDNSResolution()}, opts...)
229234
cc, err := NewClient(target, opts...)
230235
if err != nil {
231236
return nil, err

dialoptions.go

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ type dialOptions struct {
9494
idleTimeout time.Duration
9595
defaultScheme string
9696
maxCallAttempts int
97+
enableLocalDNSResolution bool // Specifies if target hostnames should be resolved when proxying is enabled.
98+
useProxy bool // Specifies if a server should be connected via proxy.
9799
}
98100

99101
// DialOption configures how we set up the connection.
@@ -377,7 +379,22 @@ func WithInsecure() DialOption {
377379
// later release.
378380
func WithNoProxy() DialOption {
379381
return newFuncDialOption(func(o *dialOptions) {
380-
o.copts.UseProxy = false
382+
o.useProxy = false
383+
})
384+
}
385+
386+
// WithLocalDNSResolution forces local DNS name resolution even when a proxy is
387+
// specified in the environment. By default, the server name is provided
388+
// directly to the proxy as part of the CONNECT handshake. This is ignored if
389+
// WithNoProxy is used.
390+
//
391+
// # Experimental
392+
//
393+
// Notice: This API is EXPERIMENTAL and may be changed or removed in a
394+
// later release.
395+
func WithLocalDNSResolution() DialOption {
396+
return newFuncDialOption(func(o *dialOptions) {
397+
o.enableLocalDNSResolution = true
381398
})
382399
}
383400

@@ -667,14 +684,15 @@ func defaultDialOptions() dialOptions {
667684
copts: transport.ConnectOptions{
668685
ReadBufferSize: defaultReadBufSize,
669686
WriteBufferSize: defaultWriteBufSize,
670-
UseProxy: true,
671687
UserAgent: grpcUA,
672688
BufferPool: mem.DefaultBufferPool(),
673689
},
674-
bs: internalbackoff.DefaultExponential,
675-
idleTimeout: 30 * time.Minute,
676-
defaultScheme: "dns",
677-
maxCallAttempts: defaultMaxCallAttempts,
690+
bs: internalbackoff.DefaultExponential,
691+
idleTimeout: 30 * time.Minute,
692+
defaultScheme: "dns",
693+
maxCallAttempts: defaultMaxCallAttempts,
694+
useProxy: true,
695+
enableLocalDNSResolution: false,
678696
}
679697
}
680698

internal/proxyattributes/proxyattributes.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ const proxyOptionsKey = keyType("grpc.resolver.delegatingresolver.proxyOptions")
3333
// Options holds the proxy connection details needed during the CONNECT
3434
// handshake.
3535
type Options struct {
36-
User url.Userinfo
36+
User *url.Userinfo
3737
ConnectAddr string
3838
}
3939

@@ -44,7 +44,8 @@ func Set(addr resolver.Address, opts Options) resolver.Address {
4444
}
4545

4646
// Get returns the Options for the proxy [resolver.Address] and a boolean
47-
// value representing if the attribute is present or not.
47+
// value representing if the attribute is present or not. The returned data
48+
// should not be mutated.
4849
func Get(addr resolver.Address) (Options, bool) {
4950
if a := addr.Attributes.Value(proxyOptionsKey); a != nil {
5051
return a.(Options), true

internal/proxyattributes/proxyattributes_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ func (s) TestGet(t *testing.T) {
4242
name string
4343
addr resolver.Address
4444
wantConnectAddr string
45-
wantUser url.Userinfo
45+
wantUser *url.Userinfo
4646
wantAttrPresent bool
4747
}{
4848
{
@@ -61,10 +61,10 @@ func (s) TestGet(t *testing.T) {
6161
addr: resolver.Address{
6262
Addr: "test-address",
6363
Attributes: attributes.New(proxyOptionsKey, Options{
64-
User: *user,
64+
User: user,
6565
}),
6666
},
67-
wantUser: *user,
67+
wantUser: user,
6868
wantAttrPresent: true,
6969
},
7070
{
@@ -97,7 +97,7 @@ func (s) TestGet(t *testing.T) {
9797
func (s) TestSet(t *testing.T) {
9898
addr := resolver.Address{Addr: "test-address"}
9999
pOpts := Options{
100-
User: *url.UserPassword("username", "password"),
100+
User: url.UserPassword("username", "password"),
101101
ConnectAddr: "proxy-address",
102102
}
103103

@@ -108,7 +108,7 @@ func (s) TestSet(t *testing.T) {
108108
t.Errorf("Get(%v) = %v, want %v ", populatedAddr, attrPresent, true)
109109
}
110110
if got, want := gotOption.ConnectAddr, pOpts.ConnectAddr; got != want {
111-
t.Errorf("Unexpected ConnectAddr proxy atrribute = %v, want %v", got, want)
111+
t.Errorf("unexpected ConnectAddr proxy atrribute = %v, want %v", got, want)
112112
}
113113
if got, want := gotOption.User, pOpts.User; got != want {
114114
t.Errorf("unexpected User proxy attribute = %v, want %v", got, want)

internal/resolver/delegatingresolver/delegatingresolver.go

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -205,13 +205,9 @@ func (r *delegatingResolver) updateClientConnStateLocked() error {
205205
proxyAddr = resolver.Address{Addr: r.proxyURL.Host}
206206
}
207207
var addresses []resolver.Address
208-
var user url.Userinfo
209-
if r.proxyURL.User != nil {
210-
user = *r.proxyURL.User
211-
}
212208
for _, targetAddr := range (*r.targetResolverState).Addresses {
213209
addresses = append(addresses, proxyattributes.Set(proxyAddr, proxyattributes.Options{
214-
User: user,
210+
User: r.proxyURL.User,
215211
ConnectAddr: targetAddr.Addr,
216212
}))
217213
}
@@ -229,7 +225,7 @@ func (r *delegatingResolver) updateClientConnStateLocked() error {
229225
for _, proxyAddr := range r.proxyAddrs {
230226
for _, targetAddr := range endpt.Addresses {
231227
addrs = append(addrs, proxyattributes.Set(proxyAddr, proxyattributes.Options{
232-
User: user,
228+
User: r.proxyURL.User,
233229
ConnectAddr: targetAddr.Addr,
234230
}))
235231
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
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 proxyserver provides an implementation of a proxy server for testing purposes.
20+
// The server supports only a single incoming connection at a time and is not concurrent.
21+
// It handles only HTTP CONNECT requests; other HTTP methods are not supported.
22+
package proxyserver
23+
24+
import (
25+
"bufio"
26+
"bytes"
27+
"io"
28+
"net"
29+
"net/http"
30+
"testing"
31+
"time"
32+
33+
"google.golang.org/grpc/internal/testutils"
34+
)
35+
36+
// ProxyServer represents a test proxy server.
37+
type ProxyServer struct {
38+
lis net.Listener
39+
in net.Conn // Connection from the client to the proxy.
40+
out net.Conn // Connection from the proxy to the backend.
41+
onRequest func(*http.Request) // Function to check the request sent to proxy.
42+
Addr string // Address of the proxy
43+
}
44+
45+
const defaultTestTimeout = 10 * time.Second
46+
47+
// Stop closes the ProxyServer and its connections to client and server.
48+
func (p *ProxyServer) stop() {
49+
p.lis.Close()
50+
if p.in != nil {
51+
p.in.Close()
52+
}
53+
if p.out != nil {
54+
p.out.Close()
55+
}
56+
}
57+
58+
func (p *ProxyServer) handleRequest(t *testing.T, in net.Conn, waitForServerHello bool) {
59+
req, err := http.ReadRequest(bufio.NewReader(in))
60+
if err != nil {
61+
t.Errorf("failed to read CONNECT req: %v", err)
62+
return
63+
}
64+
if req.Method != http.MethodConnect {
65+
t.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect)
66+
}
67+
p.onRequest(req)
68+
69+
t.Logf("Dialing to %s", req.URL.Host)
70+
out, err := net.Dial("tcp", req.URL.Host)
71+
if err != nil {
72+
in.Close()
73+
t.Logf("failed to dial to server: %v", err)
74+
return
75+
}
76+
out.SetDeadline(time.Now().Add(defaultTestTimeout))
77+
resp := http.Response{StatusCode: http.StatusOK, Proto: "HTTP/1.0"}
78+
var buf bytes.Buffer
79+
resp.Write(&buf)
80+
81+
if waitForServerHello {
82+
// Batch the first message from the server with the http connect
83+
// response. This is done to test the cases in which the grpc client has
84+
// the response to the connect request and proxied packets from the
85+
// destination server when it reads the transport.
86+
b := make([]byte, 50)
87+
bytesRead, err := out.Read(b)
88+
if err != nil {
89+
t.Errorf("Got error while reading server hello: %v", err)
90+
in.Close()
91+
out.Close()
92+
return
93+
}
94+
buf.Write(b[0:bytesRead])
95+
}
96+
p.in = in
97+
p.in.Write(buf.Bytes())
98+
p.out = out
99+
100+
go io.Copy(p.in, p.out)
101+
go io.Copy(p.out, p.in)
102+
}
103+
104+
// New initializes and starts a proxy server, registers a cleanup to
105+
// stop it, and returns a ProxyServer.
106+
func New(t *testing.T, reqCheck func(*http.Request), waitForServerHello bool) *ProxyServer {
107+
t.Helper()
108+
pLis, err := testutils.LocalTCPListener()
109+
if err != nil {
110+
t.Fatalf("failed to listen: %v", err)
111+
}
112+
113+
p := &ProxyServer{
114+
lis: pLis,
115+
onRequest: reqCheck,
116+
Addr: pLis.Addr().String(),
117+
}
118+
119+
// Start the proxy server.
120+
go func() {
121+
for {
122+
in, err := p.lis.Accept()
123+
if err != nil {
124+
return
125+
}
126+
// p.handleRequest is not invoked in a goroutine because the test
127+
// proxy currently supports handling only one connection at a time.
128+
p.handleRequest(t, in, waitForServerHello)
129+
}
130+
}()
131+
t.Logf("Started proxy at: %q", pLis.Addr().String())
132+
t.Cleanup(p.stop)
133+
return p
134+
}

internal/transport/http2_client.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import (
4343
"google.golang.org/grpc/internal/grpcsync"
4444
"google.golang.org/grpc/internal/grpcutil"
4545
imetadata "google.golang.org/grpc/internal/metadata"
46+
"google.golang.org/grpc/internal/proxyattributes"
4647
istatus "google.golang.org/grpc/internal/status"
4748
isyscall "google.golang.org/grpc/internal/syscall"
4849
"google.golang.org/grpc/internal/transport/networktype"
@@ -153,7 +154,7 @@ type http2Client struct {
153154
logger *grpclog.PrefixLogger
154155
}
155156

156-
func dial(ctx context.Context, fn func(context.Context, string) (net.Conn, error), addr resolver.Address, useProxy bool, grpcUA string) (net.Conn, error) {
157+
func dial(ctx context.Context, fn func(context.Context, string) (net.Conn, error), addr resolver.Address, grpcUA string) (net.Conn, error) {
157158
address := addr.Addr
158159
networkType, ok := networktype.Get(addr)
159160
if fn != nil {
@@ -177,8 +178,8 @@ func dial(ctx context.Context, fn func(context.Context, string) (net.Conn, error
177178
if !ok {
178179
networkType, address = parseDialTarget(address)
179180
}
180-
if networkType == "tcp" && useProxy {
181-
return proxyDial(ctx, address, grpcUA)
181+
if opts, present := proxyattributes.Get(addr); present {
182+
return proxyDial(ctx, addr, grpcUA, opts)
182183
}
183184
return internal.NetDialerWithTCPKeepalive().DialContext(ctx, networkType, address)
184185
}
@@ -217,7 +218,7 @@ func NewHTTP2Client(connectCtx, ctx context.Context, addr resolver.Address, opts
217218
// address specific arbitrary data to reach custom dialers and credential handshakers.
218219
connectCtx = icredentials.NewClientHandshakeInfoContext(connectCtx, credentials.ClientHandshakeInfo{Attributes: addr.Attributes})
219220

220-
conn, err := dial(connectCtx, opts.Dialer, addr, opts.UseProxy, opts.UserAgent)
221+
conn, err := dial(connectCtx, opts.Dialer, addr, opts.UserAgent)
221222
if err != nil {
222223
if opts.FailOnNonTempDialError {
223224
return nil, connectionErrorf(isTemporary(err), err, "transport: error while dialing: %v", err)

0 commit comments

Comments
 (0)