From 619dcd4afd74252e2935036827ea838e97939af2 Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Tue, 3 Dec 2024 09:47:26 +0530 Subject: [PATCH 01/53] Change proxy behaviour --- clientconn.go | 2 +- dialoptions.go | 26 +- internal/attributes/attributes.go | 63 ++ .../delegatingresolver/delegatingresolver.go | 226 ++++++ .../delegatingresolver_test.go | 306 ++++++++ internal/testutils/proxy.go | 106 +++ internal/transport/http2_client.go | 13 +- internal/transport/proxy.go | 54 +- internal/transport/proxy_test.go | 278 -------- internal/transport/transport.go | 2 - resolver_wrapper.go | 10 +- test/proxy_test.go | 666 ++++++++++++++++++ 12 files changed, 1417 insertions(+), 335 deletions(-) create mode 100644 internal/attributes/attributes.go create mode 100644 internal/resolver/delegatingresolver/delegatingresolver.go create mode 100644 internal/resolver/delegatingresolver/delegatingresolver_test.go create mode 100644 internal/testutils/proxy.go delete mode 100644 internal/transport/proxy_test.go create mode 100644 test/proxy_test.go diff --git a/clientconn.go b/clientconn.go index 4f57b55434f9..3a98f0c2ac29 100644 --- a/clientconn.go +++ b/clientconn.go @@ -225,7 +225,7 @@ func Dial(target string, opts ...DialOption) (*ClientConn, error) { func DialContext(ctx context.Context, target string, opts ...DialOption) (conn *ClientConn, err error) { // At the end of this method, we kick the channel out of idle, rather than // waiting for the first rpc. - opts = append([]DialOption{withDefaultScheme("passthrough")}, opts...) + opts = append([]DialOption{withDefaultScheme("passthrough"), WithTargetResolutionEnabled()}, opts...) cc, err := NewClient(target, opts...) if err != nil { return nil, err diff --git a/dialoptions.go b/dialoptions.go index 7494ae591f16..c64d5edb3035 100644 --- a/dialoptions.go +++ b/dialoptions.go @@ -94,6 +94,11 @@ type dialOptions struct { idleTimeout time.Duration defaultScheme string maxCallAttempts int + // TargetResolutionEnabled specifies if the target resolution is enabled even + // when proxy is enabled. + TargetResolutionEnabled bool + // UseProxy specifies if a proxy should be used. + UseProxy bool } // DialOption configures how we set up the connection. @@ -377,7 +382,15 @@ func WithInsecure() DialOption { // later release. func WithNoProxy() DialOption { return newFuncDialOption(func(o *dialOptions) { - o.copts.UseProxy = false + o.UseProxy = false + }) +} + +// WithTargetResolutionEnabled returns a DialOption which enables target +// resolution on client. +func WithTargetResolutionEnabled() DialOption { + return newFuncDialOption(func(o *dialOptions) { + o.TargetResolutionEnabled = true }) } @@ -662,14 +675,15 @@ func defaultDialOptions() dialOptions { copts: transport.ConnectOptions{ ReadBufferSize: defaultReadBufSize, WriteBufferSize: defaultWriteBufSize, - UseProxy: true, UserAgent: grpcUA, BufferPool: mem.DefaultBufferPool(), }, - bs: internalbackoff.DefaultExponential, - idleTimeout: 30 * time.Minute, - defaultScheme: "dns", - maxCallAttempts: defaultMaxCallAttempts, + bs: internalbackoff.DefaultExponential, + idleTimeout: 30 * time.Minute, + defaultScheme: "dns", + maxCallAttempts: defaultMaxCallAttempts, + UseProxy: true, + TargetResolutionEnabled: false, } } diff --git a/internal/attributes/attributes.go b/internal/attributes/attributes.go new file mode 100644 index 000000000000..5f66ab09bb2b --- /dev/null +++ b/internal/attributes/attributes.go @@ -0,0 +1,63 @@ +/* + * + * 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 attributes contains functions for getting and setting attributes. +package attributes + +import ( + "net/url" + + "google.golang.org/grpc/resolver" +) + +type keyType string + +const userAndConnectAddrKey = keyType("grpc.resolver.delegatingresolver.userAndConnectAddr") + +type attr struct { + user *url.Userinfo + addr string +} + +// SetUserAndConnectAddr returns a copy of the provided resolver.Address with +// attributes containing address to be sent in connect request to proxy and the +// user info. It's data should not be mutated after calling SetConnectAddr. +func SetUserAndConnectAddr(resAddr resolver.Address, user *url.Userinfo, addr string) resolver.Address { + resAddr.Attributes = resAddr.Attributes.WithValue(userAndConnectAddrKey, attr{user: user, addr: addr}) + return resAddr +} + +// ProxyConnectAddr returns the proxy connect address in resolver.Address, or nil +// if not present. The returned data should not be mutated. +func ProxyConnectAddr(addr resolver.Address) string { + attribute := addr.Attributes.Value(userAndConnectAddrKey) + if attribute != nil { + return attribute.(attr).addr + } + return "" +} + +// User returns the user info in the resolver.Address, or nil if not present. +// The returned data should not be mutated. +func User(addr resolver.Address) *url.Userinfo { + attribute := addr.Attributes.Value(userAndConnectAddrKey) + if attribute != nil { + return attribute.(attr).user + } + return nil +} diff --git a/internal/resolver/delegatingresolver/delegatingresolver.go b/internal/resolver/delegatingresolver/delegatingresolver.go new file mode 100644 index 000000000000..fa35bd21ab1d --- /dev/null +++ b/internal/resolver/delegatingresolver/delegatingresolver.go @@ -0,0 +1,226 @@ +/* + * + * 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 defines a resolver that can handle both target URI +// and proxy address resolution, unless: +// - A custom dialer is set using WithContextDialer dialoption. +// - Proxy usage is explicitly disabled using WithNoProxy dialoption. +// - Client-side resolution is explicitly enforced using WithTargetResolutionEnabled. +package delegatingresolver + +import ( + "fmt" + "net/http" + "net/url" + "sync" + + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/internal/attributes" + "google.golang.org/grpc/resolver" + "google.golang.org/grpc/serviceconfig" +) + +var ( + // HTTPSProxyFromEnvironment will be used and overwritten in the tests. + HTTPSProxyFromEnvironment = http.ProxyFromEnvironment + // ProxyScheme will be ovwewritten in tests + ProxyScheme = "dns" + logger = grpclog.Component("delegating-resolver") +) + +// delegatingResolver implements the `resolver.Resolver` interface. It uses child +// resolvers for the target and proxy resolution. It acts as an intermediatery +// between the child resolvers and the gRPC ClientConn. +type delegatingResolver struct { + target resolver.Target // parsed target URI to be resolved + cc resolver.ClientConn // gRPC ClientConn + 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 + + mu sync.Mutex // protects access to the resolver state and addresses during updates + targetAddrs []resolver.Address // resolved or unresolved target addresses, depending on proxy configuration + proxyAddrs []resolver.Address // resolved proxy addresses; empty if no proxy is configured + proxyURL *url.URL // proxy URL, derived from proxy environment and target + + targetResolverReady bool // indicates if an update from the target resolver has been received + proxyResolverReady bool // indicates if an update from the proxy resolver has been received +} + +func parsedURLForProxy(address string) (*url.URL, error) { + req := &http.Request{URL: &url.URL{Scheme: "https", Host: address}} + url, err := HTTPSProxyFromEnvironment(req) + if err != nil { + return nil, err + } + return url, nil +} + +// OnClientResolution is a no-op function in non-test code. In tests, it can +// be overwritten to send a signal to a channel, indicating that client-side +// name resolution was triggered. This enables tests to verify that resolution +// is bypassed when a proxy is in use. +var OnClientResolution = func(int) { /* no-op */ } + +// New creates a new delegating resolver that is used to call the target and +// proxy child resolver. If proxy is configured and target endpoint points +// correctly points to proxy, both proxy and target resolvers are used else +// only target resolver is used. +// +// For target resolver, if scheme is dns and target resolution is not enabled, +// it stores unresolved target address, bypassing target resolution at the +// client and resolution happens at the proxy server otherwise it resolves +// and store the resolved address. +// +// It returns error if proxy is configured but proxy target doesn't parse to +// correct url or if target resolution at client fails. +func New(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions, targetResolverBuilder resolver.Builder, targetResolutionEnabled bool) (resolver.Resolver, error) { + r := &delegatingResolver{target: target, cc: cc} + + var err error + r.proxyURL, err = parsedURLForProxy(target.Endpoint()) + // if proxy is configured but proxy target is wrong, so return early with error. + if err != nil { + return nil, fmt.Errorf("failed to determine proxy URL for %v target endpoint: %v", target.Endpoint(), err) + } + + // proxy is not configured or proxy address excluded using `NO_PROXY` env var, + // so only target resolver is used. + if r.proxyURL == nil { + OnClientResolution(1) + return targetResolverBuilder.Build(target, cc, opts) + } + + if logger.V(2) { + logger.Info("Proxy URL detected : %+v", r.proxyURL) + } + // 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 target.URL.Scheme == "dns" && !targetResolutionEnabled { + r.targetAddrs = []resolver.Address{{Addr: target.Endpoint()}} + r.targetResolverReady = true + } else { + OnClientResolution(1) + if r.targetResolver, err = targetResolverBuilder.Build(target, &wrappingClientConn{parent: r, resolverType: targetResolverType}, opts); err != nil { + return nil, fmt.Errorf("delegating_resolver: unable to build the resolver for target %v : %v", target, err) + } + } + + if r.proxyResolver, err = r.proxyURIResolver(opts); err != nil { + return nil, err + } + return r, nil +} + +func (r *delegatingResolver) proxyURIResolver(opts resolver.BuildOptions) (resolver.Resolver, error) { + proxyBuilder := resolver.Get(ProxyScheme) + if proxyBuilder == nil { + panic(fmt.Sprintln("delegating_resolver: resolver for proxy not found for scheme dns")) + } + r.proxyURL.Scheme = "dns" + r.proxyURL.Path = "/" + r.proxyURL.Host + r.proxyURL.Host = "" // Clear the Host field to conform to the "dns:///" format + proxyTarget := resolver.Target{URL: *r.proxyURL} + return proxyBuilder.Build(proxyTarget, &wrappingClientConn{parent: r, resolverType: proxyResolverType}, opts) +} + +func (r *delegatingResolver) ResolveNow(o resolver.ResolveNowOptions) { + if r.targetResolver != nil { + r.targetResolver.ResolveNow(o) + } + if r.proxyResolver != nil { + r.proxyResolver.ResolveNow(o) + } +} + +func (r *delegatingResolver) Close() { + if r.targetResolver != nil { + r.targetResolver.Close() + } + if r.proxyResolver != nil { + r.proxyResolver.Close() + } +} + +func (r *delegatingResolver) updateState() []resolver.Address { + var addresses []resolver.Address + for _, proxyAddr := range r.proxyAddrs { + for _, targetAddr := range r.targetAddrs { + newAddr := resolver.Address{Addr: proxyAddr.Addr} + newAddr = attributes.SetUserAndConnectAddr(newAddr, r.proxyURL.User, targetAddr.Addr) + addresses = append(addresses, newAddr) + } + } + // return the combined addresses. + return addresses +} + +// resolverType is an enum representing the type of resolver (target or proxy). +type resolverType int + +const ( + targetResolverType resolverType = iota + proxyResolverType +) + +type wrappingClientConn struct { + parent *delegatingResolver + resolverType resolverType // represents the type of resolver (target or proxy) +} + +// UpdateState intercepts state updates from the target and proxy resolvers. +func (wcc *wrappingClientConn) UpdateState(state resolver.State) error { + wcc.parent.mu.Lock() + defer wcc.parent.mu.Unlock() + var curState resolver.State + if wcc.resolverType == targetResolverType { + wcc.parent.targetAddrs = state.Addresses + logger.Infof("%v addresses received from target resolver", len(wcc.parent.targetAddrs)) + wcc.parent.targetResolverReady = true + curState = state + } + if wcc.resolverType == proxyResolverType { + wcc.parent.proxyAddrs = state.Addresses + logger.Infof("%v addresses received from proxy resolver", len(wcc.parent.proxyAddrs)) + wcc.parent.proxyResolverReady = true + } + + // Proceed only if updates from both resolvers have been received. + if !wcc.parent.targetResolverReady || !wcc.parent.proxyResolverReady { + return nil + } + curState.Addresses = wcc.parent.updateState() + return wcc.parent.cc.UpdateState(curState) +} + +// ReportError intercepts errors from the child resolvers and pass to ClientConn. +func (wcc *wrappingClientConn) ReportError(err error) { + wcc.parent.cc.ReportError(err) +} + +// NewAddress intercepts the new resolved address from the child resolvers and +// pass to ClientConn. +func (wcc *wrappingClientConn) NewAddress(addrs []resolver.Address) { + wcc.UpdateState(resolver.State{Addresses: addrs}) +} + +// ParseServiceConfig parses the provided service config and returns an +// object that provides the parsed config. +func (wcc *wrappingClientConn) ParseServiceConfig(serviceConfigJSON string) *serviceconfig.ParseResult { + return wcc.parent.cc.ParseServiceConfig(serviceConfigJSON) +} diff --git a/internal/resolver/delegatingresolver/delegatingresolver_test.go b/internal/resolver/delegatingresolver/delegatingresolver_test.go new file mode 100644 index 000000000000..106fefd600e8 --- /dev/null +++ b/internal/resolver/delegatingresolver/delegatingresolver_test.go @@ -0,0 +1,306 @@ +/* + * + * 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 + +import ( + "net/http" + "net/url" + "testing" + + "github.com/google/go-cmp/cmp" + "google.golang.org/grpc/internal/attributes" + "google.golang.org/grpc/internal/grpctest" + "google.golang.org/grpc/internal/testutils" + "google.golang.org/grpc/resolver" + _ "google.golang.org/grpc/resolver/dns" // To register dns resolver. + "google.golang.org/grpc/resolver/manual" + "google.golang.org/grpc/serviceconfig" +) + +type s struct { + grpctest.Tester +} + +func Test(t *testing.T) { + grpctest.RunSubTests(t, s{}) +} + +const ( + targetTestAddr = "test.com" + resolvedTargetTestAddr = "1.2.3.4:8080" + resolvedTargetTestAddr1 = "1.2.3.5:8080" + envProxyAddr = "proxytest.com" + resolvedProxyTestAddr = "2.3.4.5:7687" + resolvedProxyTestAddr1 = "2.3.4.6:7687" +) + +// overwrite function overwrites HTTPSProxyFromEnvironment and +// returns a function to restore the default values. +func overwrite(hpfe func(req *http.Request) (*url.URL, error)) func() { + originalHPFE := HTTPSProxyFromEnvironment + HTTPSProxyFromEnvironment = hpfe + return func() { + HTTPSProxyFromEnvironment = originalHPFE + } +} + +// createTestResolverClientConn initializes a test ResolverClientConn and +// returns it along with channels for resolver state updates and errors. +func createTestResolverClientConn(t *testing.T) (*testutils.ResolverClientConn, chan resolver.State, chan error) { + stateCh := make(chan resolver.State, 1) + errCh := make(chan error, 1) + + tcc := &testutils.ResolverClientConn{ + Logger: t, + UpdateStateF: func(s resolver.State) error { stateCh <- s; return nil }, + ReportErrorF: func(err error) { errCh <- err }, + } + return tcc, stateCh, errCh +} + +// TestParsedURLForProxyEnv verifies that the parsedURLForProxy function +// correctly resolves the proxy URL for a given target address. +func (s) TestParsedURLForProxyEnv(t *testing.T) { + // Overwrite the function in the test and restore them in defer. + hpfe := func(req *http.Request) (*url.URL, error) { + if req.URL.Host == targetTestAddr { + return &url.URL{ + Scheme: "https", + Host: envProxyAddr, + }, nil + } + return nil, nil + } + defer overwrite(hpfe)() + + // envTestAddr should be handled by ProxyFromEnvironment. + got, err := parsedURLForProxy(targetTestAddr) + if err != nil { + t.Errorf("Unable to get proxy URL : %v\n", err) + } + if got.Host != envProxyAddr { + t.Errorf("want %v, got %v", envProxyAddr, got) + } +} + +// TestDelegatingResolverNoProxy verifies that the delegating resolver correctly +// sends the resolved target URI to the ClientConn. +func (s) TestDelegatingResolverNoProxy(t *testing.T) { + mr := manual.NewBuilderWithScheme("test") // Set up a manual resolver to control the address resolution. + target := "test:///" + targetTestAddr + + tcc, stateCh, _ := createTestResolverClientConn(t) + // Create a delegating resolver with no proxy configuration + dr, err := New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, mr, false) + if err != nil || dr == nil { + t.Fatalf("Failed to create delegating resolver: %v", err) + } + + // Update the manual resolver with a test address. + mr.UpdateState(resolver.State{ + Addresses: []resolver.Address{{Addr: resolvedTargetTestAddr}}, + ServiceConfig: &serviceconfig.ParseResult{}, + }) + + // Verify that the delegating resolver outputs the same address. + expectedState := resolver.State{ + Addresses: []resolver.Address{{Addr: resolvedTargetTestAddr}}, + ServiceConfig: &serviceconfig.ParseResult{}, + } + + if state := <-stateCh; len(state.Addresses) != 1 || !cmp.Equal(expectedState, state) { + t.Fatalf("Unexpected state from delegating resolver: %v, want %v", state, expectedState) + } +} + +// setupDNS unregisters the DNS resolver and registers a manual resolver for the +// same scheme. This allows the test to mock the DNS resolution for the proxy resolver. +func setupDNS(t *testing.T) *manual.Resolver { + + mr := manual.NewBuilderWithScheme("dns") + + dnsResolverBuilder := resolver.Get("dns") + resolver.Register(mr) + + t.Cleanup(func() { resolver.Register(dnsResolverBuilder) }) + return mr +} + +// TestDelegatingResolverwithDNSAndProxy verifies that the delegating resolver +// correctly updates state with resolver proxy address with resolved target +// URI as attribute of the proxy address when the target URI scheme is DNS and +// a proxy is configured and target resolution is enabled. +func (s) TestDelegatingResolverwithDNSAndProxyWithTargetResolution(t *testing.T) { + hpfe := func(req *http.Request) (*url.URL, error) { + if req.URL.Host == targetTestAddr { + return &url.URL{ + Scheme: "https", + Host: envProxyAddr, + }, nil + } + return nil, nil + } + defer overwrite(hpfe)() + mrTarget := setupDNS(t) // Manual resolver to control the target resolution. + mrProxy := setupDNS(t) // Set up a manual DNS resolver to control the proxy address resolution. + target := "dns:///" + targetTestAddr + + tcc, stateCh, _ := createTestResolverClientConn(t) + dr, err := New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, mrTarget, true) + if err != nil { + t.Fatalf("Failed to create delegating resolver: %v", err) + } + if dr == nil { + t.Fatalf("Failed to create delegating resolver") + } + mrTarget.UpdateState(resolver.State{ + Addresses: []resolver.Address{ + {Addr: resolvedTargetTestAddr}, + }, + ServiceConfig: &serviceconfig.ParseResult{}, + }) + + mrProxy.UpdateState(resolver.State{ + Addresses: []resolver.Address{{Addr: resolvedProxyTestAddr}}, + ServiceConfig: &serviceconfig.ParseResult{}, + }) + + // Verify that the delegating resolver outputs the same address. + expectedAddr := resolver.Address{Addr: resolvedProxyTestAddr} + expectedAddr = attributes.SetUserAndConnectAddr(expectedAddr, nil, resolvedTargetTestAddr) + expectedState := resolver.State{Addresses: []resolver.Address{expectedAddr}} + + if state := <-stateCh; len(state.Addresses) != 1 || !cmp.Equal(expectedState, state) { + t.Fatalf("Unexpected state from delegating resolver: %v\n, want %v\n", state, expectedState) + } +} + +// TestDelegatingResolverwithDNSAndProxyWithNoTargetResolutionverifies that the +// delegating resolver correctly updates state with resolver proxy address with +// unresolved target URI as attribute of the proxy address when the target URI +// scheme is DNS and a proxy is configured and default target +// resolution(that is not enabled.) +func (s) TestDelegatingResolverwithDNSAndProxyWithNoTargetResolution(t *testing.T) { + hpfe := func(req *http.Request) (*url.URL, error) { + if req.URL.Host == targetTestAddr { + return &url.URL{ + Scheme: "https", + Host: envProxyAddr, + }, nil + } + return nil, nil + } + defer overwrite(hpfe)() + mrTarget := manual.NewBuilderWithScheme("test") // Manual resolver to control the target resolution. + mrProxy := setupDNS(t) // Set up a manual DNS resolver to control the proxy address resolution. + target := "dns:///" + targetTestAddr + + tcc, stateCh, _ := createTestResolverClientConn(t) + dr, err := New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, mrTarget, false) + if err != nil { + t.Fatalf("Failed to create delegating resolver: %v", err) + } + if dr == nil { + t.Fatalf("Failed to create delegating resolver") + } + + mrProxy.UpdateState(resolver.State{ + Addresses: []resolver.Address{{Addr: resolvedProxyTestAddr}}, + ServiceConfig: &serviceconfig.ParseResult{}, + }) + + // Verify that the delegating resolver outputs the same address. + expectedAddr := resolver.Address{Addr: resolvedProxyTestAddr} + expectedAddr = attributes.SetUserAndConnectAddr(expectedAddr, nil, targetTestAddr) + expectedState := resolver.State{Addresses: []resolver.Address{expectedAddr}} + + if state := <-stateCh; len(state.Addresses) != 1 || !cmp.Equal(expectedState, state) { + t.Fatalf("Unexpected state from delegating resolver: %v\n, want %v\n", state, expectedState) + } +} + +// TestDelegatingResolverwithDNSAndProxy verifies that the delegating resolver +// correctly updates state with resolved proxy address and custom resolved target +// address as the attributes when the target URI scheme is not DNS and a proxy is +// configured. +func (s) TestDelegatingResolverwithCustomResolverAndProxy(t *testing.T) { + hpfe := func(req *http.Request) (*url.URL, error) { + if req.URL.Host == targetTestAddr { + return &url.URL{ + Scheme: "https", + Host: envProxyAddr, + }, nil + } + return nil, nil + } + defer overwrite(hpfe)() + + mrTarget := manual.NewBuilderWithScheme("test") // Manual resolver to control the target resolution. + mrProxy := setupDNS(t) // Set up a manual DNS resolver to control the proxy address resolution. + target := "test:///" + targetTestAddr + + tcc, stateCh, _ := createTestResolverClientConn(t) + dr, err := New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, mrTarget, false) + if err != nil { + t.Fatalf("Failed to create delegating resolver: %v", err) + } + if dr == nil { + t.Fatalf("Failed to create delegating resolver") + } + + mrProxy.UpdateState(resolver.State{ + Addresses: []resolver.Address{ + {Addr: resolvedProxyTestAddr}, + {Addr: resolvedProxyTestAddr1}, + }, + ServiceConfig: &serviceconfig.ParseResult{}, + }) + mrTarget.UpdateState(resolver.State{ + Addresses: []resolver.Address{ + {Addr: resolvedTargetTestAddr}, + {Addr: resolvedTargetTestAddr1}, + }, + ServiceConfig: &serviceconfig.ParseResult{}, + }) + + expectedAddr := resolver.Address{Addr: resolvedProxyTestAddr} + expectedAddr = attributes.SetUserAndConnectAddr(expectedAddr, nil, resolvedTargetTestAddr) + + expectedAddr1 := resolver.Address{Addr: resolvedProxyTestAddr} + expectedAddr1 = attributes.SetUserAndConnectAddr(expectedAddr1, nil, resolvedTargetTestAddr1) + + expectedAddr2 := resolver.Address{Addr: resolvedProxyTestAddr1} + expectedAddr2 = attributes.SetUserAndConnectAddr(expectedAddr2, nil, resolvedTargetTestAddr) + + expectedAddr3 := resolver.Address{Addr: resolvedProxyTestAddr1} + expectedAddr3 = attributes.SetUserAndConnectAddr(expectedAddr3, nil, resolvedTargetTestAddr1) + + expectedState := resolver.State{Addresses: []resolver.Address{ + expectedAddr, + expectedAddr1, + expectedAddr2, + expectedAddr3, + }, + ServiceConfig: &serviceconfig.ParseResult{}, + } + + if state := <-stateCh; len(state.Addresses) != 4 || !cmp.Equal(expectedState, state) { + t.Fatalf("Unexpected state from delegating resolver: %v\n, want %v\n", state, expectedState) + } +} diff --git a/internal/testutils/proxy.go b/internal/testutils/proxy.go new file mode 100644 index 000000000000..8361efcdc5b1 --- /dev/null +++ b/internal/testutils/proxy.go @@ -0,0 +1,106 @@ +/* + * + * 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 testutils + +import ( + "bufio" + "bytes" + "fmt" + "io" + "net" + "net/http" + "time" +) + +const defaultTestTimeout = 10 * time.Second + +// ProxyServer is a proxy server that is used for testing. +type ProxyServer struct { + lis net.Listener + in net.Conn + out net.Conn + + requestCheck func(*http.Request) error +} + +// Stop functions stops and cleans up the proxy server. +func (p *ProxyServer) Stop() { + p.lis.Close() + if p.in != nil { + p.in.Close() + } + if p.out != nil { + p.out.Close() + } +} + +// NewProxyServer create and starts a proxy server. +func NewProxyServer(lis net.Listener, requestCheck func(*http.Request) error, errCh chan error, doneCh chan struct{}, backendAddr string, resolutionOnClient bool, proxyServerStarted func()) *ProxyServer { + p := &ProxyServer{ + lis: lis, + requestCheck: requestCheck, + } + go func() { + in, err := p.lis.Accept() + if err != nil { + return + } + p.in = in + if proxyServerStarted != nil { + proxyServerStarted() + } + req, err := http.ReadRequest(bufio.NewReader(in)) + if err != nil { + errCh <- fmt.Errorf("failed to read CONNECT req: %v", err) + return + } + if err := p.requestCheck(req); err != nil { + resp := http.Response{StatusCode: http.StatusMethodNotAllowed} + resp.Write(p.in) + p.in.Close() + errCh <- fmt.Errorf("get wrong CONNECT req: %+v, error: %v", req, err) + return + } + var out net.Conn + // if resolution is done on client,connect to address received in + // CONNECT request or else connect to backend address directly. + if resolutionOnClient { + out, err = net.Dial("tcp", req.URL.Host) + } else { + out, err = net.Dial("tcp", backendAddr) + } + + if err != nil { + errCh <- fmt.Errorf("failed to dial to server: %v", err) + return + } + out.SetDeadline(time.Now().Add(defaultTestTimeout)) + //response OK to client + resp := http.Response{StatusCode: http.StatusOK, Proto: "HTTP/1.0"} + var buf bytes.Buffer + resp.Write(&buf) + p.in.Write(buf.Bytes()) + p.out = out + // perform the proxy function, i.e pass the data from client to server and server to client. + go io.Copy(p.in, p.out) + go io.Copy(p.out, p.in) + close(doneCh) + }() + return p +} diff --git a/internal/transport/http2_client.go b/internal/transport/http2_client.go index f0c5cbc47645..831cffd587a9 100644 --- a/internal/transport/http2_client.go +++ b/internal/transport/http2_client.go @@ -37,6 +37,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" "google.golang.org/grpc/internal" + "google.golang.org/grpc/internal/attributes" "google.golang.org/grpc/internal/channelz" icredentials "google.golang.org/grpc/internal/credentials" "google.golang.org/grpc/internal/grpclog" @@ -153,8 +154,13 @@ type http2Client struct { logger *grpclog.PrefixLogger } -func dial(ctx context.Context, fn func(context.Context, string) (net.Conn, error), addr resolver.Address, useProxy bool, grpcUA string) (net.Conn, error) { +func dial(ctx context.Context, fn func(context.Context, string) (net.Conn, error), addr resolver.Address, grpcUA string) (net.Conn, error) { address := addr.Addr + + //if the ProxyConnectAddr is set in the aattribute, do a proxy dial. + if attributes.ProxyConnectAddr(addr) != "" { + return proxyDial(ctx, addr, grpcUA) + } networkType, ok := networktype.Get(addr) if fn != nil { // Special handling for unix scheme with custom dialer. Back in the day, @@ -177,9 +183,6 @@ func dial(ctx context.Context, fn func(context.Context, string) (net.Conn, error if !ok { networkType, address = parseDialTarget(address) } - if networkType == "tcp" && useProxy { - return proxyDial(ctx, address, grpcUA) - } return internal.NetDialerWithTCPKeepalive().DialContext(ctx, networkType, address) } @@ -217,7 +220,7 @@ func NewHTTP2Client(connectCtx, ctx context.Context, addr resolver.Address, opts // address specific arbitrary data to reach custom dialers and credential handshakers. connectCtx = icredentials.NewClientHandshakeInfoContext(connectCtx, credentials.ClientHandshakeInfo{Attributes: addr.Attributes}) - conn, err := dial(connectCtx, opts.Dialer, addr, opts.UseProxy, opts.UserAgent) + conn, err := dial(connectCtx, opts.Dialer, addr, opts.UserAgent) if err != nil { if opts.FailOnNonTempDialError { return nil, connectionErrorf(isTemporary(err), err, "transport: error while dialing: %v", err) diff --git a/internal/transport/proxy.go b/internal/transport/proxy.go index 54b224436544..8f69f3104d5f 100644 --- a/internal/transport/proxy.go +++ b/internal/transport/proxy.go @@ -30,29 +30,12 @@ import ( "net/url" "google.golang.org/grpc/internal" + "google.golang.org/grpc/internal/attributes" + "google.golang.org/grpc/resolver" ) const proxyAuthHeaderKey = "Proxy-Authorization" -var ( - // The following variable will be overwritten in the tests. - httpProxyFromEnvironment = http.ProxyFromEnvironment -) - -func mapAddress(address string) (*url.URL, error) { - req := &http.Request{ - URL: &url.URL{ - Scheme: "https", - Host: address, - }, - } - url, err := httpProxyFromEnvironment(req) - if err != nil { - return nil, err - } - return url, nil -} - // To read a response from a net.Conn, http.ReadResponse() takes a bufio.Reader. // It's possible that this reader reads more than what's need for the response and stores // those bytes in the buffer. @@ -72,21 +55,22 @@ func basicAuth(username, password string) string { return base64.StdEncoding.EncodeToString([]byte(auth)) } -func doHTTPConnectHandshake(ctx context.Context, conn net.Conn, backendAddr string, proxyURL *url.URL, grpcUA string) (_ net.Conn, err error) { +func doHTTPConnectHandshake(ctx context.Context, conn net.Conn, address resolver.Address, grpcUA string) (_ net.Conn, err error) { defer func() { if err != nil { conn.Close() } }() + backendAddr := attributes.ProxyConnectAddr(address) req := &http.Request{ Method: http.MethodConnect, URL: &url.URL{Host: backendAddr}, Header: map[string][]string{"User-Agent": {grpcUA}}, } - if t := proxyURL.User; t != nil { - u := t.Username() - p, _ := t.Password() + if user := attributes.User(address); user != nil { + u := user.Username() + p, _ := user.Password() req.Header.Add(proxyAuthHeaderKey, "Basic "+basicAuth(u, p)) } @@ -117,28 +101,14 @@ func doHTTPConnectHandshake(ctx context.Context, conn net.Conn, backendAddr stri return conn, nil } -// proxyDial dials, connecting to a proxy first if necessary. Checks if a proxy -// is necessary, dials, does the HTTP CONNECT handshake, and returns the -// connection. -func proxyDial(ctx context.Context, addr string, grpcUA string) (net.Conn, error) { - newAddr := addr - proxyURL, err := mapAddress(addr) +// proxyDial dials, connecting to a proxy first if necessary. Dials, does the +// HTTP CONNECT handshake, and returns the connection. +func proxyDial(ctx context.Context, address resolver.Address, grpcUA string) (net.Conn, error) { + conn, err := internal.NetDialerWithTCPKeepalive().DialContext(ctx, "tcp", address.Addr) if err != nil { return nil, err } - if proxyURL != nil { - newAddr = proxyURL.Host - } - - conn, err := internal.NetDialerWithTCPKeepalive().DialContext(ctx, "tcp", newAddr) - if err != nil { - return nil, err - } - if proxyURL == nil { - // proxy is disabled if proxyURL is nil. - return conn, err - } - return doHTTPConnectHandshake(ctx, conn, addr, proxyURL, grpcUA) + return doHTTPConnectHandshake(ctx, conn, address, grpcUA) } func sendHTTPRequest(ctx context.Context, req *http.Request, conn net.Conn) error { diff --git a/internal/transport/proxy_test.go b/internal/transport/proxy_test.go deleted file mode 100644 index 41f0918d1c90..000000000000 --- a/internal/transport/proxy_test.go +++ /dev/null @@ -1,278 +0,0 @@ -//go:build !race -// +build !race - -/* - * - * Copyright 2017 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 transport - -import ( - "bufio" - "bytes" - "context" - "encoding/base64" - "fmt" - "io" - "net" - "net/http" - "net/url" - "testing" - "time" -) - -const ( - envTestAddr = "1.2.3.4:8080" - envProxyAddr = "2.3.4.5:7687" -) - -// overwriteAndRestore overwrite function httpProxyFromEnvironment and -// returns a function to restore the default values. -func overwrite(hpfe func(req *http.Request) (*url.URL, error)) func() { - backHPFE := httpProxyFromEnvironment - httpProxyFromEnvironment = hpfe - return func() { - httpProxyFromEnvironment = backHPFE - } -} - -type proxyServer struct { - t *testing.T - lis net.Listener - in net.Conn - out net.Conn - - requestCheck func(*http.Request) error -} - -func (p *proxyServer) run(waitForServerHello bool) { - in, err := p.lis.Accept() - if err != nil { - return - } - p.in = in - - req, err := http.ReadRequest(bufio.NewReader(in)) - if err != nil { - p.t.Errorf("failed to read CONNECT req: %v", err) - return - } - if err := p.requestCheck(req); err != nil { - resp := http.Response{StatusCode: http.StatusMethodNotAllowed} - resp.Write(p.in) - p.in.Close() - p.t.Errorf("get wrong CONNECT req: %+v, error: %v", req, err) - return - } - - out, err := net.Dial("tcp", req.URL.Host) - if err != nil { - p.t.Errorf("failed to dial to server: %v", err) - return - } - out.SetDeadline(time.Now().Add(defaultTestTimeout)) - resp := http.Response{StatusCode: http.StatusOK, Proto: "HTTP/1.0"} - var buf bytes.Buffer - resp.Write(&buf) - if waitForServerHello { - // Batch the first message from the server with the http connect - // response. This is done to test the cases in which the grpc client has - // the response to the connect request and proxied packets from the - // destination server when it reads the transport. - b := make([]byte, 50) - bytesRead, err := out.Read(b) - if err != nil { - p.t.Errorf("Got error while reading server hello: %v", err) - in.Close() - out.Close() - return - } - buf.Write(b[0:bytesRead]) - } - p.in.Write(buf.Bytes()) - p.out = out - go io.Copy(p.in, p.out) - go io.Copy(p.out, p.in) -} - -func (p *proxyServer) stop() { - p.lis.Close() - if p.in != nil { - p.in.Close() - } - if p.out != nil { - p.out.Close() - } -} - -type testArgs struct { - proxyURLModify func(*url.URL) *url.URL - proxyReqCheck func(*http.Request) error - serverMessage []byte -} - -func testHTTPConnect(t *testing.T, args testArgs) { - plis, err := net.Listen("tcp", "localhost:0") - if err != nil { - t.Fatalf("failed to listen: %v", err) - } - p := &proxyServer{ - t: t, - lis: plis, - requestCheck: args.proxyReqCheck, - } - go p.run(len(args.serverMessage) > 0) - defer p.stop() - - blis, err := net.Listen("tcp", "localhost:0") - if err != nil { - t.Fatalf("failed to listen: %v", err) - } - - msg := []byte{4, 3, 5, 2} - recvBuf := make([]byte, len(msg)) - done := make(chan error, 1) - go func() { - in, err := blis.Accept() - if err != nil { - done <- err - return - } - defer in.Close() - in.Write(args.serverMessage) - in.Read(recvBuf) - done <- nil - }() - - // Overwrite the function in the test and restore them in defer. - hpfe := func(*http.Request) (*url.URL, error) { - return args.proxyURLModify(&url.URL{Host: plis.Addr().String()}), nil - } - defer overwrite(hpfe)() - - // Dial to proxy server. - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - c, err := proxyDial(ctx, blis.Addr().String(), "test") - if err != nil { - t.Fatalf("HTTP connect Dial failed: %v", err) - } - defer c.Close() - c.SetDeadline(time.Now().Add(defaultTestTimeout)) - - // Send msg on the connection. - c.Write(msg) - if err := <-done; err != nil { - t.Fatalf("Failed to accept: %v", err) - } - - // Check received msg. - if string(recvBuf) != string(msg) { - t.Fatalf("Received msg: %v, want %v", recvBuf, msg) - } - - if len(args.serverMessage) > 0 { - gotServerMessage := make([]byte, len(args.serverMessage)) - if _, err := c.Read(gotServerMessage); err != nil { - t.Errorf("Got error while reading message from server: %v", err) - return - } - if string(gotServerMessage) != string(args.serverMessage) { - t.Errorf("Message from server: %v, want %v", gotServerMessage, args.serverMessage) - } - } -} - -func (s) TestHTTPConnect(t *testing.T) { - args := testArgs{ - proxyURLModify: func(in *url.URL) *url.URL { - return in - }, - proxyReqCheck: func(req *http.Request) error { - if req.Method != http.MethodConnect { - return fmt.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) - } - return nil - }, - } - testHTTPConnect(t, args) -} - -func (s) TestHTTPConnectWithServerHello(t *testing.T) { - args := testArgs{ - proxyURLModify: func(in *url.URL) *url.URL { - return in - }, - proxyReqCheck: func(req *http.Request) error { - if req.Method != http.MethodConnect { - return fmt.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) - } - return nil - }, - serverMessage: []byte("server-hello"), - } - testHTTPConnect(t, args) -} - -func (s) TestHTTPConnectBasicAuth(t *testing.T) { - const ( - user = "notAUser" - password = "notAPassword" - ) - args := testArgs{ - proxyURLModify: func(in *url.URL) *url.URL { - in.User = url.UserPassword(user, password) - return in - }, - proxyReqCheck: func(req *http.Request) error { - if req.Method != http.MethodConnect { - return fmt.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) - } - wantProxyAuthStr := "Basic " + base64.StdEncoding.EncodeToString([]byte(user+":"+password)) - if got := req.Header.Get(proxyAuthHeaderKey); got != wantProxyAuthStr { - gotDecoded, _ := base64.StdEncoding.DecodeString(got) - wantDecoded, _ := base64.StdEncoding.DecodeString(wantProxyAuthStr) - return fmt.Errorf("unexpected auth %q (%q), want %q (%q)", got, gotDecoded, wantProxyAuthStr, wantDecoded) - } - return nil - }, - } - testHTTPConnect(t, args) -} - -func (s) TestMapAddressEnv(t *testing.T) { - // Overwrite the function in the test and restore them in defer. - hpfe := func(req *http.Request) (*url.URL, error) { - if req.URL.Host == envTestAddr { - return &url.URL{ - Scheme: "https", - Host: envProxyAddr, - }, nil - } - return nil, nil - } - defer overwrite(hpfe)() - - // envTestAddr should be handled by ProxyFromEnvironment. - got, err := mapAddress(envTestAddr) - if err != nil { - t.Error(err) - } - if got.Host != envProxyAddr { - t.Errorf("want %v, got %v", envProxyAddr, got) - } -} diff --git a/internal/transport/transport.go b/internal/transport/transport.go index f3148e31c5dd..314899b847df 100644 --- a/internal/transport/transport.go +++ b/internal/transport/transport.go @@ -503,8 +503,6 @@ type ConnectOptions struct { ChannelzParent *channelz.SubChannel // MaxHeaderListSize sets the max (uncompressed) size of header list that is prepared to be received. MaxHeaderListSize *uint32 - // UseProxy specifies if a proxy should be used. - UseProxy bool // The mem.BufferPool to use when reading/writing to the wire. BufferPool mem.BufferPool } diff --git a/resolver_wrapper.go b/resolver_wrapper.go index 23bb3fb25824..8873b3fc37aa 100644 --- a/resolver_wrapper.go +++ b/resolver_wrapper.go @@ -26,6 +26,7 @@ import ( "google.golang.org/grpc/internal/channelz" "google.golang.org/grpc/internal/grpcsync" "google.golang.org/grpc/internal/pretty" + "google.golang.org/grpc/internal/resolver/delegatingresolver" "google.golang.org/grpc/resolver" "google.golang.org/grpc/serviceconfig" ) @@ -78,7 +79,14 @@ func (ccr *ccResolverWrapper) start() error { Authority: ccr.cc.authority, } var err error - ccr.resolver, err = ccr.cc.resolverBuilder.Build(ccr.cc.parsedTarget, ccr, opts) + // The delegating resolver is not used if WithNoProxy or WithCustomDialer is set. + // It is not used when explicitly disabled with TargetResolutionEnabled as well. + // In these cases, the normal name resolver determined by the scheme will be used directly. + if ccr.cc.dopts.copts.Dialer != nil || !ccr.cc.dopts.UseProxy { + ccr.resolver, err = ccr.cc.resolverBuilder.Build(ccr.cc.parsedTarget, ccr, opts) + } else { + ccr.resolver, err = delegatingresolver.New(ccr.cc.parsedTarget, ccr, opts, ccr.cc.resolverBuilder, ccr.cc.dopts.TargetResolutionEnabled) + } errCh <- err }) return <-errCh diff --git a/test/proxy_test.go b/test/proxy_test.go new file mode 100644 index 000000000000..f98c8a5c0821 --- /dev/null +++ b/test/proxy_test.go @@ -0,0 +1,666 @@ +/* + * + * 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 test + +import ( + "context" + "encoding/base64" + "fmt" + "net" + "net/http" + "net/url" + "testing" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/internal/resolver/delegatingresolver" + "google.golang.org/grpc/internal/stubserver" + "google.golang.org/grpc/internal/testutils" + testgrpc "google.golang.org/grpc/interop/grpc_testing" + "google.golang.org/grpc/resolver" + "google.golang.org/grpc/resolver/manual" +) + +const ( + unresolvedTargetURI = "example.com" + unresolvedProxyURI = "proxyExample.com" +) + +// overwriteAndRestore temporarily replaces `HTTPSProxyFromEnvironment` with a +// custom function and returns a function to restore the original. +func overwriteAndRestore(customFunc func(req *http.Request) (*url.URL, error)) func() { + originalFunc := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = customFunc + return func() { + delegatingresolver.HTTPSProxyFromEnvironment = originalFunc + } +} + +// createAndStartBackendServer creates and starts a test backend server, +// registering a cleanup to stop it. +func createAndStartBackendServer(t *testing.T) string { + backend := &stubserver.StubServer{ + EmptyCallF: func(context.Context, *testgrpc.Empty) (*testgrpc.Empty, error) { return &testgrpc.Empty{}, nil }, + } + if err := backend.StartServer(); err != nil { + t.Fatalf("Failed to start backend: %v", err) + } + t.Logf("Started TestService backend at: %q", backend.Address) + t.Cleanup(backend.Stop) + return backend.Address +} + +// setupDNS sets up a manual DNS resolver and registers it, returning the +// builder for test customization. +func setupDNS(t *testing.T) *manual.Resolver { + manualResolver := manual.NewBuilderWithScheme("dns") + originalResolver := resolver.Get("dns") + resolver.Register(manualResolver) + t.Cleanup(func() { + resolver.Register(originalResolver) + }) + return manualResolver +} + +// setupProxy initializes and starts a proxy server, registers a cleanup to +// stop it, and returns the proxy's listener and helper channels. +func setupProxy(t *testing.T, backendAddr string, resolutionOnClient bool, reqCheck func(*http.Request) error) (net.Listener, chan error, chan struct{}, chan struct{}) { + proxyListener, err := net.Listen("tcp", "localhost:0") + if err != nil { + t.Fatalf("failed to listen: %v", err) + } + + errCh := make(chan error, 1) + doneCh := make(chan struct{}) + proxyStartedCh := make(chan struct{}) + t.Cleanup(func() { + close(errCh) + }) + + proxyServer := testutils.NewProxyServer(proxyListener, reqCheck, errCh, doneCh, backendAddr, resolutionOnClient, func() { close(proxyStartedCh) }) + t.Cleanup(proxyServer.Stop) + + return proxyListener, errCh, doneCh, proxyStartedCh +} + +// TestGrpcDialWithProxy tests grpc.Dial using a proxy and default +// resolver in the target URI.and verifies that it connects to the proxy server +// and sends unresolved target URI in the HTTP CONNECT request and then +// connects to the backend server. +func (s) TestGrpcDialWithProxy(t *testing.T) { + backendAddr := createAndStartBackendServer(t) + proxyListener, errCh, doneCh, _ := setupProxy(t, backendAddr, false, func(req *http.Request) error { + if req.Method != http.MethodConnect { + return fmt.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) + } + if req.URL.Host != unresolvedTargetURI { + return fmt.Errorf("unexpected URL.Host %q, want %q", req.URL.Host, unresolvedTargetURI) + } + return nil + }) + + hpfe := func(req *http.Request) (*url.URL, error) { + if req.URL.Host == unresolvedTargetURI { + return &url.URL{ + Scheme: "https", + Host: proxyListener.Addr().String(), + }, nil + } + return nil, nil + } + defer overwriteAndRestore(hpfe)() + + ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) + defer cancel() + + conn, err := grpc.Dial(unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + t.Fatalf("grpc.Dial failed: %v", err) + } + defer conn.Close() + + client := testgrpc.NewTestServiceClient(conn) + if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { + t.Errorf("EmptyCall failed: %v", err) + } + + select { + case err := <-errCh: + t.Fatalf("proxy server encountered an error: %v", err) + case <-doneCh: + t.Logf("proxy server succeeded") + } +} + +// TestGrpcDialWithProxyAndResolution tests grpc.Dial with a proxy and ensures DNS +// resolution of the proxy URI is performed. +func (s) TestGrpcDialWithProxyAndResolution(t *testing.T) { + // Create and start a backend server. + backendAddr := createAndStartBackendServer(t) + + // Set up and start a proxy listener. + proxyLis, errCh, doneCh, _ := setupProxy(t, backendAddr, false, func(req *http.Request) error { + if req.Method != http.MethodConnect { + return fmt.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) + } + if req.URL.Host != unresolvedTargetURI { + return fmt.Errorf("unexpected URL.Host %q, want %q", req.URL.Host, unresolvedTargetURI) + } + return nil + }) + + // Overwrite the default proxy function and restore it after the test. + hpfe := func(req *http.Request) (*url.URL, error) { + if req.URL.Host == unresolvedTargetURI { + return &url.URL{ + Scheme: "https", + Host: unresolvedProxyURI, + }, nil + } + return nil, nil + } + defer overwriteAndRestore(hpfe)() + + // Set up a manual resolver for proxy resolution. + mrProxy := setupDNS(t) + + // Update the proxy resolver state with the proxy's address. + mrProxy.InitialState(resolver.State{ + Addresses: []resolver.Address{ + {Addr: proxyLis.Addr().String()}, + }, + }) + // Dial to the proxy server. + ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) + defer cancel() + conn, err := grpc.Dial(unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + t.Fatalf("grpc.Dial failed: %v", err) + } + defer conn.Close() + + // Send an RPC to the backend through the proxy. + client := testgrpc.NewTestServiceClient(conn) + if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { + t.Errorf("EmptyCall failed: %v", err) + } + + // Verify that the proxy server encountered no errors. + select { + case err := <-errCh: + t.Fatalf("proxy server encountered an error: %v", err) + case <-doneCh: + t.Logf("proxy server succeeded") + } +} + +// TestGrpcNewClientWithProxy tests grpc.NewClient with default i.e DNS resolver +// for targetURI and a proxy and verifies that it connects to proxy server and +// sends unresolved target URI in the HTTP CONNECT req and connects to backend. +func (s) TestGrpcNewClientWithProxy(t *testing.T) { + // Set up a channel to receive signals from OnClientResolution. + resolutionCh := make(chan bool, 1) + + // Overwrite OnClientResolution to send a signal to the channel. + origOnClientResolution := delegatingresolver.OnClientResolution + delegatingresolver.OnClientResolution = func(int) { + resolutionCh <- true + } + t.Cleanup(func() { delegatingresolver.OnClientResolution = origOnClientResolution }) + + // Create and start a backend server. + backendAddr := createAndStartBackendServer(t) + + // Set up and start the proxy server. + proxyLis, errCh, doneCh, _ := setupProxy(t, backendAddr, false, func(req *http.Request) error { + if req.Method != http.MethodConnect { + return fmt.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) + } + if req.URL.Host != unresolvedTargetURI { + return fmt.Errorf("unexpected URL.Host %q, want %q", req.URL.Host, unresolvedTargetURI) + } + return nil + }) + + // Overwrite the proxy resolution function and restore it afterward. + hpfe := func(req *http.Request) (*url.URL, error) { + if req.URL.Host == unresolvedTargetURI { + return &url.URL{ + Scheme: "https", + Host: unresolvedProxyURI, + }, nil + } + return nil, nil + } + defer overwriteAndRestore(hpfe)() + + // Set up a manual resolver for proxy resolution. + mrProxy := setupDNS(t) + + // Update the proxy resolver state with the proxy's address. + mrProxy.InitialState(resolver.State{ + Addresses: []resolver.Address{ + {Addr: proxyLis.Addr().String()}, + }, + }) + + // Dial to the proxy server. + ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) + defer cancel() + + conn, err := grpc.NewClient(unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + t.Fatalf("grpc.NewClient failed: %v", err) + } + defer conn.Close() + + // Send an RPC to the backend through the proxy. + client := testgrpc.NewTestServiceClient(conn) + if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { + t.Errorf("EmptyCall failed: %v", err) + } + + // Verify if the proxy server encountered any errors. + select { + case err := <-errCh: + t.Fatalf("proxy server encountered an error: %v", err) + case <-doneCh: + t.Logf("proxy server succeeded") + } + + // Verify if OnClientResolution was triggered. + select { + case <-resolutionCh: + t.Error("Client-side resolution was unexpectedly called") + default: + // Success: OnClientResolution was not called. + } +} + +// TestGrpcNewClientWithProxyAndCustomResolver tests grpc.NewClient with a +// resolver other than "dns" mentioned in targetURI with a proxyand verifies +// that it connects to proxy server and passes resolved target URI in the +// HTTP CONNECT req and connects to the backend server. +func (s) TestGrpcNewClientWithProxyAndCustomResolver(t *testing.T) { + // Set up a channel to receive signals from OnClientResolution. + resolutionCh := make(chan bool, 1) + + // Overwrite OnClientResolution to send a signal to the channel. + origOnClientResolution := delegatingresolver.OnClientResolution + delegatingresolver.OnClientResolution = func(int) { + resolutionCh <- true + } + t.Cleanup(func() { delegatingresolver.OnClientResolution = origOnClientResolution }) + + // Create and start a backend server. + backendAddr := createAndStartBackendServer(t) + + // Set up and start the proxy server. + proxyLis, errCh, doneCh, _ := setupProxy(t, backendAddr, true, func(req *http.Request) error { + if req.Method != http.MethodConnect { + return fmt.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) + } + if req.URL.Host != backendAddr { + return fmt.Errorf("unexpected URL.Host %q, want %q", req.URL.Host, backendAddr) + } + return nil + }) + + // Overwrite the proxy resolution function and restore it afterward. + hpfe := func(req *http.Request) (*url.URL, error) { + fmt.Printf("emchandwani : the request URL host is %v\n", req.URL.Host) + if req.URL.Host == unresolvedTargetURI { + return &url.URL{ + Scheme: "https", + Host: unresolvedProxyURI, + }, nil + } + return nil, nil + } + defer overwriteAndRestore(hpfe)() + + // Dial options for the gRPC client. + dopts := []grpc.DialOption{ + grpc.WithTransportCredentials(insecure.NewCredentials()), + } + // Create a custom resolver. + mrTarget := manual.NewBuilderWithScheme("whatever") + resolver.Register(mrTarget) + mrTarget.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) + + // Set up a manual resolver for proxy resolution. + mrProxy := setupDNS(t) + // Update the proxy resolver state with the proxy's address. + mrProxy.InitialState(resolver.State{ + Addresses: []resolver.Address{{Addr: proxyLis.Addr().String()}}}) + + // Dial to the proxy server. + ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) + defer cancel() + + conn, err := grpc.NewClient(mrTarget.Scheme()+":///"+unresolvedTargetURI, dopts...) + if err != nil { + t.Fatalf("grpc.NewClient() failed: %v", err) + } + t.Cleanup(func() { conn.Close() }) + + // Create a test service client and make an RPC call. + client := testgrpc.NewTestServiceClient(conn) + if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { + t.Errorf("EmptyCall() failed: %v", err) + } + + // Check if the proxy encountered any errors. + select { + case err := <-errCh: + t.Fatalf("proxy server encountered an error: %v", err) + case <-doneCh: + t.Logf("proxy server succeeded") + } + + // Check if client-side resolution signal was sent to the channel. + select { + case <-resolutionCh: + // Success: OnClientResolution was called. + default: + t.Fatalf("Client side resolution should be called but wasn't") + } +} + +// TestGrpcNewClientWithProxyAndTargetResolutionEnabled tests grpc.NewClient with +// the default "dns" resolver and dial option enabling target resolution on the +// client and verifies that the resoltion happens on client. +func (s) TestGrpcNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { + // Set up a channel to receive signals from OnClientResolution. + resolutionCh := make(chan bool, 1) + + // Temporarily modify ProxyScheme for this test. + origProxyScheme := delegatingresolver.ProxyScheme + delegatingresolver.ProxyScheme = "whatever" + t.Cleanup(func() { delegatingresolver.ProxyScheme = origProxyScheme }) + + // Overwrite OnClientResolution to send a signal to the channel. + origOnClientResolution := delegatingresolver.OnClientResolution + delegatingresolver.OnClientResolution = func(int) { + resolutionCh <- true + } + t.Cleanup(func() { delegatingresolver.OnClientResolution = origOnClientResolution }) + + // Create and start a backend server. + backendAddr := createAndStartBackendServer(t) + + // Set up the proxy server. + proxyLis, errCh, doneCh, _ := setupProxy(t, backendAddr, true, func(req *http.Request) error { + if req.Method != http.MethodConnect { + return fmt.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) + } + if req.URL.Host != backendAddr { + return fmt.Errorf("unexpected URL.Host %q, want %q", req.URL.Host, backendAddr) + } + return nil + }) + + // Overwrite the proxy resolution function and restore it afterward. + hpfe := func(req *http.Request) (*url.URL, error) { + if req.URL.Host == unresolvedTargetURI { + return &url.URL{ + Scheme: "https", + Host: unresolvedProxyURI, + }, nil + } + return nil, nil + } + defer overwriteAndRestore(hpfe)() + + // Configure manual resolvers for both proxy and target backends. + proxyResolver := manual.NewBuilderWithScheme("whatever") + resolver.Register(proxyResolver) + proxyResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: proxyLis.Addr().String()}}}) + + targetResolver := setupDNS(t) + targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) + + // Dial options with target resolution enabled. + dopts := []grpc.DialOption{ + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithTargetResolutionEnabled(), + } + + // Dial to the proxy server. + ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) + defer cancel() + + conn, err := grpc.NewClient(unresolvedTargetURI, dopts...) + if err != nil { + t.Fatalf("grpc.NewClient() failed: %v", err) + } + t.Cleanup(func() { conn.Close() }) + + // Create a test service client and make an RPC call. + client := testgrpc.NewTestServiceClient(conn) + if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { + t.Errorf("EmptyCall() failed: %v", err) + } + + // Verify if the proxy encountered any errors. + select { + case err := <-errCh: + t.Fatalf("proxy server encountered an error: %v", err) + case <-doneCh: + t.Logf("proxy server succeeded") + } + + // Check if client-side resolution signal was sent to the channel. + select { + case <-resolutionCh: + // Success: OnClientResolution was called. + default: + t.Fatalf("Client-side resolution should be called but wasn't") + } +} + +// TestGrpcNewClientWithNoProxy tests grpc.NewClient with grpc.WithNoProxy() set +// and verifies that it does not dail to proxy, but directly to backend. +func (s) TestGrpcNewClientWithNoProxy(t *testing.T) { + // Create and start a backend server. + backendAddr := createAndStartBackendServer(t) + + // Set up and start a proxy server. + _, _, _, proxyStartedCh := setupProxy(t, backendAddr, true, func(req *http.Request) error { + return fmt.Errorf("Proxy server received Connect req %v, but should not", req) + }) + + // Dial options with proxy explicitly disabled. + dopts := []grpc.DialOption{ + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithNoProxy(), // Disable proxy. + } + + targetResolver := setupDNS(t) + targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) + // Establish a connection. + ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) + defer cancel() + + conn, err := grpc.NewClient(unresolvedTargetURI, dopts...) + if err != nil { + t.Fatalf("grpc.NewClient() failed: %v", err) + } + defer conn.Close() + + // Create a test service client and make an RPC call. + client := testgrpc.NewTestServiceClient(conn) + if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { + t.Errorf("EmptyCall() failed: %v", err) + } + + // Verify that the proxy was not dialed. + select { + case <-proxyStartedCh: + t.Error("Proxy server was dialed, but it should have been bypassed") + default: + } + +} + +// TestGrpcNewClientWithContextDialer tests grpc.NewClient with +// grpc.WithContextDialer() set and verifies that it does not dial to proxy but +// rather uses the custom dialer. +func (s) TestGrpcNewClientWithContextDialer(t *testing.T) { + backendAddr := createAndStartBackendServer(t) + + // Set up and start a proxy server. + _, _, _, proxyStartedCh := setupProxy(t, backendAddr, true, func(req *http.Request) error { + return fmt.Errorf("Proxy server received Connect req %v, but should not", req) + }) + + // Create a custom dialer that directly dials the backend. We'll use this + // to bypass any proxy logic. + dialerCalled := make(chan bool, 1) + customDialer := func(_ context.Context, backendAddr string) (net.Conn, error) { + dialerCalled <- true + return net.Dial("tcp", backendAddr) + } + + dopts := []grpc.DialOption{ + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithContextDialer(customDialer), + } + + ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) + defer cancel() + targetResolver := setupDNS(t) + targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) + + conn, err := grpc.NewClient(unresolvedTargetURI, dopts...) + if err != nil { + t.Fatalf("grpc.NewClient() failed: %v", err) + } + defer conn.Close() + + client := testgrpc.NewTestServiceClient(conn) + if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { + t.Errorf("EmptyCall() failed: %v", err) + } + select { + case <-dialerCalled: + default: + t.Errorf("custom dialer was not called by grpc.NewClient()") + } + select { + case <-proxyStartedCh: + t.Fatalf("Proxy Server was dialled") + default: + } +} + +// TestBasicAuthInGrpcNewClientWithProxy tests grpc.NewClient with default i.e +// DNS resolver for targetURI and a proxy and verifies that it connects to proxy +// server and sends unresolved target URI in the HTTP CONNECT req and connects +// to backend. Also verifies that correct user info is sent in the CONNECT. +func (s) TestBasicAuthInGrpcNewClientWithProxy(t *testing.T) { + // Set up a channel to receive signals from OnClientResolution. + resolutionCh := make(chan bool, 1) + + // Overwrite OnClientResolution to send a signal to the channel. + origOnClientResolution := delegatingresolver.OnClientResolution + delegatingresolver.OnClientResolution = func(int) { + resolutionCh <- true + } + t.Cleanup(func() { delegatingresolver.OnClientResolution = origOnClientResolution }) + + // Create and start a backend server. + backendAddr := createAndStartBackendServer(t) + const ( + user = "notAUser" + password = "notAPassword" + ) + // Set up and start the proxy server. + proxyLis, errCh, doneCh, _ := setupProxy(t, backendAddr, false, func(req *http.Request) error { + if req.Method != http.MethodConnect { + return fmt.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) + } + if req.URL.Host != unresolvedTargetURI { + return fmt.Errorf("unexpected URL.Host %q, want %q", req.URL.Host, unresolvedTargetURI) + } + wantProxyAuthStr := "Basic " + base64.StdEncoding.EncodeToString([]byte(user+":"+password)) + if got := req.Header.Get("Proxy-Authorization"); got != wantProxyAuthStr { + gotDecoded, _ := base64.StdEncoding.DecodeString(got) + wantDecoded, _ := base64.StdEncoding.DecodeString(wantProxyAuthStr) + return fmt.Errorf("unexpected auth %q (%q), want %q (%q)", got, gotDecoded, wantProxyAuthStr, wantDecoded) + } + return nil + }) + + // Overwrite the proxy resolution function and restore it afterward. + hpfe := func(req *http.Request) (*url.URL, error) { + if req.URL.Host == unresolvedTargetURI { + u := url.URL{ + Scheme: "https", + Host: unresolvedProxyURI, + } + u.User = url.UserPassword(user, password) + return &u, nil + } + return nil, nil + } + defer overwriteAndRestore(hpfe)() + + // Set up a manual resolver for proxy resolution. + mrProxy := setupDNS(t) + + // Update the proxy resolver state with the proxy's address. + mrProxy.InitialState(resolver.State{ + Addresses: []resolver.Address{ + {Addr: proxyLis.Addr().String()}, + }, + }) + + // Dial to the proxy server. + ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) + defer cancel() + + conn, err := grpc.NewClient(unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + t.Fatalf("grpc.NewClient failed: %v", err) + } + defer conn.Close() + + // Send an RPC to the backend through the proxy. + client := testgrpc.NewTestServiceClient(conn) + if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { + t.Errorf("EmptyCall failed: %v", err) + } + + // Verify if the proxy server encountered any errors. + select { + case err := <-errCh: + t.Fatalf("proxy server encountered an error: %v", err) + case <-doneCh: + t.Logf("proxy server succeeded") + } + + // Verify if OnClientResolution was triggered. + select { + case <-resolutionCh: + t.Error("Client-side resolution was unexpectedly called") + default: + // Success: OnClientResolution was not called. + } +} From 65b33bdd94808620698e0d1e8d583fd9aeda624b Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Thu, 5 Dec 2024 00:07:33 +0530 Subject: [PATCH 02/53] change delegating resolver --- internal/internal.go | 5 + internal/proxyattributes/proxyattributes.go | 64 ++++ .../proxyattributes/proxyattributes_test.go | 103 ++++++ .../delegatingresolver/delegatingresolver.go | 120 ++++--- .../delegatingresolver_ext_test.go | 276 ++++++++++++++++ .../delegatingresolver_test.go | 309 ++++-------------- 6 files changed, 578 insertions(+), 299 deletions(-) create mode 100644 internal/proxyattributes/proxyattributes.go create mode 100644 internal/proxyattributes/proxyattributes_test.go create mode 100644 internal/resolver/delegatingresolver/delegatingresolver_ext_test.go diff --git a/internal/internal.go b/internal/internal.go index 7fbfaacde9bf..1098a837a06d 100644 --- a/internal/internal.go +++ b/internal/internal.go @@ -238,6 +238,11 @@ var ( // SetBufferPoolingThresholdForTesting updates the buffer pooling threshold, for // testing purposes. SetBufferPoolingThresholdForTesting any // func(int) + + // HTTPSProxyFromEnvironmentForTesting returns the URL of the proxy to use + // for testing purposes. It is used to override the `http.ProxyFromEnvironment` + // function for testing purposes. + HTTPSProxyFromEnvironmentForTesting any // func(*http.Request) (*url.URL, error) ) // HealthChecker defines the signature of the client-side LB channel health diff --git a/internal/proxyattributes/proxyattributes.go b/internal/proxyattributes/proxyattributes.go new file mode 100644 index 000000000000..8dd6404c0478 --- /dev/null +++ b/internal/proxyattributes/proxyattributes.go @@ -0,0 +1,64 @@ +/* + * + * 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 proxyattributes contains functions for getting and setting proxy +// attributes like the CONNECT address and user info. +package proxyattributes + +import ( + "net/url" + + "google.golang.org/grpc/resolver" +) + +type keyType string + +const userAndConnectAddrKey = keyType("grpc.resolver.delegatingresolver.userAndConnectAddr") + +type attr struct { + user *url.Userinfo + addr string +} + +// Populate returns a copy of addr with attributes containing the +// provided user and connect address, which are needed during the CONNECT +// handshake for a proxy connection. +func Populate(resAddr resolver.Address, user *url.Userinfo, addr string) resolver.Address { + resAddr.Attributes = resAddr.Attributes.WithValue(userAndConnectAddrKey, attr{user: user, addr: addr}) + return resAddr +} + +// ProxyConnectAddr returns the proxy connect address in resolver.Address, or nil +// if not present. The returned data should not be mutated. +func ProxyConnectAddr(addr resolver.Address) string { + attribute := addr.Attributes.Value(userAndConnectAddrKey) + if attribute != nil { + return attribute.(attr).addr + } + return "" +} + +// User returns the user info in the resolver.Address, or nil if not present. +// The returned data should not be mutated. +func User(addr resolver.Address) *url.Userinfo { + attribute := addr.Attributes.Value(userAndConnectAddrKey) + if attribute != nil { + return attribute.(attr).user + } + return nil +} diff --git a/internal/proxyattributes/proxyattributes_test.go b/internal/proxyattributes/proxyattributes_test.go new file mode 100644 index 000000000000..1f8294cdef89 --- /dev/null +++ b/internal/proxyattributes/proxyattributes_test.go @@ -0,0 +1,103 @@ +/* + * + * 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 proxyattributes + +import ( + "net/url" + "testing" + + "google.golang.org/grpc/attributes" + "google.golang.org/grpc/internal/grpctest" + "google.golang.org/grpc/resolver" +) + +type s struct { + grpctest.Tester +} + +func Test(t *testing.T) { + grpctest.RunSubTests(t, s{}) +} + +// TestProxyConnectAddr tests ProxyConnectAddr returns the coorect connect +// address in the attribute. +func (s) TestProxyConnectAddr(t *testing.T) { + addr := resolver.Address{ + Addr: "test-address", + Attributes: attributes.New(userAndConnectAddrKey, attr{user: nil, addr: "proxy-address"}), + } + + // Validate ProxyConnectAddr returns empty string for missing attributes + if got, want := ProxyConnectAddr(addr), "proxy-address"; got != want { + t.Errorf("Unexpected ConnectAddr proxy atrribute = %v, want : %v", got, want) + } +} + +// TestUser tests User returns the correct user in the attribute. +func (s) TestUser(t *testing.T) { + user := url.UserPassword("username", "password") + addr := resolver.Address{ + Addr: "test-address", + Attributes: attributes.New(userAndConnectAddrKey, attr{user: user, addr: ""}), + } + + // Validate User returns nil for missing attributes + if got, want := User(addr), user; got != want { + t.Errorf("unexpected User proxy attribute = %v, want %v", got, want) + } +} + +// TestEmptyProxyAttribute tests ProxyConnectAddr and User return empty string +// and nil respectively when not set. +func (s) TestEmptyProxyAttribute(t *testing.T) { + addr := resolver.Address{ + Addr: "test-address", + } + + // Validate ProxyConnectAddr returns empty string for missing attributes + if got := ProxyConnectAddr(addr); got != "" { + t.Errorf("Unexpected ConnectAddr proxy atrribute = %v, want empty string", got) + } + // Validate User returns nil for missing attributes + if got := User(addr); got != nil { + t.Errorf("unexpected User proxy attribute = %v, want nil", got) + } +} + +// TestPopulate tests Populate returns a copy of addr with attributes +// containing correct user and connect address. +func (s) TestPopulate(t *testing.T) { + addr := resolver.Address{ + Addr: "test-address", + } + user := url.UserPassword("username", "password") + connectAddr := "proxy-address" + + // Call Populate and validate attributes + populatedAddr := Populate(addr, user, connectAddr) + + // Verify that the returned address is updated correctly + if got, want := ProxyConnectAddr(populatedAddr), connectAddr; got != want { + t.Errorf("Unexpected ConnectAddr proxy atrribute = %v, want %v", got, want) + } + + if got, want := User(populatedAddr), user; got != want { + t.Errorf("unexpected User proxy attribute = %v, want %v", got, want) + } +} diff --git a/internal/resolver/delegatingresolver/delegatingresolver.go b/internal/resolver/delegatingresolver/delegatingresolver.go index fa35bd21ab1d..e11531af4a4c 100644 --- a/internal/resolver/delegatingresolver/delegatingresolver.go +++ b/internal/resolver/delegatingresolver/delegatingresolver.go @@ -17,10 +17,7 @@ */ // Package delegatingresolver defines a resolver that can handle both target URI -// and proxy address resolution, unless: -// - A custom dialer is set using WithContextDialer dialoption. -// - Proxy usage is explicitly disabled using WithNoProxy dialoption. -// - Client-side resolution is explicitly enforced using WithTargetResolutionEnabled. +// and proxy address resolution. package delegatingresolver import ( @@ -30,17 +27,17 @@ import ( "sync" "google.golang.org/grpc/grpclog" - "google.golang.org/grpc/internal/attributes" + "google.golang.org/grpc/internal" + "google.golang.org/grpc/internal/proxyattributes" "google.golang.org/grpc/resolver" "google.golang.org/grpc/serviceconfig" ) var ( // HTTPSProxyFromEnvironment will be used and overwritten in the tests. - HTTPSProxyFromEnvironment = http.ProxyFromEnvironment - // ProxyScheme will be ovwewritten in tests - ProxyScheme = "dns" - logger = grpclog.Component("delegating-resolver") + httpProxyFromEnvironmentFunc = http.ProxyFromEnvironment + + logger = grpclog.Component("delegating-resolver") ) // delegatingResolver implements the `resolver.Resolver` interface. It uses child @@ -52,62 +49,73 @@ type delegatingResolver struct { 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 - mu sync.Mutex // protects access to the resolver state and addresses during updates - targetAddrs []resolver.Address // resolved or unresolved target addresses, depending on proxy configuration - proxyAddrs []resolver.Address // resolved proxy addresses; empty if no proxy is configured - proxyURL *url.URL // proxy URL, derived from proxy environment and target - - targetResolverReady bool // indicates if an update from the target resolver has been received - proxyResolverReady bool // indicates if an update from the proxy resolver has been received + mu sync.Mutex // protects access to the resolver state and addresses during updates + targetAddrs []resolver.Address // resolved or unresolved target addresses, depending on proxy configuration + proxyAddrs []resolver.Address // resolved proxy addresses; empty if no proxy is configured + proxyURL *url.URL // proxy URL, derived from proxy environment and target + targetResolverReady bool // indicates if an update from the target resolver has been received + proxyResolverReady bool // indicates if an update from the proxy resolver has been received } +// parsedURLForProxy 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. +// - 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 parsedURLForProxy(address string) (*url.URL, error) { - req := &http.Request{URL: &url.URL{Scheme: "https", Host: address}} - url, err := HTTPSProxyFromEnvironment(req) + proxyFunc := httpProxyFromEnvironmentFunc + if pf := internal.HTTPSProxyFromEnvironmentForTesting; pf != nil { + proxyFunc = pf.(func(*http.Request) (*url.URL, error)) + } + + req := &http.Request{URL: &url.URL{ + Scheme: "https", + Host: address, + }} + url, err := proxyFunc(req) if err != nil { return nil, err } return url, nil } -// OnClientResolution is a no-op function in non-test code. In tests, it can -// be overwritten to send a signal to a channel, indicating that client-side -// name resolution was triggered. This enables tests to verify that resolution -// is bypassed when a proxy is in use. -var OnClientResolution = func(int) { /* no-op */ } - // New creates a new delegating resolver that is used to call the target and -// proxy child resolver. If proxy is configured and target endpoint points -// correctly points to proxy, both proxy and target resolvers are used else -// only target resolver is used. +// proxy child resolver. If proxy is configured, both proxy and target resolvers +// are used else only target resolver is used. // -// For target resolver, if scheme is dns and target resolution is not enabled, -// it stores unresolved target address, bypassing target resolution at the -// client and resolution happens at the proxy server otherwise it resolves -// and store the resolved address. +// For the target resolver: +// - If the scheme is DNS and target resolution is disabled, +// the unresolved target address is stored, allowing the proxy server to +// handle resolution instead of the client. +// - If target resolution is enabled, +// the target is resolved, and the resolved address is stored. // // It returns error if proxy is configured but proxy target doesn't parse to // correct url or if target resolution at client fails. func New(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions, targetResolverBuilder resolver.Builder, targetResolutionEnabled bool) (resolver.Resolver, error) { - r := &delegatingResolver{target: target, cc: cc} + r := &delegatingResolver{ + target: target, + cc: cc, + } var err error r.proxyURL, err = parsedURLForProxy(target.Endpoint()) - // if proxy is configured but proxy target is wrong, so return early with error. if err != nil { - return nil, fmt.Errorf("failed to determine proxy URL for %v target endpoint: %v", target.Endpoint(), err) + return nil, fmt.Errorf("delegating_resolver: failed to determine proxy URL for %v target endpoint: %v", target.Endpoint(), err) } // proxy is not configured or proxy address excluded using `NO_PROXY` env var, // so only target resolver is used. if r.proxyURL == nil { - OnClientResolution(1) return targetResolverBuilder.Build(target, cc, opts) } if logger.V(2) { - logger.Info("Proxy URL detected : %+v", r.proxyURL) + logger.Info("delegating_resolver: Proxy URL detected : %+v", r.proxyURL) } + // 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. @@ -115,28 +123,39 @@ func New(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOpti r.targetAddrs = []resolver.Address{{Addr: target.Endpoint()}} r.targetResolverReady = true } else { - OnClientResolution(1) - if r.targetResolver, err = targetResolverBuilder.Build(target, &wrappingClientConn{parent: r, resolverType: targetResolverType}, opts); err != nil { + wcc := &wrappingClientConn{ + parent: r, + resolverType: targetResolverType, + } + if r.targetResolver, err = targetResolverBuilder.Build(target, wcc, opts); err != nil { return nil, fmt.Errorf("delegating_resolver: unable to build the resolver for target %v : %v", target, err) } } if r.proxyResolver, err = r.proxyURIResolver(opts); err != nil { - return nil, err + return nil, fmt.Errorf("delegating_resolver: unable to build the resolver for proxy : %v", err) } return r, 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 (r *delegatingResolver) proxyURIResolver(opts resolver.BuildOptions) (resolver.Resolver, error) { - proxyBuilder := resolver.Get(ProxyScheme) + proxyBuilder := resolver.Get("dns") if proxyBuilder == nil { panic(fmt.Sprintln("delegating_resolver: resolver for proxy not found for scheme dns")) } r.proxyURL.Scheme = "dns" r.proxyURL.Path = "/" + r.proxyURL.Host r.proxyURL.Host = "" // Clear the Host field to conform to the "dns:///" format + proxyTarget := resolver.Target{URL: *r.proxyURL} - return proxyBuilder.Build(proxyTarget, &wrappingClientConn{parent: r, resolverType: proxyResolverType}, opts) + wcc := &wrappingClientConn{ + parent: r, + resolverType: proxyResolverType, + } + return proxyBuilder.Build(proxyTarget, wcc, opts) } func (r *delegatingResolver) ResolveNow(o resolver.ResolveNowOptions) { @@ -157,16 +176,19 @@ func (r *delegatingResolver) Close() { } } +// updateState creates a list of combined addresses by pairing each proxy address +// with every target address. For each pair, it generates a new `resolver.Address` +// using the proxy address, and adding the target address as the attribute +// along with user info. func (r *delegatingResolver) updateState() []resolver.Address { var addresses []resolver.Address for _, proxyAddr := range r.proxyAddrs { for _, targetAddr := range r.targetAddrs { newAddr := resolver.Address{Addr: proxyAddr.Addr} - newAddr = attributes.SetUserAndConnectAddr(newAddr, r.proxyURL.User, targetAddr.Addr) + newAddr = proxyattributes.Populate(newAddr, r.proxyURL.User, targetAddr.Addr) addresses = append(addresses, newAddr) } } - // return the combined addresses. return addresses } @@ -178,6 +200,10 @@ const ( proxyResolverType ) +// wrappingClientConn wraps around the client connection, intercepting state +// updates, errors, and new resolved addresses from the target and proxy +// resolvers. It facilitates combining the results from both resolvers and +// passing them to the clientConn. type wrappingClientConn struct { parent *delegatingResolver resolverType resolverType // represents the type of resolver (target or proxy) @@ -190,13 +216,13 @@ func (wcc *wrappingClientConn) UpdateState(state resolver.State) error { var curState resolver.State if wcc.resolverType == targetResolverType { wcc.parent.targetAddrs = state.Addresses - logger.Infof("%v addresses received from target resolver", len(wcc.parent.targetAddrs)) + logger.Infof("delegating_resolver: %v addresses received from target resolver", len(wcc.parent.targetAddrs)) wcc.parent.targetResolverReady = true curState = state } if wcc.resolverType == proxyResolverType { wcc.parent.proxyAddrs = state.Addresses - logger.Infof("%v addresses received from proxy resolver", len(wcc.parent.proxyAddrs)) + logger.Infof("delegating_resolver: %v addresses received from proxy resolver", len(wcc.parent.proxyAddrs)) wcc.parent.proxyResolverReady = true } @@ -208,13 +234,13 @@ func (wcc *wrappingClientConn) UpdateState(state resolver.State) error { return wcc.parent.cc.UpdateState(curState) } -// ReportError intercepts errors from the child resolvers and pass to ClientConn. +// ReportError intercepts errors from the child resolvers and passes them to ClientConn. func (wcc *wrappingClientConn) ReportError(err error) { wcc.parent.cc.ReportError(err) } // NewAddress intercepts the new resolved address from the child resolvers and -// pass to ClientConn. +// passes them to ClientConn. func (wcc *wrappingClientConn) NewAddress(addrs []resolver.Address) { wcc.UpdateState(resolver.State{Addresses: addrs}) } diff --git a/internal/resolver/delegatingresolver/delegatingresolver_ext_test.go b/internal/resolver/delegatingresolver/delegatingresolver_ext_test.go new file mode 100644 index 000000000000..704559a2070d --- /dev/null +++ b/internal/resolver/delegatingresolver/delegatingresolver_ext_test.go @@ -0,0 +1,276 @@ +/* + * + * 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_test + +import ( + "net/http" + "net/url" + "testing" + + "github.com/google/go-cmp/cmp" + "google.golang.org/grpc/internal" + "google.golang.org/grpc/internal/grpctest" + "google.golang.org/grpc/internal/proxyattributes" + "google.golang.org/grpc/internal/resolver/delegatingresolver" + "google.golang.org/grpc/internal/testutils" + "google.golang.org/grpc/resolver" + _ "google.golang.org/grpc/resolver/dns" // To register dns resolver. + "google.golang.org/grpc/resolver/manual" + "google.golang.org/grpc/serviceconfig" +) + +type s struct { + grpctest.Tester +} + +func Test(t *testing.T) { + grpctest.RunSubTests(t, s{}) +} + +const ( + targetTestAddr = "test.com" + resolvedTargetTestAddr = "1.2.3.4:8080" + resolvedTargetTestAddr1 = "1.2.3.5:8080" + envProxyAddr = "proxytest.com" + resolvedProxyTestAddr = "2.3.4.5:7687" + resolvedProxyTestAddr1 = "2.3.4.6:7687" +) + +// overrideHTTPSProxyFromEnvironment function overwrites HTTPSProxyFromEnvironment and +// returns a function to restore the default values. +func overrideHTTPSProxyFromEnvironment(hpfe func(req *http.Request) (*url.URL, error)) func() { + internal.HTTPSProxyFromEnvironmentForTesting = hpfe + return func() { + internal.HTTPSProxyFromEnvironmentForTesting = nil + } +} + +// createTestResolverClientConn initializes a test ResolverClientConn and +// returns it along with channels for resolver state updates and errors. +func createTestResolverClientConn(t *testing.T) (*testutils.ResolverClientConn, chan resolver.State, chan error) { + stateCh := make(chan resolver.State, 1) + errCh := make(chan error, 1) + + tcc := &testutils.ResolverClientConn{ + Logger: t, + UpdateStateF: func(s resolver.State) error { stateCh <- s; return nil }, + ReportErrorF: func(err error) { errCh <- err }, + } + return tcc, stateCh, errCh +} + +// Tests that the delegating resolver behaves correctly when no proxy +// environment variables are set. In this case, no proxy resolver is created, +// only a target resolver is used, and the addresses returned by the delegating +// resolver should exactly match those returned by the target resolver. +func (s) TestDelegatingResolverNoProxy(t *testing.T) { + mr := manual.NewBuilderWithScheme("test") // Set up a manual resolver to control the address resolution. + target := mr.Scheme() + ":///" + targetTestAddr + + tcc, stateCh, _ := createTestResolverClientConn(t) + // Create a delegating resolver with no proxy configuration + _, err := delegatingresolver.New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, mr, false) + if err != nil { + t.Fatalf("Failed to create delegating resolver: %v", err) + } + + // Update the manual resolver with a test address. + mr.UpdateState(resolver.State{ + Addresses: []resolver.Address{{Addr: resolvedTargetTestAddr}}, + ServiceConfig: &serviceconfig.ParseResult{}, + }) + + // Verify that the delegating resolver outputs the same address. + wantedState := resolver.State{ + Addresses: []resolver.Address{{Addr: resolvedTargetTestAddr}}, + ServiceConfig: &serviceconfig.ParseResult{}, + } + + if state := <-stateCh; !cmp.Equal(wantedState, state) { + t.Fatalf("Unexpected state from delegating resolver: %v, want %v", state, wantedState) + } +} + +// setupDNS registers a new manual resolver for the DNS scheme, effectively +// overwriting the previously registered DNS resolver. This allows the test to +// mock the DNS resolution for the proxy resolver. It also registers the original +// DNS resolver after the test is done. +func setupDNS(t *testing.T) *manual.Resolver { + mr := manual.NewBuilderWithScheme("dns") + + dnsResolverBuilder := resolver.Get("dns") + resolver.Register(mr) + + t.Cleanup(func() { resolver.Register(dnsResolverBuilder) }) + return mr +} + +// Tests that the delegating resolver correctly updates state with resolver +// proxy address with resolved target URI as attribute of the proxy address when +// the target URI scheme is DNS and a proxy is configured and target resolution +// is enabled. +func (s) TestDelegatingResolverwithDNSAndProxyWithTargetResolution(t *testing.T) { + hpfe := func(req *http.Request) (*url.URL, error) { + if req.URL.Host == targetTestAddr { + return &url.URL{ + Scheme: "https", + Host: envProxyAddr, + }, nil + } + return nil, nil + } + defer overrideHTTPSProxyFromEnvironment(hpfe)() + mrTarget := setupDNS(t) // Manual resolver to control the target resolution. + mrProxy := setupDNS(t) // Set up a manual DNS resolver to control the proxy address resolution. + target := "dns:///" + targetTestAddr + + tcc, stateCh, _ := createTestResolverClientConn(t) + _, err := delegatingresolver.New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, mrTarget, true) + if err != nil { + t.Fatalf("Failed to create delegating resolver: %v", err) + } + + mrTarget.UpdateState(resolver.State{ + Addresses: []resolver.Address{ + {Addr: resolvedTargetTestAddr}, + }, + ServiceConfig: &serviceconfig.ParseResult{}, + }) + + mrProxy.UpdateState(resolver.State{ + Addresses: []resolver.Address{{Addr: resolvedProxyTestAddr}}, + ServiceConfig: &serviceconfig.ParseResult{}, + }) + + // Verify that the delegating resolver outputs the same address. + wantedAddr := resolver.Address{Addr: resolvedProxyTestAddr} + wantedAddr = proxyattributes.Populate(wantedAddr, nil, resolvedTargetTestAddr) + wantedState := resolver.State{Addresses: []resolver.Address{wantedAddr}} + + if state := <-stateCh; len(state.Addresses) != 1 || !cmp.Equal(wantedState, state) { + t.Fatalf("Unexpected state from delegating resolver: %v\n, want %v\n", state, wantedState) + } +} + +// Tests that the delegating resolver correctly updates state with resolver +// proxy address with unresolved target URI as attribute of the proxy address +// when the target URI scheme is DNS and a proxy is configured and default target +// resolution(that is not enabled.) +func (s) TestDelegatingResolverwithDNSAndProxyWithNoTargetResolution(t *testing.T) { + hpfe := func(req *http.Request) (*url.URL, error) { + if req.URL.Host == targetTestAddr { + return &url.URL{ + Scheme: "https", + Host: envProxyAddr, + }, nil + } + return nil, nil + } + defer overrideHTTPSProxyFromEnvironment(hpfe)() + mrTarget := manual.NewBuilderWithScheme("test") // Manual resolver to control the target resolution. + mrProxy := setupDNS(t) // Set up a manual DNS resolver to control the proxy address resolution. + target := "dns:///" + targetTestAddr + + tcc, stateCh, _ := createTestResolverClientConn(t) + _, err := delegatingresolver.New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, mrTarget, false) + if err != nil { + t.Fatalf("Failed to create delegating resolver: %v", err) + } + + mrProxy.UpdateState(resolver.State{ + Addresses: []resolver.Address{{Addr: resolvedProxyTestAddr}}, + ServiceConfig: &serviceconfig.ParseResult{}, + }) + + // Verify that the delegating resolver outputs the same address. + wantedAddr := resolver.Address{Addr: resolvedProxyTestAddr} + wantedAddr = proxyattributes.Populate(wantedAddr, nil, targetTestAddr) + wantedState := resolver.State{Addresses: []resolver.Address{wantedAddr}} + + if state := <-stateCh; len(state.Addresses) != 1 || !cmp.Equal(wantedState, state) { + t.Fatalf("Unexpected state from delegating resolver: %v\n, want %v\n", state, wantedState) + } +} + +// Tests that the delegating resolver +// correctly updates state with resolved proxy address and custom resolved target +// address as the proxyattributes when the target URI scheme is not DNS and a proxy is +// configured. +func (s) TestDelegatingResolverwithCustomResolverAndProxy(t *testing.T) { + hpfe := func(req *http.Request) (*url.URL, error) { + if req.URL.Host == targetTestAddr { + return &url.URL{ + Scheme: "https", + Host: envProxyAddr, + }, nil + } + return nil, nil + } + defer overrideHTTPSProxyFromEnvironment(hpfe)() + + mrTarget := manual.NewBuilderWithScheme("test") // Manual resolver to control the target resolution. + mrProxy := setupDNS(t) // Set up a manual DNS resolver to control the proxy address resolution. + target := mrTarget.Scheme() + ":///" + targetTestAddr + + tcc, stateCh, _ := createTestResolverClientConn(t) + _, err := delegatingresolver.New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, mrTarget, false) + if err != nil { + t.Fatalf("Failed to create delegating resolver: %v", err) + } + + mrProxy.UpdateState(resolver.State{ + Addresses: []resolver.Address{ + {Addr: resolvedProxyTestAddr}, + {Addr: resolvedProxyTestAddr1}, + }, + ServiceConfig: &serviceconfig.ParseResult{}, + }) + mrTarget.UpdateState(resolver.State{ + Addresses: []resolver.Address{ + {Addr: resolvedTargetTestAddr}, + {Addr: resolvedTargetTestAddr1}, + }, + ServiceConfig: &serviceconfig.ParseResult{}, + }) + + wantedAddr := resolver.Address{Addr: resolvedProxyTestAddr} + wantedAddr = proxyattributes.Populate(wantedAddr, nil, resolvedTargetTestAddr) + + wantedAddr1 := resolver.Address{Addr: resolvedProxyTestAddr} + wantedAddr1 = proxyattributes.Populate(wantedAddr1, nil, resolvedTargetTestAddr1) + + wantedAddr2 := resolver.Address{Addr: resolvedProxyTestAddr1} + wantedAddr2 = proxyattributes.Populate(wantedAddr2, nil, resolvedTargetTestAddr) + + wantedAddr3 := resolver.Address{Addr: resolvedProxyTestAddr1} + wantedAddr3 = proxyattributes.Populate(wantedAddr3, nil, resolvedTargetTestAddr1) + + wantedState := resolver.State{Addresses: []resolver.Address{ + wantedAddr, + wantedAddr1, + wantedAddr2, + wantedAddr3, + }, + ServiceConfig: &serviceconfig.ParseResult{}, + } + + if state := <-stateCh; len(state.Addresses) != 4 || !cmp.Equal(wantedState, state) { + t.Fatalf("Unexpected state from delegating resolver: %v\n, want %v\n", state, wantedState) + } +} diff --git a/internal/resolver/delegatingresolver/delegatingresolver_test.go b/internal/resolver/delegatingresolver/delegatingresolver_test.go index 106fefd600e8..55eaef79a954 100644 --- a/internal/resolver/delegatingresolver/delegatingresolver_test.go +++ b/internal/resolver/delegatingresolver/delegatingresolver_test.go @@ -19,18 +19,15 @@ package delegatingresolver import ( + "errors" "net/http" "net/url" "testing" "github.com/google/go-cmp/cmp" - "google.golang.org/grpc/internal/attributes" + "google.golang.org/grpc/internal" "google.golang.org/grpc/internal/grpctest" - "google.golang.org/grpc/internal/testutils" - "google.golang.org/grpc/resolver" _ "google.golang.org/grpc/resolver/dns" // To register dns resolver. - "google.golang.org/grpc/resolver/manual" - "google.golang.org/grpc/serviceconfig" ) type s struct { @@ -42,265 +39,73 @@ func Test(t *testing.T) { } const ( - targetTestAddr = "test.com" - resolvedTargetTestAddr = "1.2.3.4:8080" - resolvedTargetTestAddr1 = "1.2.3.5:8080" - envProxyAddr = "proxytest.com" - resolvedProxyTestAddr = "2.3.4.5:7687" - resolvedProxyTestAddr1 = "2.3.4.6:7687" + targetTestAddr = "test.com" + envProxyAddr = "proxytest.com" ) -// overwrite function overwrites HTTPSProxyFromEnvironment and +// overrideHTTPSProxyFromEnvironment function overwrites HTTPSProxyFromEnvironment and // returns a function to restore the default values. -func overwrite(hpfe func(req *http.Request) (*url.URL, error)) func() { - originalHPFE := HTTPSProxyFromEnvironment - HTTPSProxyFromEnvironment = hpfe +func overrideHTTPSProxyFromEnvironment(hpfe func(req *http.Request) (*url.URL, error)) func() { + internal.HTTPSProxyFromEnvironmentForTesting = hpfe return func() { - HTTPSProxyFromEnvironment = originalHPFE + internal.HTTPSProxyFromEnvironmentForTesting = nil } } -// createTestResolverClientConn initializes a test ResolverClientConn and -// returns it along with channels for resolver state updates and errors. -func createTestResolverClientConn(t *testing.T) (*testutils.ResolverClientConn, chan resolver.State, chan error) { - stateCh := make(chan resolver.State, 1) - errCh := make(chan error, 1) - - tcc := &testutils.ResolverClientConn{ - Logger: t, - UpdateStateF: func(s resolver.State) error { stateCh <- s; return nil }, - ReportErrorF: func(err error) { errCh <- err }, - } - return tcc, stateCh, errCh -} - -// TestParsedURLForProxyEnv verifies that the parsedURLForProxy function -// correctly resolves the proxy URL for a given target address. +// Tests that the parsedURLForProxy function correctly resolves the proxy URL +// for a given target address. Tests all the possible output cases. func (s) TestParsedURLForProxyEnv(t *testing.T) { - // Overwrite the function in the test and restore them in defer. - hpfe := func(req *http.Request) (*url.URL, error) { - if req.URL.Host == targetTestAddr { - return &url.URL{ - Scheme: "https", - Host: envProxyAddr, - }, nil - } - return nil, nil - } - defer overwrite(hpfe)() - - // envTestAddr should be handled by ProxyFromEnvironment. - got, err := parsedURLForProxy(targetTestAddr) - if err != nil { - t.Errorf("Unable to get proxy URL : %v\n", err) - } - if got.Host != envProxyAddr { - t.Errorf("want %v, got %v", envProxyAddr, got) - } -} - -// TestDelegatingResolverNoProxy verifies that the delegating resolver correctly -// sends the resolved target URI to the ClientConn. -func (s) TestDelegatingResolverNoProxy(t *testing.T) { - mr := manual.NewBuilderWithScheme("test") // Set up a manual resolver to control the address resolution. - target := "test:///" + targetTestAddr - - tcc, stateCh, _ := createTestResolverClientConn(t) - // Create a delegating resolver with no proxy configuration - dr, err := New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, mr, false) - if err != nil || dr == nil { - t.Fatalf("Failed to create delegating resolver: %v", err) - } - - // Update the manual resolver with a test address. - mr.UpdateState(resolver.State{ - Addresses: []resolver.Address{{Addr: resolvedTargetTestAddr}}, - ServiceConfig: &serviceconfig.ParseResult{}, - }) - - // Verify that the delegating resolver outputs the same address. - expectedState := resolver.State{ - Addresses: []resolver.Address{{Addr: resolvedTargetTestAddr}}, - ServiceConfig: &serviceconfig.ParseResult{}, - } - - if state := <-stateCh; len(state.Addresses) != 1 || !cmp.Equal(expectedState, state) { - t.Fatalf("Unexpected state from delegating resolver: %v, want %v", state, expectedState) - } -} - -// setupDNS unregisters the DNS resolver and registers a manual resolver for the -// same scheme. This allows the test to mock the DNS resolution for the proxy resolver. -func setupDNS(t *testing.T) *manual.Resolver { - - mr := manual.NewBuilderWithScheme("dns") - - dnsResolverBuilder := resolver.Get("dns") - resolver.Register(mr) - - t.Cleanup(func() { resolver.Register(dnsResolverBuilder) }) - return mr -} - -// TestDelegatingResolverwithDNSAndProxy verifies that the delegating resolver -// correctly updates state with resolver proxy address with resolved target -// URI as attribute of the proxy address when the target URI scheme is DNS and -// a proxy is configured and target resolution is enabled. -func (s) TestDelegatingResolverwithDNSAndProxyWithTargetResolution(t *testing.T) { - hpfe := func(req *http.Request) (*url.URL, error) { - if req.URL.Host == targetTestAddr { - return &url.URL{ + err := errors.New("invalid proxy url") + tests := []struct { + name string + hpfeFunc func(req *http.Request) (*url.URL, error) + wantURL *url.URL + wantErr error + }{ + { + name: "valid proxy url and nil error", + hpfeFunc: func(_ *http.Request) (*url.URL, error) { + return &url.URL{ + Scheme: "https", + Host: "proxy.example.com", + }, nil + }, + wantURL: &url.URL{ Scheme: "https", - Host: envProxyAddr, - }, nil - } - return nil, nil - } - defer overwrite(hpfe)() - mrTarget := setupDNS(t) // Manual resolver to control the target resolution. - mrProxy := setupDNS(t) // Set up a manual DNS resolver to control the proxy address resolution. - target := "dns:///" + targetTestAddr - - tcc, stateCh, _ := createTestResolverClientConn(t) - dr, err := New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, mrTarget, true) - if err != nil { - t.Fatalf("Failed to create delegating resolver: %v", err) - } - if dr == nil { - t.Fatalf("Failed to create delegating resolver") - } - mrTarget.UpdateState(resolver.State{ - Addresses: []resolver.Address{ - {Addr: resolvedTargetTestAddr}, + Host: "proxy.example.com", + }, + wantErr: nil, }, - ServiceConfig: &serviceconfig.ParseResult{}, - }) - - mrProxy.UpdateState(resolver.State{ - Addresses: []resolver.Address{{Addr: resolvedProxyTestAddr}}, - ServiceConfig: &serviceconfig.ParseResult{}, - }) - - // Verify that the delegating resolver outputs the same address. - expectedAddr := resolver.Address{Addr: resolvedProxyTestAddr} - expectedAddr = attributes.SetUserAndConnectAddr(expectedAddr, nil, resolvedTargetTestAddr) - expectedState := resolver.State{Addresses: []resolver.Address{expectedAddr}} - - if state := <-stateCh; len(state.Addresses) != 1 || !cmp.Equal(expectedState, state) { - t.Fatalf("Unexpected state from delegating resolver: %v\n, want %v\n", state, expectedState) - } -} - -// TestDelegatingResolverwithDNSAndProxyWithNoTargetResolutionverifies that the -// delegating resolver correctly updates state with resolver proxy address with -// unresolved target URI as attribute of the proxy address when the target URI -// scheme is DNS and a proxy is configured and default target -// resolution(that is not enabled.) -func (s) TestDelegatingResolverwithDNSAndProxyWithNoTargetResolution(t *testing.T) { - hpfe := func(req *http.Request) (*url.URL, error) { - if req.URL.Host == targetTestAddr { - return &url.URL{ - Scheme: "https", - Host: envProxyAddr, - }, nil - } - return nil, nil - } - defer overwrite(hpfe)() - mrTarget := manual.NewBuilderWithScheme("test") // Manual resolver to control the target resolution. - mrProxy := setupDNS(t) // Set up a manual DNS resolver to control the proxy address resolution. - target := "dns:///" + targetTestAddr - - tcc, stateCh, _ := createTestResolverClientConn(t) - dr, err := New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, mrTarget, false) - if err != nil { - t.Fatalf("Failed to create delegating resolver: %v", err) - } - if dr == nil { - t.Fatalf("Failed to create delegating resolver") - } - - mrProxy.UpdateState(resolver.State{ - Addresses: []resolver.Address{{Addr: resolvedProxyTestAddr}}, - ServiceConfig: &serviceconfig.ParseResult{}, - }) - - // Verify that the delegating resolver outputs the same address. - expectedAddr := resolver.Address{Addr: resolvedProxyTestAddr} - expectedAddr = attributes.SetUserAndConnectAddr(expectedAddr, nil, targetTestAddr) - expectedState := resolver.State{Addresses: []resolver.Address{expectedAddr}} - - if state := <-stateCh; len(state.Addresses) != 1 || !cmp.Equal(expectedState, state) { - t.Fatalf("Unexpected state from delegating resolver: %v\n, want %v\n", state, expectedState) - } -} - -// TestDelegatingResolverwithDNSAndProxy verifies that the delegating resolver -// correctly updates state with resolved proxy address and custom resolved target -// address as the attributes when the target URI scheme is not DNS and a proxy is -// configured. -func (s) TestDelegatingResolverwithCustomResolverAndProxy(t *testing.T) { - hpfe := func(req *http.Request) (*url.URL, error) { - if req.URL.Host == targetTestAddr { - return &url.URL{ - Scheme: "https", - Host: envProxyAddr, - }, nil - } - return nil, nil - } - defer overwrite(hpfe)() - - mrTarget := manual.NewBuilderWithScheme("test") // Manual resolver to control the target resolution. - mrProxy := setupDNS(t) // Set up a manual DNS resolver to control the proxy address resolution. - target := "test:///" + targetTestAddr - - tcc, stateCh, _ := createTestResolverClientConn(t) - dr, err := New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, mrTarget, false) - if err != nil { - t.Fatalf("Failed to create delegating resolver: %v", err) - } - if dr == nil { - t.Fatalf("Failed to create delegating resolver") - } - - mrProxy.UpdateState(resolver.State{ - Addresses: []resolver.Address{ - {Addr: resolvedProxyTestAddr}, - {Addr: resolvedProxyTestAddr1}, + { + name: "invalid proxy url and non-nil error", + hpfeFunc: func(_ *http.Request) (*url.URL, error) { + return &url.URL{ + Scheme: "https", + Host: "notproxy.example.com", + }, err + }, + wantURL: nil, + wantErr: err, }, - ServiceConfig: &serviceconfig.ParseResult{}, - }) - mrTarget.UpdateState(resolver.State{ - Addresses: []resolver.Address{ - {Addr: resolvedTargetTestAddr}, - {Addr: resolvedTargetTestAddr1}, + { + name: "nil proxy url and nil error", + hpfeFunc: func(_ *http.Request) (*url.URL, error) { + return nil, nil + }, + wantURL: nil, + wantErr: nil, }, - ServiceConfig: &serviceconfig.ParseResult{}, - }) - - expectedAddr := resolver.Address{Addr: resolvedProxyTestAddr} - expectedAddr = attributes.SetUserAndConnectAddr(expectedAddr, nil, resolvedTargetTestAddr) - - expectedAddr1 := resolver.Address{Addr: resolvedProxyTestAddr} - expectedAddr1 = attributes.SetUserAndConnectAddr(expectedAddr1, nil, resolvedTargetTestAddr1) - - expectedAddr2 := resolver.Address{Addr: resolvedProxyTestAddr1} - expectedAddr2 = attributes.SetUserAndConnectAddr(expectedAddr2, nil, resolvedTargetTestAddr) - - expectedAddr3 := resolver.Address{Addr: resolvedProxyTestAddr1} - expectedAddr3 = attributes.SetUserAndConnectAddr(expectedAddr3, nil, resolvedTargetTestAddr1) - - expectedState := resolver.State{Addresses: []resolver.Address{ - expectedAddr, - expectedAddr1, - expectedAddr2, - expectedAddr3, - }, - ServiceConfig: &serviceconfig.ParseResult{}, } - - if state := <-stateCh; len(state.Addresses) != 4 || !cmp.Equal(expectedState, state) { - t.Fatalf("Unexpected state from delegating resolver: %v\n, want %v\n", state, expectedState) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer overrideHTTPSProxyFromEnvironment(tt.hpfeFunc)() + got, err := parsedURLForProxy(targetTestAddr) + if err != tt.wantErr { + t.Errorf("parsedProxyURLForProxy(%v) failed with error :%v, want %v\n", targetTestAddr, err, tt.wantErr) + } + if !cmp.Equal(got, tt.wantURL) { + t.Fatalf("parsedProxyURLForProxy(%v) = %v, want %v\n", targetTestAddr, got, tt.wantURL) + } + }) } } From 94364e2d84778d0add6179d74a3333198bf93225 Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Thu, 5 Dec 2024 10:42:27 +0530 Subject: [PATCH 03/53] Improving code --- internal/attributes/attributes.go | 63 ------------------- .../delegatingresolver/delegatingresolver.go | 15 ++++- internal/transport/http2_client.go | 6 +- internal/transport/proxy.go | 8 +-- resolver_wrapper.go | 6 +- test/proxy_test.go | 33 +++++----- 6 files changed, 39 insertions(+), 92 deletions(-) delete mode 100644 internal/attributes/attributes.go diff --git a/internal/attributes/attributes.go b/internal/attributes/attributes.go deleted file mode 100644 index 5f66ab09bb2b..000000000000 --- a/internal/attributes/attributes.go +++ /dev/null @@ -1,63 +0,0 @@ -/* - * - * 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 attributes contains functions for getting and setting attributes. -package attributes - -import ( - "net/url" - - "google.golang.org/grpc/resolver" -) - -type keyType string - -const userAndConnectAddrKey = keyType("grpc.resolver.delegatingresolver.userAndConnectAddr") - -type attr struct { - user *url.Userinfo - addr string -} - -// SetUserAndConnectAddr returns a copy of the provided resolver.Address with -// attributes containing address to be sent in connect request to proxy and the -// user info. It's data should not be mutated after calling SetConnectAddr. -func SetUserAndConnectAddr(resAddr resolver.Address, user *url.Userinfo, addr string) resolver.Address { - resAddr.Attributes = resAddr.Attributes.WithValue(userAndConnectAddrKey, attr{user: user, addr: addr}) - return resAddr -} - -// ProxyConnectAddr returns the proxy connect address in resolver.Address, or nil -// if not present. The returned data should not be mutated. -func ProxyConnectAddr(addr resolver.Address) string { - attribute := addr.Attributes.Value(userAndConnectAddrKey) - if attribute != nil { - return attribute.(attr).addr - } - return "" -} - -// User returns the user info in the resolver.Address, or nil if not present. -// The returned data should not be mutated. -func User(addr resolver.Address) *url.Userinfo { - attribute := addr.Attributes.Value(userAndConnectAddrKey) - if attribute != nil { - return attribute.(attr).user - } - return nil -} diff --git a/internal/resolver/delegatingresolver/delegatingresolver.go b/internal/resolver/delegatingresolver/delegatingresolver.go index e11531af4a4c..684de379c6da 100644 --- a/internal/resolver/delegatingresolver/delegatingresolver.go +++ b/internal/resolver/delegatingresolver/delegatingresolver.go @@ -36,8 +36,9 @@ import ( var ( // HTTPSProxyFromEnvironment will be used and overwritten in the tests. httpProxyFromEnvironmentFunc = http.ProxyFromEnvironment - - logger = grpclog.Component("delegating-resolver") + // ProxyScheme will be ovwewritten in tests + ProxyScheme = "dns" + logger = grpclog.Component("delegating-resolver") ) // delegatingResolver implements the `resolver.Resolver` interface. It uses child @@ -81,6 +82,12 @@ func parsedURLForProxy(address string) (*url.URL, error) { return url, nil } +// OnClientResolution is a no-op function in non-test code. In tests, it can +// be overwritten to send a signal to a channel, indicating that client-side +// name resolution was triggered. This enables tests to verify that resolution +// is bypassed when a proxy is in use. +var OnClientResolution = func(int) { /* no-op */ } + // New creates a new delegating resolver that is used to call the target and // proxy child resolver. If proxy is configured, both proxy and target resolvers // are used else only target resolver is used. @@ -109,6 +116,7 @@ func New(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOpti // proxy is not configured or proxy address excluded using `NO_PROXY` env var, // so only target resolver is used. if r.proxyURL == nil { + OnClientResolution(1) return targetResolverBuilder.Build(target, cc, opts) } @@ -123,6 +131,7 @@ func New(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOpti r.targetAddrs = []resolver.Address{{Addr: target.Endpoint()}} r.targetResolverReady = true } else { + OnClientResolution(1) wcc := &wrappingClientConn{ parent: r, resolverType: targetResolverType, @@ -142,7 +151,7 @@ func New(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOpti // "dns" scheme. It adjusts the proxyURL to conform to the "dns:///" format and // builds a resolver with a wrappingClientConn to capture resolved addresses. func (r *delegatingResolver) proxyURIResolver(opts resolver.BuildOptions) (resolver.Resolver, error) { - proxyBuilder := resolver.Get("dns") + proxyBuilder := resolver.Get(ProxyScheme) if proxyBuilder == nil { panic(fmt.Sprintln("delegating_resolver: resolver for proxy not found for scheme dns")) } diff --git a/internal/transport/http2_client.go b/internal/transport/http2_client.go index 831cffd587a9..1983702cb326 100644 --- a/internal/transport/http2_client.go +++ b/internal/transport/http2_client.go @@ -37,7 +37,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" "google.golang.org/grpc/internal" - "google.golang.org/grpc/internal/attributes" + "google.golang.org/grpc/internal/proxyattributes" "google.golang.org/grpc/internal/channelz" icredentials "google.golang.org/grpc/internal/credentials" "google.golang.org/grpc/internal/grpclog" @@ -157,8 +157,8 @@ type http2Client struct { func dial(ctx context.Context, fn func(context.Context, string) (net.Conn, error), addr resolver.Address, grpcUA string) (net.Conn, error) { address := addr.Addr - //if the ProxyConnectAddr is set in the aattribute, do a proxy dial. - if attributes.ProxyConnectAddr(addr) != "" { + //if the ProxyConnectAddr is set in the attribute, do a proxy dial. + if proxyattributes.ProxyConnectAddr(addr) != "" { return proxyDial(ctx, addr, grpcUA) } networkType, ok := networktype.Get(addr) diff --git a/internal/transport/proxy.go b/internal/transport/proxy.go index 8f69f3104d5f..43f1008b5138 100644 --- a/internal/transport/proxy.go +++ b/internal/transport/proxy.go @@ -30,7 +30,7 @@ import ( "net/url" "google.golang.org/grpc/internal" - "google.golang.org/grpc/internal/attributes" + "google.golang.org/grpc/internal/proxyattributes" "google.golang.org/grpc/resolver" ) @@ -62,13 +62,13 @@ func doHTTPConnectHandshake(ctx context.Context, conn net.Conn, address resolver } }() - backendAddr := attributes.ProxyConnectAddr(address) + backendAddr := proxyattributes.ProxyConnectAddr(address) req := &http.Request{ Method: http.MethodConnect, URL: &url.URL{Host: backendAddr}, Header: map[string][]string{"User-Agent": {grpcUA}}, } - if user := attributes.User(address); user != nil { + if user := proxyattributes.User(address); user != nil { u := user.Username() p, _ := user.Password() req.Header.Add(proxyAuthHeaderKey, "Basic "+basicAuth(u, p)) @@ -101,7 +101,7 @@ func doHTTPConnectHandshake(ctx context.Context, conn net.Conn, address resolver return conn, nil } -// proxyDial dials, connecting to a proxy first if necessary. Dials, does the +// proxyDial dials, connecting to a proxy first.It dials, does the // HTTP CONNECT handshake, and returns the connection. func proxyDial(ctx context.Context, address resolver.Address, grpcUA string) (net.Conn, error) { conn, err := internal.NetDialerWithTCPKeepalive().DialContext(ctx, "tcp", address.Addr) diff --git a/resolver_wrapper.go b/resolver_wrapper.go index 8873b3fc37aa..fb9d0149bf42 100644 --- a/resolver_wrapper.go +++ b/resolver_wrapper.go @@ -79,8 +79,10 @@ func (ccr *ccResolverWrapper) start() error { Authority: ccr.cc.authority, } var err error - // The delegating resolver is not used if WithNoProxy or WithCustomDialer is set. - // It is not used when explicitly disabled with TargetResolutionEnabled as well. + // The delegating resolver is used unless + // - A custom dialer is set using WithContextDialer dialoption. + // - Proxy usage is explicitly disabled using WithNoProxy dialoption. + // - Client-side resolution is explicitly enforced using WithTargetResolutionEnabled. // In these cases, the normal name resolver determined by the scheme will be used directly. if ccr.cc.dopts.copts.Dialer != nil || !ccr.cc.dopts.UseProxy { ccr.resolver, err = ccr.cc.resolverBuilder.Build(ccr.cc.parsedTarget, ccr, opts) diff --git a/test/proxy_test.go b/test/proxy_test.go index f98c8a5c0821..8019c64d809b 100644 --- a/test/proxy_test.go +++ b/test/proxy_test.go @@ -29,6 +29,7 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/internal" "google.golang.org/grpc/internal/resolver/delegatingresolver" "google.golang.org/grpc/internal/stubserver" "google.golang.org/grpc/internal/testutils" @@ -42,13 +43,12 @@ const ( unresolvedProxyURI = "proxyExample.com" ) -// overwriteAndRestore temporarily replaces `HTTPSProxyFromEnvironment` with a -// custom function and returns a function to restore the original. -func overwriteAndRestore(customFunc func(req *http.Request) (*url.URL, error)) func() { - originalFunc := delegatingresolver.HTTPSProxyFromEnvironment - delegatingresolver.HTTPSProxyFromEnvironment = customFunc +// overrideHTTPSProxyFromEnvironment function overwrites HTTPSProxyFromEnvironment and +// returns a function to restore the default values. +func overrideHTTPSProxyFromEnvironment(hpfe func(req *http.Request) (*url.URL, error)) func() { + internal.HTTPSProxyFromEnvironmentForTesting = hpfe return func() { - delegatingresolver.HTTPSProxyFromEnvironment = originalFunc + internal.HTTPSProxyFromEnvironmentForTesting = nil } } @@ -100,7 +100,7 @@ func setupProxy(t *testing.T, backendAddr string, resolutionOnClient bool, reqCh } // TestGrpcDialWithProxy tests grpc.Dial using a proxy and default -// resolver in the target URI.and verifies that it connects to the proxy server +// resolver in the target URI and verifies that it connects to the proxy server // and sends unresolved target URI in the HTTP CONNECT request and then // connects to the backend server. func (s) TestGrpcDialWithProxy(t *testing.T) { @@ -124,7 +124,7 @@ func (s) TestGrpcDialWithProxy(t *testing.T) { } return nil, nil } - defer overwriteAndRestore(hpfe)() + defer overrideHTTPSProxyFromEnvironment(hpfe)() ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() @@ -175,7 +175,7 @@ func (s) TestGrpcDialWithProxyAndResolution(t *testing.T) { } return nil, nil } - defer overwriteAndRestore(hpfe)() + defer overrideHTTPSProxyFromEnvironment(hpfe)() // Set up a manual resolver for proxy resolution. mrProxy := setupDNS(t) @@ -248,7 +248,7 @@ func (s) TestGrpcNewClientWithProxy(t *testing.T) { } return nil, nil } - defer overwriteAndRestore(hpfe)() + defer overrideHTTPSProxyFromEnvironment(hpfe)() // Set up a manual resolver for proxy resolution. mrProxy := setupDNS(t) @@ -333,7 +333,7 @@ func (s) TestGrpcNewClientWithProxyAndCustomResolver(t *testing.T) { } return nil, nil } - defer overwriteAndRestore(hpfe)() + defer overrideHTTPSProxyFromEnvironment(hpfe)() // Dial options for the gRPC client. dopts := []grpc.DialOption{ @@ -426,16 +426,15 @@ func (s) TestGrpcNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { } return nil, nil } - defer overwriteAndRestore(hpfe)() + defer overrideHTTPSProxyFromEnvironment(hpfe)() - // Configure manual resolvers for both proxy and target backends. + // Configure manual resolvers for both proxy and target backends + targetResolver := setupDNS(t) + targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) proxyResolver := manual.NewBuilderWithScheme("whatever") resolver.Register(proxyResolver) proxyResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: proxyLis.Addr().String()}}}) - targetResolver := setupDNS(t) - targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) - // Dial options with target resolution enabled. dopts := []grpc.DialOption{ grpc.WithTransportCredentials(insecure.NewCredentials()), @@ -620,7 +619,7 @@ func (s) TestBasicAuthInGrpcNewClientWithProxy(t *testing.T) { } return nil, nil } - defer overwriteAndRestore(hpfe)() + defer overrideHTTPSProxyFromEnvironment(hpfe)() // Set up a manual resolver for proxy resolution. mrProxy := setupDNS(t) From 9f6a06700aed384c3a25f350d57ccd26ff0bf76b Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Thu, 5 Dec 2024 11:09:46 +0530 Subject: [PATCH 04/53] correct import --- internal/transport/http2_client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/transport/http2_client.go b/internal/transport/http2_client.go index 1983702cb326..67eace059950 100644 --- a/internal/transport/http2_client.go +++ b/internal/transport/http2_client.go @@ -37,13 +37,13 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" "google.golang.org/grpc/internal" - "google.golang.org/grpc/internal/proxyattributes" "google.golang.org/grpc/internal/channelz" icredentials "google.golang.org/grpc/internal/credentials" "google.golang.org/grpc/internal/grpclog" "google.golang.org/grpc/internal/grpcsync" "google.golang.org/grpc/internal/grpcutil" imetadata "google.golang.org/grpc/internal/metadata" + "google.golang.org/grpc/internal/proxyattributes" istatus "google.golang.org/grpc/internal/status" isyscall "google.golang.org/grpc/internal/syscall" "google.golang.org/grpc/internal/transport/networktype" From 5ce86d057ef47d8b51e0095296a77294634a7df6 Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Thu, 5 Dec 2024 12:36:36 +0530 Subject: [PATCH 05/53] improve tests --- dialoptions.go | 4 +- internal/testutils/proxy.go | 9 +- internal/transport/http2_client.go | 3 +- test/proxy_test.go | 193 ++++++++++------------------- 4 files changed, 76 insertions(+), 133 deletions(-) diff --git a/dialoptions.go b/dialoptions.go index c64d5edb3035..6f46e14209c2 100644 --- a/dialoptions.go +++ b/dialoptions.go @@ -94,8 +94,8 @@ type dialOptions struct { idleTimeout time.Duration defaultScheme string maxCallAttempts int - // TargetResolutionEnabled specifies if the target resolution is enabled even - // when proxy is enabled. + // TargetResolutionEnabled specifies if the target resolution on client is + // enabled even when proxy is enabled. TargetResolutionEnabled bool // UseProxy specifies if a proxy should be used. UseProxy bool diff --git a/internal/testutils/proxy.go b/internal/testutils/proxy.go index 8361efcdc5b1..93655447491c 100644 --- a/internal/testutils/proxy.go +++ b/internal/testutils/proxy.go @@ -32,10 +32,9 @@ const defaultTestTimeout = 10 * time.Second // ProxyServer is a proxy server that is used for testing. type ProxyServer struct { - lis net.Listener - in net.Conn - out net.Conn - + lis net.Listener + in net.Conn + out net.Conn requestCheck func(*http.Request) error } @@ -56,6 +55,8 @@ func NewProxyServer(lis net.Listener, requestCheck func(*http.Request) error, er lis: lis, requestCheck: requestCheck, } + + // Start the proxy server. go func() { in, err := p.lis.Accept() if err != nil { diff --git a/internal/transport/http2_client.go b/internal/transport/http2_client.go index 67eace059950..be9ab69921d3 100644 --- a/internal/transport/http2_client.go +++ b/internal/transport/http2_client.go @@ -157,7 +157,8 @@ type http2Client struct { func dial(ctx context.Context, fn func(context.Context, string) (net.Conn, error), addr resolver.Address, grpcUA string) (net.Conn, error) { address := addr.Addr - //if the ProxyConnectAddr is set in the attribute, do a proxy dial. + // If the ProxyConnectAddr is set in the attribute, the proxy is set and we + // do a proxy dial. if proxyattributes.ProxyConnectAddr(addr) != "" { return proxyDial(ctx, addr, grpcUA) } diff --git a/test/proxy_test.go b/test/proxy_test.go index 8019c64d809b..1973036b5901 100644 --- a/test/proxy_test.go +++ b/test/proxy_test.go @@ -99,21 +99,26 @@ func setupProxy(t *testing.T, backendAddr string, resolutionOnClient bool, reqCh return proxyListener, errCh, doneCh, proxyStartedCh } -// TestGrpcDialWithProxy tests grpc.Dial using a proxy and default -// resolver in the target URI and verifies that it connects to the proxy server -// and sends unresolved target URI in the HTTP CONNECT request and then -// connects to the backend server. -func (s) TestGrpcDialWithProxy(t *testing.T) { - backendAddr := createAndStartBackendServer(t) - proxyListener, errCh, doneCh, _ := setupProxy(t, backendAddr, false, func(req *http.Request) error { +// requestCheck returns a function that checks the HTTP CONNECT request for the +// correct CONNECT method and address. +func requestCheck(connectAddr string) func(*http.Request) error { + return func(req *http.Request) error { if req.Method != http.MethodConnect { return fmt.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) } - if req.URL.Host != unresolvedTargetURI { - return fmt.Errorf("unexpected URL.Host %q, want %q", req.URL.Host, unresolvedTargetURI) + if req.URL.Host != connectAddr { + return fmt.Errorf("unexpected URL.Host in CONNECT req %q, want %q", req.URL.Host, connectAddr) } return nil - }) + } +} + +// Tests grpc.Dial using a proxy and default resolver in the target URI and +// verifies that it connects to the proxy server and sends unresolved target +// URI in the HTTP CONNECT request and then connects to the backend server. +func (s) TestGrpcDialWithProxy(t *testing.T) { + backendAddr := createAndStartBackendServer(t) + proxyListener, errCh, doneCh, _ := setupProxy(t, backendAddr, false, requestCheck(unresolvedTargetURI)) hpfe := func(req *http.Request) (*url.URL, error) { if req.URL.Host == unresolvedTargetURI { @@ -148,22 +153,13 @@ func (s) TestGrpcDialWithProxy(t *testing.T) { } } -// TestGrpcDialWithProxyAndResolution tests grpc.Dial with a proxy and ensures DNS -// resolution of the proxy URI is performed. +// Tests grpc.Dial with a proxy and ensures DNS resolution of the proxy URI is +// performed. func (s) TestGrpcDialWithProxyAndResolution(t *testing.T) { // Create and start a backend server. backendAddr := createAndStartBackendServer(t) - // Set up and start a proxy listener. - proxyLis, errCh, doneCh, _ := setupProxy(t, backendAddr, false, func(req *http.Request) error { - if req.Method != http.MethodConnect { - return fmt.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) - } - if req.URL.Host != unresolvedTargetURI { - return fmt.Errorf("unexpected URL.Host %q, want %q", req.URL.Host, unresolvedTargetURI) - } - return nil - }) + proxyLis, errCh, doneCh, _ := setupProxy(t, backendAddr, false, requestCheck(unresolvedTargetURI)) // Overwrite the default proxy function and restore it after the test. hpfe := func(req *http.Request) (*url.URL, error) { @@ -177,15 +173,10 @@ func (s) TestGrpcDialWithProxyAndResolution(t *testing.T) { } defer overrideHTTPSProxyFromEnvironment(hpfe)() - // Set up a manual resolver for proxy resolution. + // Set up and update a manual resolver for proxy resolution. mrProxy := setupDNS(t) + mrProxy.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: proxyLis.Addr().String()}}}) - // Update the proxy resolver state with the proxy's address. - mrProxy.InitialState(resolver.State{ - Addresses: []resolver.Address{ - {Addr: proxyLis.Addr().String()}, - }, - }) // Dial to the proxy server. ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() @@ -210,13 +201,12 @@ func (s) TestGrpcDialWithProxyAndResolution(t *testing.T) { } } -// TestGrpcNewClientWithProxy tests grpc.NewClient with default i.e DNS resolver -// for targetURI and a proxy and verifies that it connects to proxy server and -// sends unresolved target URI in the HTTP CONNECT req and connects to backend. +// Tests grpc.NewClient with default i.e DNS resolver for targetURI and a proxy +// and verifies that it connects to proxy server and sends unresolved target URI +// in the HTTP CONNECT req and connects to backend. func (s) TestGrpcNewClientWithProxy(t *testing.T) { // Set up a channel to receive signals from OnClientResolution. resolutionCh := make(chan bool, 1) - // Overwrite OnClientResolution to send a signal to the channel. origOnClientResolution := delegatingresolver.OnClientResolution delegatingresolver.OnClientResolution = func(int) { @@ -226,17 +216,8 @@ func (s) TestGrpcNewClientWithProxy(t *testing.T) { // Create and start a backend server. backendAddr := createAndStartBackendServer(t) - // Set up and start the proxy server. - proxyLis, errCh, doneCh, _ := setupProxy(t, backendAddr, false, func(req *http.Request) error { - if req.Method != http.MethodConnect { - return fmt.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) - } - if req.URL.Host != unresolvedTargetURI { - return fmt.Errorf("unexpected URL.Host %q, want %q", req.URL.Host, unresolvedTargetURI) - } - return nil - }) + proxyLis, errCh, doneCh, _ := setupProxy(t, backendAddr, false, requestCheck(unresolvedTargetURI)) // Overwrite the proxy resolution function and restore it afterward. hpfe := func(req *http.Request) (*url.URL, error) { @@ -250,10 +231,8 @@ func (s) TestGrpcNewClientWithProxy(t *testing.T) { } defer overrideHTTPSProxyFromEnvironment(hpfe)() - // Set up a manual resolver for proxy resolution. + // Set up and update a manual resolver for proxy resolution. mrProxy := setupDNS(t) - - // Update the proxy resolver state with the proxy's address. mrProxy.InitialState(resolver.State{ Addresses: []resolver.Address{ {Addr: proxyLis.Addr().String()}, @@ -263,7 +242,6 @@ func (s) TestGrpcNewClientWithProxy(t *testing.T) { // Dial to the proxy server. ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() - conn, err := grpc.NewClient(unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { t.Fatalf("grpc.NewClient failed: %v", err) @@ -289,18 +267,17 @@ func (s) TestGrpcNewClientWithProxy(t *testing.T) { case <-resolutionCh: t.Error("Client-side resolution was unexpectedly called") default: - // Success: OnClientResolution was not called. + t.Logf("target resolution did not happen on client") } } -// TestGrpcNewClientWithProxyAndCustomResolver tests grpc.NewClient with a -// resolver other than "dns" mentioned in targetURI with a proxyand verifies -// that it connects to proxy server and passes resolved target URI in the -// HTTP CONNECT req and connects to the backend server. +// Tests grpc.NewClient with a custom targetURI scheme with a proxy and +// verifies that the client connects to the proxy server, includes the resolved +// target URI in the HTTP CONNECT request, and successfully establishes a +// connection to the backend server. func (s) TestGrpcNewClientWithProxyAndCustomResolver(t *testing.T) { // Set up a channel to receive signals from OnClientResolution. resolutionCh := make(chan bool, 1) - // Overwrite OnClientResolution to send a signal to the channel. origOnClientResolution := delegatingresolver.OnClientResolution delegatingresolver.OnClientResolution = func(int) { @@ -310,21 +287,11 @@ func (s) TestGrpcNewClientWithProxyAndCustomResolver(t *testing.T) { // Create and start a backend server. backendAddr := createAndStartBackendServer(t) - // Set up and start the proxy server. - proxyLis, errCh, doneCh, _ := setupProxy(t, backendAddr, true, func(req *http.Request) error { - if req.Method != http.MethodConnect { - return fmt.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) - } - if req.URL.Host != backendAddr { - return fmt.Errorf("unexpected URL.Host %q, want %q", req.URL.Host, backendAddr) - } - return nil - }) + proxyLis, errCh, doneCh, _ := setupProxy(t, backendAddr, true, requestCheck(backendAddr)) // Overwrite the proxy resolution function and restore it afterward. hpfe := func(req *http.Request) (*url.URL, error) { - fmt.Printf("emchandwani : the request URL host is %v\n", req.URL.Host) if req.URL.Host == unresolvedTargetURI { return &url.URL{ Scheme: "https", @@ -335,26 +302,18 @@ func (s) TestGrpcNewClientWithProxyAndCustomResolver(t *testing.T) { } defer overrideHTTPSProxyFromEnvironment(hpfe)() - // Dial options for the gRPC client. - dopts := []grpc.DialOption{ - grpc.WithTransportCredentials(insecure.NewCredentials()), - } - // Create a custom resolver. + // Create and update a custom resolver for target URI. mrTarget := manual.NewBuilderWithScheme("whatever") resolver.Register(mrTarget) mrTarget.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) - - // Set up a manual resolver for proxy resolution. + // Set up and update a manual resolver for proxy resolution. mrProxy := setupDNS(t) - // Update the proxy resolver state with the proxy's address. - mrProxy.InitialState(resolver.State{ - Addresses: []resolver.Address{{Addr: proxyLis.Addr().String()}}}) + mrProxy.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: proxyLis.Addr().String()}}}) // Dial to the proxy server. ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() - - conn, err := grpc.NewClient(mrTarget.Scheme()+":///"+unresolvedTargetURI, dopts...) + conn, err := grpc.NewClient(mrTarget.Scheme()+":///"+unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { t.Fatalf("grpc.NewClient() failed: %v", err) } @@ -377,24 +336,23 @@ func (s) TestGrpcNewClientWithProxyAndCustomResolver(t *testing.T) { // Check if client-side resolution signal was sent to the channel. select { case <-resolutionCh: - // Success: OnClientResolution was called. + t.Logf("target resolution happened on client") default: - t.Fatalf("Client side resolution should be called but wasn't") + t.Fatalf("client side resolution should be called but wasn't") } } -// TestGrpcNewClientWithProxyAndTargetResolutionEnabled tests grpc.NewClient with -// the default "dns" resolver and dial option enabling target resolution on the -// client and verifies that the resoltion happens on client. +// Tests grpc.NewClient with the default "dns" resolver and dial option +// enabling target resolution on the client and verifies that the resolution +// happens on client. func (s) TestGrpcNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { - // Set up a channel to receive signals from OnClientResolution. - resolutionCh := make(chan bool, 1) - // Temporarily modify ProxyScheme for this test. origProxyScheme := delegatingresolver.ProxyScheme delegatingresolver.ProxyScheme = "whatever" t.Cleanup(func() { delegatingresolver.ProxyScheme = origProxyScheme }) + // Set up a channel to receive signals from OnClientResolution. + resolutionCh := make(chan bool, 1) // Overwrite OnClientResolution to send a signal to the channel. origOnClientResolution := delegatingresolver.OnClientResolution delegatingresolver.OnClientResolution = func(int) { @@ -404,17 +362,8 @@ func (s) TestGrpcNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { // Create and start a backend server. backendAddr := createAndStartBackendServer(t) - // Set up the proxy server. - proxyLis, errCh, doneCh, _ := setupProxy(t, backendAddr, true, func(req *http.Request) error { - if req.Method != http.MethodConnect { - return fmt.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) - } - if req.URL.Host != backendAddr { - return fmt.Errorf("unexpected URL.Host %q, want %q", req.URL.Host, backendAddr) - } - return nil - }) + proxyLis, errCh, doneCh, _ := setupProxy(t, backendAddr, true, requestCheck(backendAddr)) // Overwrite the proxy resolution function and restore it afterward. hpfe := func(req *http.Request) (*url.URL, error) { @@ -440,11 +389,9 @@ func (s) TestGrpcNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithTargetResolutionEnabled(), } - // Dial to the proxy server. ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() - conn, err := grpc.NewClient(unresolvedTargetURI, dopts...) if err != nil { t.Fatalf("grpc.NewClient() failed: %v", err) @@ -468,35 +415,32 @@ func (s) TestGrpcNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { // Check if client-side resolution signal was sent to the channel. select { case <-resolutionCh: - // Success: OnClientResolution was called. + t.Logf("target resolution happened on client") default: t.Fatalf("Client-side resolution should be called but wasn't") } } -// TestGrpcNewClientWithNoProxy tests grpc.NewClient with grpc.WithNoProxy() set -// and verifies that it does not dail to proxy, but directly to backend. +// Tests grpc.NewClient with grpc.WithNoProxy() set and verifies that it does +// not dail to proxy, but directly to backend. func (s) TestGrpcNewClientWithNoProxy(t *testing.T) { // Create and start a backend server. backendAddr := createAndStartBackendServer(t) - // Set up and start a proxy server. _, _, _, proxyStartedCh := setupProxy(t, backendAddr, true, func(req *http.Request) error { return fmt.Errorf("Proxy server received Connect req %v, but should not", req) }) + targetResolver := setupDNS(t) + targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) + // Dial options with proxy explicitly disabled. dopts := []grpc.DialOption{ grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithNoProxy(), // Disable proxy. } - - targetResolver := setupDNS(t) - targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) - // Establish a connection. ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() - conn, err := grpc.NewClient(unresolvedTargetURI, dopts...) if err != nil { t.Fatalf("grpc.NewClient() failed: %v", err) @@ -512,18 +456,17 @@ func (s) TestGrpcNewClientWithNoProxy(t *testing.T) { // Verify that the proxy was not dialed. select { case <-proxyStartedCh: - t.Error("Proxy server was dialed, but it should have been bypassed") + t.Error("Proxy server was dialed, but it shouldn't have been") default: + t.Logf("Proxy server was not dialed") } } -// TestGrpcNewClientWithContextDialer tests grpc.NewClient with -// grpc.WithContextDialer() set and verifies that it does not dial to proxy but -// rather uses the custom dialer. +// Tests grpc.NewClient with grpc.WithContextDialer() set and verifies that it +// does not dial to proxy but rather uses the custom dialer. func (s) TestGrpcNewClientWithContextDialer(t *testing.T) { backendAddr := createAndStartBackendServer(t) - // Set up and start a proxy server. _, _, _, proxyStartedCh := setupProxy(t, backendAddr, true, func(req *http.Request) error { return fmt.Errorf("Proxy server received Connect req %v, but should not", req) @@ -537,16 +480,15 @@ func (s) TestGrpcNewClientWithContextDialer(t *testing.T) { return net.Dial("tcp", backendAddr) } + targetResolver := setupDNS(t) + targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) + dopts := []grpc.DialOption{ grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithContextDialer(customDialer), } - ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() - targetResolver := setupDNS(t) - targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) - conn, err := grpc.NewClient(unresolvedTargetURI, dopts...) if err != nil { t.Fatalf("grpc.NewClient() failed: %v", err) @@ -557,26 +499,29 @@ func (s) TestGrpcNewClientWithContextDialer(t *testing.T) { if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { t.Errorf("EmptyCall() failed: %v", err) } + select { case <-dialerCalled: + t.Logf("custom dialer was called by grpc.NewClient()") default: t.Errorf("custom dialer was not called by grpc.NewClient()") } + select { case <-proxyStartedCh: t.Fatalf("Proxy Server was dialled") default: + t.Logf("Proxy Server was not dialled") } } -// TestBasicAuthInGrpcNewClientWithProxy tests grpc.NewClient with default i.e -// DNS resolver for targetURI and a proxy and verifies that it connects to proxy -// server and sends unresolved target URI in the HTTP CONNECT req and connects -// to backend. Also verifies that correct user info is sent in the CONNECT. +// Tests grpc.NewClient with default i.e DNS resolver for targetURI and a proxy +// and verifies that it connects to proxy server and sends unresolved target +// URI in the HTTP CONNECT req and connects to backend. Also verifies that +// correct user info is sent in the CONNECT. func (s) TestBasicAuthInGrpcNewClientWithProxy(t *testing.T) { // Set up a channel to receive signals from OnClientResolution. resolutionCh := make(chan bool, 1) - // Overwrite OnClientResolution to send a signal to the channel. origOnClientResolution := delegatingresolver.OnClientResolution delegatingresolver.OnClientResolution = func(int) { @@ -621,20 +566,16 @@ func (s) TestBasicAuthInGrpcNewClientWithProxy(t *testing.T) { } defer overrideHTTPSProxyFromEnvironment(hpfe)() - // Set up a manual resolver for proxy resolution. + // Set up and update a manual resolver for proxy resolution. mrProxy := setupDNS(t) - - // Update the proxy resolver state with the proxy's address. mrProxy.InitialState(resolver.State{ Addresses: []resolver.Address{ {Addr: proxyLis.Addr().String()}, }, }) - // Dial to the proxy server. ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() - conn, err := grpc.NewClient(unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { t.Fatalf("grpc.NewClient failed: %v", err) @@ -658,8 +599,8 @@ func (s) TestBasicAuthInGrpcNewClientWithProxy(t *testing.T) { // Verify if OnClientResolution was triggered. select { case <-resolutionCh: - t.Error("Client-side resolution was unexpectedly called") + t.Error("client-side resolution was unexpectedly called") default: - // Success: OnClientResolution was not called. + t.Logf("client-side resolution was not called") } } From fbef3a4e41dfee037418cb04f76e0b771ec6cbbb Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Thu, 5 Dec 2024 17:05:27 +0530 Subject: [PATCH 06/53] address comments --- .../proxyattributes/proxyattributes_test.go | 3 -- test/proxy_test.go | 33 ++++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/internal/proxyattributes/proxyattributes_test.go b/internal/proxyattributes/proxyattributes_test.go index 1f8294cdef89..46c369184eee 100644 --- a/internal/proxyattributes/proxyattributes_test.go +++ b/internal/proxyattributes/proxyattributes_test.go @@ -42,7 +42,6 @@ func (s) TestProxyConnectAddr(t *testing.T) { Addr: "test-address", Attributes: attributes.New(userAndConnectAddrKey, attr{user: nil, addr: "proxy-address"}), } - // Validate ProxyConnectAddr returns empty string for missing attributes if got, want := ProxyConnectAddr(addr), "proxy-address"; got != want { t.Errorf("Unexpected ConnectAddr proxy atrribute = %v, want : %v", got, want) @@ -56,7 +55,6 @@ func (s) TestUser(t *testing.T) { Addr: "test-address", Attributes: attributes.New(userAndConnectAddrKey, attr{user: user, addr: ""}), } - // Validate User returns nil for missing attributes if got, want := User(addr), user; got != want { t.Errorf("unexpected User proxy attribute = %v, want %v", got, want) @@ -69,7 +67,6 @@ func (s) TestEmptyProxyAttribute(t *testing.T) { addr := resolver.Address{ Addr: "test-address", } - // Validate ProxyConnectAddr returns empty string for missing attributes if got := ProxyConnectAddr(addr); got != "" { t.Errorf("Unexpected ConnectAddr proxy atrribute = %v, want empty string", got) diff --git a/test/proxy_test.go b/test/proxy_test.go index 1973036b5901..91583f7d03ad 100644 --- a/test/proxy_test.go +++ b/test/proxy_test.go @@ -116,7 +116,7 @@ func requestCheck(connectAddr string) func(*http.Request) error { // Tests grpc.Dial using a proxy and default resolver in the target URI and // verifies that it connects to the proxy server and sends unresolved target // URI in the HTTP CONNECT request and then connects to the backend server. -func (s) TestGrpcDialWithProxy(t *testing.T) { +func (s) TestGRPCDialWithProxy(t *testing.T) { backendAddr := createAndStartBackendServer(t) proxyListener, errCh, doneCh, _ := setupProxy(t, backendAddr, false, requestCheck(unresolvedTargetURI)) @@ -155,7 +155,7 @@ func (s) TestGrpcDialWithProxy(t *testing.T) { // Tests grpc.Dial with a proxy and ensures DNS resolution of the proxy URI is // performed. -func (s) TestGrpcDialWithProxyAndResolution(t *testing.T) { +func (s) TestGRPCDialWithProxyAndResolution(t *testing.T) { // Create and start a backend server. backendAddr := createAndStartBackendServer(t) // Set up and start a proxy listener. @@ -204,7 +204,7 @@ func (s) TestGrpcDialWithProxyAndResolution(t *testing.T) { // Tests grpc.NewClient with default i.e DNS resolver for targetURI and a proxy // and verifies that it connects to proxy server and sends unresolved target URI // in the HTTP CONNECT req and connects to backend. -func (s) TestGrpcNewClientWithProxy(t *testing.T) { +func (s) TestGRPCNewClientWithProxy(t *testing.T) { // Set up a channel to receive signals from OnClientResolution. resolutionCh := make(chan bool, 1) // Overwrite OnClientResolution to send a signal to the channel. @@ -265,7 +265,7 @@ func (s) TestGrpcNewClientWithProxy(t *testing.T) { // Verify if OnClientResolution was triggered. select { case <-resolutionCh: - t.Error("Client-side resolution was unexpectedly called") + t.Error("target resolution happened on client but shouldn't") default: t.Logf("target resolution did not happen on client") } @@ -275,7 +275,7 @@ func (s) TestGrpcNewClientWithProxy(t *testing.T) { // verifies that the client connects to the proxy server, includes the resolved // target URI in the HTTP CONNECT request, and successfully establishes a // connection to the backend server. -func (s) TestGrpcNewClientWithProxyAndCustomResolver(t *testing.T) { +func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { // Set up a channel to receive signals from OnClientResolution. resolutionCh := make(chan bool, 1) // Overwrite OnClientResolution to send a signal to the channel. @@ -338,14 +338,14 @@ func (s) TestGrpcNewClientWithProxyAndCustomResolver(t *testing.T) { case <-resolutionCh: t.Logf("target resolution happened on client") default: - t.Fatalf("client side resolution should be called but wasn't") + t.Fatalf("target resolution did not happen on client") } } // Tests grpc.NewClient with the default "dns" resolver and dial option -// enabling target resolution on the client and verifies that the resolution -// happens on client. -func (s) TestGrpcNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { +// grpc.WithTargetResolutionEnabled() set, enabling target resolution on the +// client and verifies that the resolution happens on client. +func (s) TestGRPCNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { // Temporarily modify ProxyScheme for this test. origProxyScheme := delegatingresolver.ProxyScheme delegatingresolver.ProxyScheme = "whatever" @@ -417,13 +417,13 @@ func (s) TestGrpcNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { case <-resolutionCh: t.Logf("target resolution happened on client") default: - t.Fatalf("Client-side resolution should be called but wasn't") + t.Fatalf("target resolution did not happen on client") } } // Tests grpc.NewClient with grpc.WithNoProxy() set and verifies that it does // not dail to proxy, but directly to backend. -func (s) TestGrpcNewClientWithNoProxy(t *testing.T) { +func (s) TestGRPCNewClientWithNoProxy(t *testing.T) { // Create and start a backend server. backendAddr := createAndStartBackendServer(t) // Set up and start a proxy server. @@ -465,7 +465,7 @@ func (s) TestGrpcNewClientWithNoProxy(t *testing.T) { // Tests grpc.NewClient with grpc.WithContextDialer() set and verifies that it // does not dial to proxy but rather uses the custom dialer. -func (s) TestGrpcNewClientWithContextDialer(t *testing.T) { +func (s) TestGRPCNewClientWithContextDialer(t *testing.T) { backendAddr := createAndStartBackendServer(t) // Set up and start a proxy server. _, _, _, proxyStartedCh := setupProxy(t, backendAddr, true, func(req *http.Request) error { @@ -545,7 +545,10 @@ func (s) TestBasicAuthInGrpcNewClientWithProxy(t *testing.T) { } wantProxyAuthStr := "Basic " + base64.StdEncoding.EncodeToString([]byte(user+":"+password)) if got := req.Header.Get("Proxy-Authorization"); got != wantProxyAuthStr { - gotDecoded, _ := base64.StdEncoding.DecodeString(got) + gotDecoded, err := base64.StdEncoding.DecodeString(got) + if err != nil { + return fmt.Errorf("filaeed to decode Proxy-Authorization header %s with error: %v", got, err) + } wantDecoded, _ := base64.StdEncoding.DecodeString(wantProxyAuthStr) return fmt.Errorf("unexpected auth %q (%q), want %q (%q)", got, gotDecoded, wantProxyAuthStr, wantDecoded) } @@ -599,8 +602,8 @@ func (s) TestBasicAuthInGrpcNewClientWithProxy(t *testing.T) { // Verify if OnClientResolution was triggered. select { case <-resolutionCh: - t.Error("client-side resolution was unexpectedly called") + t.Error("target resolution happened on client but shouldn't") default: - t.Logf("client-side resolution was not called") + t.Logf("target resolution didn't happen on client") } } From 342c332906b6ccb78801ec7782b3f83e602165ed Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Thu, 5 Dec 2024 21:23:16 +0530 Subject: [PATCH 07/53] add warning and httpfunc test --- internal/transport/proxy.go | 5 ++++- test/proxy_test.go | 26 +++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/internal/transport/proxy.go b/internal/transport/proxy.go index 43f1008b5138..0c5a0bd7dbf3 100644 --- a/internal/transport/proxy.go +++ b/internal/transport/proxy.go @@ -70,7 +70,10 @@ func doHTTPConnectHandshake(ctx context.Context, conn net.Conn, address resolver } if user := proxyattributes.User(address); user != nil { u := user.Username() - p, _ := user.Password() + p, pSet := user.Password() + if !pSet { + logger.Warningf("password not set for basic authentication for proxy dialing") + } req.Header.Add(proxyAuthHeaderKey, "Basic "+basicAuth(u, p)) } diff --git a/test/proxy_test.go b/test/proxy_test.go index 91583f7d03ad..e0dc354a2d5e 100644 --- a/test/proxy_test.go +++ b/test/proxy_test.go @@ -430,7 +430,16 @@ func (s) TestGRPCNewClientWithNoProxy(t *testing.T) { _, _, _, proxyStartedCh := setupProxy(t, backendAddr, true, func(req *http.Request) error { return fmt.Errorf("Proxy server received Connect req %v, but should not", req) }) + proxyCalled := false + hpfe := func(_ *http.Request) (*url.URL, error) { + proxyCalled = true + return &url.URL{ + Scheme: "https", + Host: unresolvedProxyURI, + }, nil + } + defer overrideHTTPSProxyFromEnvironment(hpfe)() targetResolver := setupDNS(t) targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) @@ -453,6 +462,9 @@ func (s) TestGRPCNewClientWithNoProxy(t *testing.T) { t.Errorf("EmptyCall() failed: %v", err) } + if proxyCalled { + t.Error("http.ProxyFromEnvironmentfunction was called, but it shouldn't have been") + } // Verify that the proxy was not dialed. select { case <-proxyStartedCh: @@ -472,6 +484,16 @@ func (s) TestGRPCNewClientWithContextDialer(t *testing.T) { return fmt.Errorf("Proxy server received Connect req %v, but should not", req) }) + proxyCalled := false + hpfe := func(_ *http.Request) (*url.URL, error) { + proxyCalled = true + return &url.URL{ + Scheme: "https", + Host: unresolvedProxyURI, + }, nil + + } + defer overrideHTTPSProxyFromEnvironment(hpfe)() // Create a custom dialer that directly dials the backend. We'll use this // to bypass any proxy logic. dialerCalled := make(chan bool, 1) @@ -499,7 +521,9 @@ func (s) TestGRPCNewClientWithContextDialer(t *testing.T) { if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { t.Errorf("EmptyCall() failed: %v", err) } - + if proxyCalled { + t.Error("http.ProxyFromEnvironmentfunction was called, but it shouldn't have been") + } select { case <-dialerCalled: t.Logf("custom dialer was called by grpc.NewClient()") From 8726188bd6725a00082d63be353003128913cb14 Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Thu, 5 Dec 2024 21:23:54 +0530 Subject: [PATCH 08/53] add warning and httpfunc test --- internal/transport/proxy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/transport/proxy.go b/internal/transport/proxy.go index 0c5a0bd7dbf3..a0198df9902b 100644 --- a/internal/transport/proxy.go +++ b/internal/transport/proxy.go @@ -104,7 +104,7 @@ func doHTTPConnectHandshake(ctx context.Context, conn net.Conn, address resolver return conn, nil } -// proxyDial dials, connecting to a proxy first.It dials, does the +// proxyDial dials, connecting to a proxy first. It dials, does the // HTTP CONNECT handshake, and returns the connection. func proxyDial(ctx context.Context, address resolver.Address, grpcUA string) (net.Conn, error) { conn, err := internal.NetDialerWithTCPKeepalive().DialContext(ctx, "tcp", address.Addr) From 1cfb089ccbe0a45604148de941750c6befcc1b92 Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Sat, 7 Dec 2024 16:33:28 +0530 Subject: [PATCH 09/53] from delegating_pr --- internal/proxyattributes/proxyattributes.go | 36 +-- .../proxyattributes/proxyattributes_test.go | 101 ++++--- .../delegatingresolver/delegatingresolver.go | 139 ++++++---- .../delegatingresolver_ext_test.go | 249 +++++++++++------- 4 files changed, 318 insertions(+), 207 deletions(-) diff --git a/internal/proxyattributes/proxyattributes.go b/internal/proxyattributes/proxyattributes.go index 8dd6404c0478..ed5e027b4383 100644 --- a/internal/proxyattributes/proxyattributes.go +++ b/internal/proxyattributes/proxyattributes.go @@ -30,25 +30,25 @@ type keyType string const userAndConnectAddrKey = keyType("grpc.resolver.delegatingresolver.userAndConnectAddr") -type attr struct { - user *url.Userinfo - addr string +type userAndConnectAddr struct { + user *url.Userinfo + ConnectAddr string } -// Populate returns a copy of addr with attributes containing the -// provided user and connect address, which are needed during the CONNECT -// handshake for a proxy connection. -func Populate(resAddr resolver.Address, user *url.Userinfo, addr string) resolver.Address { - resAddr.Attributes = resAddr.Attributes.WithValue(userAndConnectAddrKey, attr{user: user, addr: addr}) - return resAddr +// Populate returns a copy of addr with attributes containing the provided user +// and connect address, which are needed during the CONNECT handshake for a +// proxy connection. +func Populate(addr resolver.Address, user *url.Userinfo, connectAddr string) resolver.Address { + addr.Attributes = addr.Attributes.WithValue(userAndConnectAddrKey, userAndConnectAddr{user: user, ConnectAddr: connectAddr}) + return addr } -// ProxyConnectAddr returns the proxy connect address in resolver.Address, or nil -// if not present. The returned data should not be mutated. -func ProxyConnectAddr(addr resolver.Address) string { - attribute := addr.Attributes.Value(userAndConnectAddrKey) - if attribute != nil { - return attribute.(attr).addr +// ConnectAddr returns the proxy connect address in resolver.Address, or nil if +// not present. The returned data should not be mutated. +func ConnectAddr(addr resolver.Address) string { + a := addr.Attributes.Value(userAndConnectAddrKey) + if a != nil { + return a.(userAndConnectAddr).ConnectAddr } return "" } @@ -56,9 +56,9 @@ func ProxyConnectAddr(addr resolver.Address) string { // User returns the user info in the resolver.Address, or nil if not present. // The returned data should not be mutated. func User(addr resolver.Address) *url.Userinfo { - attribute := addr.Attributes.Value(userAndConnectAddrKey) - if attribute != nil { - return attribute.(attr).user + a := addr.Attributes.Value(userAndConnectAddrKey) + if a != nil { + return a.(userAndConnectAddr).user } return nil } diff --git a/internal/proxyattributes/proxyattributes_test.go b/internal/proxyattributes/proxyattributes_test.go index 46c369184eee..eefcd2dc8ab4 100644 --- a/internal/proxyattributes/proxyattributes_test.go +++ b/internal/proxyattributes/proxyattributes_test.go @@ -35,54 +35,77 @@ func Test(t *testing.T) { grpctest.RunSubTests(t, s{}) } -// TestProxyConnectAddr tests ProxyConnectAddr returns the coorect connect -// address in the attribute. -func (s) TestProxyConnectAddr(t *testing.T) { - addr := resolver.Address{ - Addr: "test-address", - Attributes: attributes.New(userAndConnectAddrKey, attr{user: nil, addr: "proxy-address"}), +// Tests that ConnectAddr returns the correct connect address in the attribute. +func (s) TestConnectAddr(t *testing.T) { + tests := []struct { + name string + addr resolver.Address + want string + }{ + { + name: "connect address in attribute", + addr: resolver.Address{ + Addr: "test-address", + Attributes: attributes.New(userAndConnectAddrKey, userAndConnectAddr{ + user: nil, + ConnectAddr: "proxy-address", + }), + }, + want: "proxy-address", + }, + { + name: "no attribute", + addr: resolver.Address{Addr: "test-address"}, + want: "", + }, } - // Validate ProxyConnectAddr returns empty string for missing attributes - if got, want := ProxyConnectAddr(addr), "proxy-address"; got != want { - t.Errorf("Unexpected ConnectAddr proxy atrribute = %v, want : %v", got, want) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Validate ConnectAddr returns the correct connect address in the attribute. + if got := ConnectAddr(tt.addr); got != tt.want { + t.Errorf("ConnetAddr(%v) = %v, want %v ", tt.addr, got, tt.want) + } + }) } } -// TestUser tests User returns the correct user in the attribute. -func (s) TestUser(t *testing.T) { +// Tests that User returns the correct user in the attribute. +func TestUser(t *testing.T) { user := url.UserPassword("username", "password") - addr := resolver.Address{ - Addr: "test-address", - Attributes: attributes.New(userAndConnectAddrKey, attr{user: user, addr: ""}), + tests := []struct { + name string + addr resolver.Address + want *url.Userinfo + }{ + { + name: "user in attribute", + addr: resolver.Address{ + Addr: "test-address", + Attributes: attributes.New(userAndConnectAddrKey, userAndConnectAddr{user: user, + ConnectAddr: "proxy-address", + })}, + want: user, + }, + { + name: "no attribute", + addr: resolver.Address{Addr: "test-address"}, + want: nil, + }, } - // Validate User returns nil for missing attributes - if got, want := User(addr), user; got != want { - t.Errorf("unexpected User proxy attribute = %v, want %v", got, want) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Validate User returns the correct user in the attribute. + if got := User(tt.addr); got != tt.want { + t.Errorf("User(%v) = %v, want %v ", tt.addr, got, tt.want) + } + }) } } -// TestEmptyProxyAttribute tests ProxyConnectAddr and User return empty string -// and nil respectively when not set. -func (s) TestEmptyProxyAttribute(t *testing.T) { - addr := resolver.Address{ - Addr: "test-address", - } - // Validate ProxyConnectAddr returns empty string for missing attributes - if got := ProxyConnectAddr(addr); got != "" { - t.Errorf("Unexpected ConnectAddr proxy atrribute = %v, want empty string", got) - } - // Validate User returns nil for missing attributes - if got := User(addr); got != nil { - t.Errorf("unexpected User proxy attribute = %v, want nil", got) - } -} - -// TestPopulate tests Populate returns a copy of addr with attributes -// containing correct user and connect address. +// Tests that Populate returns a copy of addr with attributes containing correct +// user and connect address. func (s) TestPopulate(t *testing.T) { - addr := resolver.Address{ - Addr: "test-address", - } + addr := resolver.Address{Addr: "test-address"} user := url.UserPassword("username", "password") connectAddr := "proxy-address" @@ -90,7 +113,7 @@ func (s) TestPopulate(t *testing.T) { populatedAddr := Populate(addr, user, connectAddr) // Verify that the returned address is updated correctly - if got, want := ProxyConnectAddr(populatedAddr), connectAddr; got != want { + if got, want := ConnectAddr(populatedAddr), connectAddr; got != want { t.Errorf("Unexpected ConnectAddr proxy atrribute = %v, want %v", got, want) } diff --git a/internal/resolver/delegatingresolver/delegatingresolver.go b/internal/resolver/delegatingresolver/delegatingresolver.go index 684de379c6da..bc490f6c6ce8 100644 --- a/internal/resolver/delegatingresolver/delegatingresolver.go +++ b/internal/resolver/delegatingresolver/delegatingresolver.go @@ -16,8 +16,8 @@ * */ -// Package delegatingresolver defines a resolver that can handle both target URI -// and proxy address resolution. +// Package delegatingresolver implements a resolver capable of resolving both +// target URIs and proxy addresses. package delegatingresolver import ( @@ -36,32 +36,35 @@ import ( var ( // HTTPSProxyFromEnvironment will be used and overwritten in the tests. httpProxyFromEnvironmentFunc = http.ProxyFromEnvironment - // ProxyScheme will be ovwewritten in tests - ProxyScheme = "dns" - logger = grpclog.Component("delegating-resolver") + + logger = grpclog.Component("delegating-resolver") ) -// delegatingResolver implements the `resolver.Resolver` interface. It uses child -// resolvers for the target and proxy resolution. It acts as an intermediatery -// between the child resolvers and the gRPC ClientConn. +// delegatingResolver manages both target URI and proxy address resolution by +// delegating these tasks to separate child resolvers. Essentially, it acts as +// a middleman 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 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 - mu sync.Mutex // protects access to the resolver state and addresses during updates - targetAddrs []resolver.Address // resolved or unresolved target addresses, depending on proxy configuration - proxyAddrs []resolver.Address // resolved proxy addresses; empty if no proxy is configured - proxyURL *url.URL // proxy URL, derived from proxy environment and target - targetResolverReady bool // indicates if an update from the target resolver has been received - proxyResolverReady bool // indicates if an update from the proxy resolver has been received + mu sync.Mutex // protects access to the resolver state and addresses during updates + targetAddrs []resolver.Address // resolved or unresolved target addresses, depending on proxy configuration + targetEndpt []resolver.Endpoint // resolved target endpoint + proxyAddrs []resolver.Address // resolved proxy addresses; empty if no proxy is configured + proxyURL *url.URL // proxy URL, derived from proxy environment and target + targetResolverReady bool // indicates if an update from the target resolver has been received + proxyResolverReady bool // indicates if an update from the proxy resolver has been received } // parsedURLForProxy 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. +// 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. @@ -82,25 +85,17 @@ func parsedURLForProxy(address string) (*url.URL, error) { return url, nil } -// OnClientResolution is a no-op function in non-test code. In tests, it can -// be overwritten to send a signal to a channel, indicating that client-side -// name resolution was triggered. This enables tests to verify that resolution -// is bypassed when a proxy is in use. -var OnClientResolution = func(int) { /* no-op */ } - -// New creates a new delegating resolver that is used to call the target and -// proxy child resolver. If proxy is configured, both proxy and target resolvers -// are used else only target resolver is used. -// -// For the target resolver: -// - If the scheme is DNS and target resolution is disabled, -// the unresolved target address is stored, allowing the proxy server to -// handle resolution instead of the client. -// - If target resolution is enabled, -// the target is resolved, and the resolved address is stored. -// -// It returns error if proxy is configured but proxy target doesn't parse to -// correct url or if target resolution at client fails. +// 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. +// - 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 New(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions, targetResolverBuilder resolver.Builder, targetResolutionEnabled bool) (resolver.Resolver, error) { r := &delegatingResolver{ target: target, @@ -110,18 +105,17 @@ func New(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOpti var err error r.proxyURL, err = parsedURLForProxy(target.Endpoint()) if err != nil { - return nil, fmt.Errorf("delegating_resolver: failed to determine proxy URL for %v target endpoint: %v", target.Endpoint(), err) + return nil, fmt.Errorf("delegating_resolver: failed to determine proxy URL for target %s: %v", target, err) } - // proxy is not configured or proxy address excluded using `NO_PROXY` env var, - // so only target resolver is used. + // proxy is not configured or proxy address excluded using `NO_PROXY` env + // var, so only target resolver is used. if r.proxyURL == nil { - OnClientResolution(1) return targetResolverBuilder.Build(target, cc, opts) } if logger.V(2) { - logger.Info("delegating_resolver: Proxy URL detected : %+v", r.proxyURL) + logger.Info("Proxy URL detected : %s", r.proxyURL) } // When the scheme is 'dns' and target resolution on client is not enabled, @@ -131,18 +125,17 @@ func New(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOpti r.targetAddrs = []resolver.Address{{Addr: target.Endpoint()}} r.targetResolverReady = true } else { - OnClientResolution(1) wcc := &wrappingClientConn{ parent: r, resolverType: targetResolverType, } if r.targetResolver, err = targetResolverBuilder.Build(target, wcc, opts); err != nil { - return nil, fmt.Errorf("delegating_resolver: unable to build the resolver for target %v : %v", target, err) + return nil, fmt.Errorf("delegating_resolver: unable to build the resolver for target %s: %v", target, err) } } if r.proxyResolver, err = r.proxyURIResolver(opts); err != nil { - return nil, fmt.Errorf("delegating_resolver: unable to build the resolver for proxy : %v", err) + return nil, fmt.Errorf("delegating_resolver: failed to build resolver for proxy URL %q: %v", r.proxyURL, err) } return r, nil } @@ -151,7 +144,7 @@ func New(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOpti // "dns" scheme. It adjusts the proxyURL to conform to the "dns:///" format and // builds a resolver with a wrappingClientConn to capture resolved addresses. func (r *delegatingResolver) proxyURIResolver(opts resolver.BuildOptions) (resolver.Resolver, error) { - proxyBuilder := resolver.Get(ProxyScheme) + proxyBuilder := resolver.Get("dns") if proxyBuilder == nil { panic(fmt.Sprintln("delegating_resolver: resolver for proxy not found for scheme dns")) } @@ -185,11 +178,11 @@ func (r *delegatingResolver) Close() { } } -// updateState creates a list of combined addresses by pairing each proxy address -// with every target address. For each pair, it generates a new `resolver.Address` -// using the proxy address, and adding the target address as the attribute -// along with user info. -func (r *delegatingResolver) updateState() []resolver.Address { +// generateCombinedAddressesLocked creates a list of combined addresses by +// pairing each proxy address with every target address. For each pair, it +// generates a new [resolver.Address] using the proxy address, and adding the +// target address as the attribute along with user info. +func (r *delegatingResolver) generateCombinedAddressesLocked() ([]resolver.Address, []resolver.Endpoint) { var addresses []resolver.Address for _, proxyAddr := range r.proxyAddrs { for _, targetAddr := range r.targetAddrs { @@ -198,7 +191,24 @@ func (r *delegatingResolver) updateState() []resolver.Address { addresses = append(addresses, newAddr) } } - return addresses + + if r.targetEndpt == nil { + return addresses, nil + } + + var endpoints []resolver.Endpoint + for _, proxyAddr := range r.proxyAddrs { + for _, endpt := range r.targetEndpt { + var addrs []resolver.Address + for _, targetAddr := range endpt.Addresses { + newAddr := resolver.Address{Addr: proxyAddr.Addr} + newAddr = proxyattributes.Populate(newAddr, r.proxyURL.User, targetAddr.Addr) + addrs = append(addrs, newAddr) + } + endpoints = append(endpoints, resolver.Endpoint{Addresses: addrs}) + } + } + return addresses, endpoints } // resolverType is an enum representing the type of resolver (target or proxy). @@ -209,10 +219,13 @@ const ( proxyResolverType ) -// wrappingClientConn wraps around the client connection, intercepting state -// updates, errors, and new resolved addresses from the target and proxy -// resolvers. It facilitates combining the results from both resolvers and -// passing them to the clientConn. +// 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. +// +// Its primary function is to aggregate addresses returned by the child +// resolvers before passing them to the parent ClientConn. Any errors returned +// by the child resolvers are propagated verbatim to the parent ClientConn. type wrappingClientConn struct { parent *delegatingResolver resolverType resolverType // represents the type of resolver (target or proxy) @@ -222,16 +235,30 @@ type wrappingClientConn struct { func (wcc *wrappingClientConn) UpdateState(state resolver.State) error { wcc.parent.mu.Lock() defer wcc.parent.mu.Unlock() + var curState resolver.State if wcc.resolverType == targetResolverType { + if logger.V(2) { + logger.Infof("Addresses received from target resolver: %v", state.Addresses) + } wcc.parent.targetAddrs = state.Addresses - logger.Infof("delegating_resolver: %v addresses received from target resolver", len(wcc.parent.targetAddrs)) + wcc.parent.targetEndpt = state.Endpoints wcc.parent.targetResolverReady = true + // Update curState to include other state information, such as the + // service config, provided by the target resolver. This ensures + // curState contains all necessary information when passed to + // UpdateState. 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. curState = state } + // The proxy resolver is always "dns," so only the address will be + // received, not an endpoint. if wcc.resolverType == proxyResolverType { + if logger.V(2) { + logger.Infof("Addresses received from proxy resolver: %s", state.Addresses) + } wcc.parent.proxyAddrs = state.Addresses - logger.Infof("delegating_resolver: %v addresses received from proxy resolver", len(wcc.parent.proxyAddrs)) wcc.parent.proxyResolverReady = true } @@ -239,7 +266,7 @@ func (wcc *wrappingClientConn) UpdateState(state resolver.State) error { if !wcc.parent.targetResolverReady || !wcc.parent.proxyResolverReady { return nil } - curState.Addresses = wcc.parent.updateState() + curState.Addresses, curState.Endpoints = wcc.parent.generateCombinedAddressesLocked() return wcc.parent.cc.UpdateState(curState) } diff --git a/internal/resolver/delegatingresolver/delegatingresolver_ext_test.go b/internal/resolver/delegatingresolver/delegatingresolver_ext_test.go index 704559a2070d..734cd791d4e4 100644 --- a/internal/resolver/delegatingresolver/delegatingresolver_ext_test.go +++ b/internal/resolver/delegatingresolver/delegatingresolver_ext_test.go @@ -22,10 +22,12 @@ import ( "net/http" "net/url" "testing" + "time" "github.com/google/go-cmp/cmp" "google.golang.org/grpc/internal" "google.golang.org/grpc/internal/grpctest" + "google.golang.org/grpc/internal/pretty" "google.golang.org/grpc/internal/proxyattributes" "google.golang.org/grpc/internal/resolver/delegatingresolver" "google.golang.org/grpc/internal/testutils" @@ -45,23 +47,16 @@ func Test(t *testing.T) { const ( targetTestAddr = "test.com" - resolvedTargetTestAddr = "1.2.3.4:8080" - resolvedTargetTestAddr1 = "1.2.3.5:8080" + resolvedTargetTestAddr1 = "1.2.3.4:8080" + resolvedTargetTestAddr2 = "1.2.3.5:8080" envProxyAddr = "proxytest.com" - resolvedProxyTestAddr = "2.3.4.5:7687" - resolvedProxyTestAddr1 = "2.3.4.6:7687" + resolvedProxyTestAddr1 = "2.3.4.5:7687" + resolvedProxyTestAddr2 = "2.3.4.6:7687" + defaultTestTimeout = 10 * time.Second + defaultTestShortTimeout = 10 * time.Millisecond ) -// overrideHTTPSProxyFromEnvironment function overwrites HTTPSProxyFromEnvironment and -// returns a function to restore the default values. -func overrideHTTPSProxyFromEnvironment(hpfe func(req *http.Request) (*url.URL, error)) func() { - internal.HTTPSProxyFromEnvironmentForTesting = hpfe - return func() { - internal.HTTPSProxyFromEnvironmentForTesting = nil - } -} - -// createTestResolverClientConn initializes a test ResolverClientConn and +// createTestResolverClientConn initializes a [testutils.ResolverClientConn] and // returns it along with channels for resolver state updates and errors. func createTestResolverClientConn(t *testing.T) (*testutils.ResolverClientConn, chan resolver.State, chan error) { stateCh := make(chan resolver.State, 1) @@ -75,42 +70,57 @@ func createTestResolverClientConn(t *testing.T) (*testutils.ResolverClientConn, return tcc, stateCh, errCh } -// Tests that the delegating resolver behaves correctly when no proxy -// environment variables are set. In this case, no proxy resolver is created, -// only a target resolver is used, and the addresses returned by the delegating -// resolver should exactly match those returned by the target resolver. -func (s) TestDelegatingResolverNoProxy(t *testing.T) { +// Tests the scenario where no proxy environment variables are set or proxying +// is disabled by the `NO_PROXY` environment variable. The test verifies that +// the delegating resolver creates only a target resolver and that the +// addresses returned by the delegating resolver exactly match those returned +// by the target resolver. +func (s) TestDelegatingResolverNoProxyEnvVarsSet(t *testing.T) { + hpfe := func(req *http.Request) (*url.URL, error) { return nil, nil } + originalhpfe := internal.HTTPSProxyFromEnvironmentForTesting + internal.HTTPSProxyFromEnvironmentForTesting = hpfe + defer func() { + internal.HTTPSProxyFromEnvironmentForTesting = originalhpfe + }() + mr := manual.NewBuilderWithScheme("test") // Set up a manual resolver to control the address resolution. target := mr.Scheme() + ":///" + targetTestAddr - tcc, stateCh, _ := createTestResolverClientConn(t) // Create a delegating resolver with no proxy configuration - _, err := delegatingresolver.New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, mr, false) - if err != nil { + tcc, stateCh, _ := createTestResolverClientConn(t) + if _, err := delegatingresolver.New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, mr, false); err != nil { t.Fatalf("Failed to create delegating resolver: %v", err) } // Update the manual resolver with a test address. mr.UpdateState(resolver.State{ - Addresses: []resolver.Address{{Addr: resolvedTargetTestAddr}}, + Addresses: []resolver.Address{{Addr: resolvedTargetTestAddr1}}, ServiceConfig: &serviceconfig.ParseResult{}, }) - // Verify that the delegating resolver outputs the same address. - wantedState := resolver.State{ - Addresses: []resolver.Address{{Addr: resolvedTargetTestAddr}}, + // Verify that the delegating resolver outputs the same addresses, as returned + // by the target resolver. + wantState := resolver.State{ + Addresses: []resolver.Address{{Addr: resolvedTargetTestAddr1}}, ServiceConfig: &serviceconfig.ParseResult{}, } - if state := <-stateCh; !cmp.Equal(wantedState, state) { - t.Fatalf("Unexpected state from delegating resolver: %v, want %v", state, wantedState) + var gotState resolver.State + select { + case gotState = <-stateCh: + case <-time.After(defaultTestTimeout): + t.Fatal("Timeout when waiting for a state update from the delegating resolver") + } + + if !cmp.Equal(gotState, wantState) { + t.Fatalf("Unexpected state from delegating resolver: %s, want %s", pretty.ToJSON(gotState), pretty.ToJSON(wantState)) } } // setupDNS registers a new manual resolver for the DNS scheme, effectively // overwriting the previously registered DNS resolver. This allows the test to -// mock the DNS resolution for the proxy resolver. It also registers the original -// DNS resolver after the test is done. +// mock the DNS resolution for the proxy resolver. It also registers the +// original DNS resolver after the test is done. func setupDNS(t *testing.T) *manual.Resolver { mr := manual.NewBuilderWithScheme("dns") @@ -121,10 +131,18 @@ func setupDNS(t *testing.T) *manual.Resolver { return mr } -// Tests that the delegating resolver correctly updates state with resolver -// proxy address with resolved target URI as attribute of the proxy address when -// the target URI scheme is DNS and a proxy is configured and target resolution -// is enabled. +// proxyAddressWithTargetAttribute creates a resolver.Address for the proxy, +// adding the target address as an attribute. +func proxyAddressWithTargetAttribute(proxyAddr string, targetAddr string) resolver.Address { + addr := resolver.Address{Addr: proxyAddr} + addr = proxyattributes.Populate(addr, nil, targetAddr) + return addr +} + +// Tests the scenario where proxy is configured and the target URI contains the +// "dns" scheme and target resolution is enabled. The test verifies that the +// addresses returned by the delegating resolver combines the addresses +// returned by the proxy resolver and the target resolver. func (s) TestDelegatingResolverwithDNSAndProxyWithTargetResolution(t *testing.T) { hpfe := func(req *http.Request) (*url.URL, error) { if req.URL.Host == targetTestAddr { @@ -135,43 +153,69 @@ func (s) TestDelegatingResolverwithDNSAndProxyWithTargetResolution(t *testing.T) } return nil, nil } - defer overrideHTTPSProxyFromEnvironment(hpfe)() + originalhpfe := internal.HTTPSProxyFromEnvironmentForTesting + internal.HTTPSProxyFromEnvironmentForTesting = hpfe + defer func() { + internal.HTTPSProxyFromEnvironmentForTesting = originalhpfe + }() + mrTarget := setupDNS(t) // Manual resolver to control the target resolution. - mrProxy := setupDNS(t) // Set up a manual DNS resolver to control the proxy address resolution. - target := "dns:///" + targetTestAddr + target := mrTarget.Scheme() + ":///" + targetTestAddr + mrProxy := setupDNS(t) // Set up a manual DNS resolver to control the proxy address resolution. tcc, stateCh, _ := createTestResolverClientConn(t) - _, err := delegatingresolver.New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, mrTarget, true) - if err != nil { + if _, err := delegatingresolver.New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, mrTarget, true); err != nil { t.Fatalf("Failed to create delegating resolver: %v", err) } - mrTarget.UpdateState(resolver.State{ + mrProxy.UpdateState(resolver.State{ Addresses: []resolver.Address{ - {Addr: resolvedTargetTestAddr}, + {Addr: resolvedProxyTestAddr1}, + {Addr: resolvedProxyTestAddr2}, }, ServiceConfig: &serviceconfig.ParseResult{}, }) - mrProxy.UpdateState(resolver.State{ - Addresses: []resolver.Address{{Addr: resolvedProxyTestAddr}}, + select { + case <-stateCh: + t.Fatalf("Delegating resolver invoked UpdateState before both the proxy and target resolvers had updated their states.") + case <-time.After(defaultTestShortTimeout): + } + + mrTarget.UpdateState(resolver.State{ + Addresses: []resolver.Address{ + {Addr: resolvedTargetTestAddr1}, + {Addr: resolvedTargetTestAddr2}, + }, ServiceConfig: &serviceconfig.ParseResult{}, }) - // Verify that the delegating resolver outputs the same address. - wantedAddr := resolver.Address{Addr: resolvedProxyTestAddr} - wantedAddr = proxyattributes.Populate(wantedAddr, nil, resolvedTargetTestAddr) - wantedState := resolver.State{Addresses: []resolver.Address{wantedAddr}} + // Verify that the delegating resolver outputs the expected address. + wantState := resolver.State{Addresses: []resolver.Address{ + proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, resolvedTargetTestAddr1), + proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, resolvedTargetTestAddr2), + proxyAddressWithTargetAttribute(resolvedProxyTestAddr2, resolvedTargetTestAddr1), + proxyAddressWithTargetAttribute(resolvedProxyTestAddr2, resolvedTargetTestAddr2), + }, + ServiceConfig: &serviceconfig.ParseResult{}, + } + var gotState resolver.State + select { + case gotState = <-stateCh: + case <-time.After(defaultTestTimeout): + t.Fatal("Timeout when waiting for a state update from the delegating resolver") + } - if state := <-stateCh; len(state.Addresses) != 1 || !cmp.Equal(wantedState, state) { - t.Fatalf("Unexpected state from delegating resolver: %v\n, want %v\n", state, wantedState) + if !cmp.Equal(gotState, wantState) { + t.Fatalf("Unexpected state from delegating resolver: %s, want %s", pretty.ToJSON(gotState), pretty.ToJSON(wantState)) } } -// Tests that the delegating resolver correctly updates state with resolver -// proxy address with unresolved target URI as attribute of the proxy address -// when the target URI scheme is DNS and a proxy is configured and default target -// resolution(that is not enabled.) +// Tests the scenario where a proxy is configured, the target URI contains the +// "dns" scheme, and target resolution is disabled(default behavior). The test +// verifies that the addresses returned by the delegating resolver include the +// proxy resolver's addresses, with the unresolved target URI as an attribute +// of the proxy address. func (s) TestDelegatingResolverwithDNSAndProxyWithNoTargetResolution(t *testing.T) { hpfe := func(req *http.Request) (*url.URL, error) { if req.URL.Host == targetTestAddr { @@ -182,36 +226,49 @@ func (s) TestDelegatingResolverwithDNSAndProxyWithNoTargetResolution(t *testing. } return nil, nil } - defer overrideHTTPSProxyFromEnvironment(hpfe)() + originalhpfe := internal.HTTPSProxyFromEnvironmentForTesting + internal.HTTPSProxyFromEnvironmentForTesting = hpfe + defer func() { + internal.HTTPSProxyFromEnvironmentForTesting = originalhpfe + }() + mrTarget := manual.NewBuilderWithScheme("test") // Manual resolver to control the target resolution. - mrProxy := setupDNS(t) // Set up a manual DNS resolver to control the proxy address resolution. target := "dns:///" + targetTestAddr + mrProxy := setupDNS(t) // Set up a manual DNS resolver to control the proxy address resolution. tcc, stateCh, _ := createTestResolverClientConn(t) - _, err := delegatingresolver.New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, mrTarget, false) - if err != nil { + if _, err := delegatingresolver.New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, mrTarget, false); err != nil { t.Fatalf("Failed to create delegating resolver: %v", err) } mrProxy.UpdateState(resolver.State{ - Addresses: []resolver.Address{{Addr: resolvedProxyTestAddr}}, - ServiceConfig: &serviceconfig.ParseResult{}, + Addresses: []resolver.Address{ + {Addr: resolvedProxyTestAddr1}, + {Addr: resolvedProxyTestAddr2}, + }, }) - // Verify that the delegating resolver outputs the same address. - wantedAddr := resolver.Address{Addr: resolvedProxyTestAddr} - wantedAddr = proxyattributes.Populate(wantedAddr, nil, targetTestAddr) - wantedState := resolver.State{Addresses: []resolver.Address{wantedAddr}} + wantState := resolver.State{Addresses: []resolver.Address{ + proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, targetTestAddr), + proxyAddressWithTargetAttribute(resolvedProxyTestAddr2, targetTestAddr), + }} - if state := <-stateCh; len(state.Addresses) != 1 || !cmp.Equal(wantedState, state) { - t.Fatalf("Unexpected state from delegating resolver: %v\n, want %v\n", state, wantedState) + var gotState resolver.State + select { + case gotState = <-stateCh: + case <-time.After(defaultTestTimeout): + t.Fatal("Timeout when waiting for a state update from the delegating resolver") + } + + if !cmp.Equal(gotState, wantState) { + t.Fatalf("Unexpected state from delegating resolver: %s, want %s", pretty.ToJSON(gotState), pretty.ToJSON(wantState)) } } -// Tests that the delegating resolver -// correctly updates state with resolved proxy address and custom resolved target -// address as the proxyattributes when the target URI scheme is not DNS and a proxy is -// configured. +// Tests the scenario where a proxy is configured, and the target URI scheme is +// not "dns". The test verifies that the addresses returned by the delegating +// resolver include the resolved proxy address and the custom resolved target +// address as attributes of the proxy address. func (s) TestDelegatingResolverwithCustomResolverAndProxy(t *testing.T) { hpfe := func(req *http.Request) (*url.URL, error) { if req.URL.Host == targetTestAddr { @@ -222,55 +279,59 @@ func (s) TestDelegatingResolverwithCustomResolverAndProxy(t *testing.T) { } return nil, nil } - defer overrideHTTPSProxyFromEnvironment(hpfe)() + originalhpfe := internal.HTTPSProxyFromEnvironmentForTesting + internal.HTTPSProxyFromEnvironmentForTesting = hpfe + defer func() { + internal.HTTPSProxyFromEnvironmentForTesting = originalhpfe + }() mrTarget := manual.NewBuilderWithScheme("test") // Manual resolver to control the target resolution. - mrProxy := setupDNS(t) // Set up a manual DNS resolver to control the proxy address resolution. target := mrTarget.Scheme() + ":///" + targetTestAddr + mrProxy := setupDNS(t) // Set up a manual DNS resolver to control the proxy address resolution. tcc, stateCh, _ := createTestResolverClientConn(t) - _, err := delegatingresolver.New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, mrTarget, false) - if err != nil { + if _, err := delegatingresolver.New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, mrTarget, false); err != nil { t.Fatalf("Failed to create delegating resolver: %v", err) } mrProxy.UpdateState(resolver.State{ Addresses: []resolver.Address{ - {Addr: resolvedProxyTestAddr}, {Addr: resolvedProxyTestAddr1}, + {Addr: resolvedProxyTestAddr2}, }, ServiceConfig: &serviceconfig.ParseResult{}, }) + + select { + case <-stateCh: + t.Fatalf("Delegating resolver invoked UpdateState before both the proxy and target resolvers had updated their states.") + case <-time.After(defaultTestShortTimeout): + } + mrTarget.UpdateState(resolver.State{ Addresses: []resolver.Address{ - {Addr: resolvedTargetTestAddr}, {Addr: resolvedTargetTestAddr1}, + {Addr: resolvedTargetTestAddr2}, }, ServiceConfig: &serviceconfig.ParseResult{}, }) - wantedAddr := resolver.Address{Addr: resolvedProxyTestAddr} - wantedAddr = proxyattributes.Populate(wantedAddr, nil, resolvedTargetTestAddr) - - wantedAddr1 := resolver.Address{Addr: resolvedProxyTestAddr} - wantedAddr1 = proxyattributes.Populate(wantedAddr1, nil, resolvedTargetTestAddr1) - - wantedAddr2 := resolver.Address{Addr: resolvedProxyTestAddr1} - wantedAddr2 = proxyattributes.Populate(wantedAddr2, nil, resolvedTargetTestAddr) - - wantedAddr3 := resolver.Address{Addr: resolvedProxyTestAddr1} - wantedAddr3 = proxyattributes.Populate(wantedAddr3, nil, resolvedTargetTestAddr1) - - wantedState := resolver.State{Addresses: []resolver.Address{ - wantedAddr, - wantedAddr1, - wantedAddr2, - wantedAddr3, + wantState := resolver.State{Addresses: []resolver.Address{ + proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, resolvedTargetTestAddr1), + proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, resolvedTargetTestAddr2), + proxyAddressWithTargetAttribute(resolvedProxyTestAddr2, resolvedTargetTestAddr1), + proxyAddressWithTargetAttribute(resolvedProxyTestAddr2, resolvedTargetTestAddr2), }, ServiceConfig: &serviceconfig.ParseResult{}, } + var gotState resolver.State + select { + case gotState = <-stateCh: + case <-time.After(defaultTestTimeout): + t.Fatal("Timeout when waiting for a state update from the delegating resolver") + } - if state := <-stateCh; len(state.Addresses) != 4 || !cmp.Equal(wantedState, state) { - t.Fatalf("Unexpected state from delegating resolver: %v\n, want %v\n", state, wantedState) + if !cmp.Equal(gotState, wantState) { + t.Fatalf("Unexpected state from delegating resolver: %s, want %s", pretty.ToJSON(gotState), pretty.ToJSON(wantState)) } } From 32d9de5e7847cabdee94eb7a92754633d7b89ea0 Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Sun, 8 Dec 2024 17:15:09 +0530 Subject: [PATCH 10/53] improve code --- dialoptions.go | 6 +- .../delegatingresolver/delegatingresolver.go | 15 +- internal/testutils/proxy.go | 20 +- internal/transport/http2_client.go | 6 +- internal/transport/proxy.go | 24 +- resolver_wrapper.go | 11 +- test/proxy_test.go | 278 ++++++++++-------- 7 files changed, 204 insertions(+), 156 deletions(-) diff --git a/dialoptions.go b/dialoptions.go index 6f46e14209c2..bd814a3eecad 100644 --- a/dialoptions.go +++ b/dialoptions.go @@ -94,8 +94,8 @@ type dialOptions struct { idleTimeout time.Duration defaultScheme string maxCallAttempts int - // TargetResolutionEnabled specifies if the target resolution on client is - // enabled even when proxy is enabled. + // TargetResolutionEnabled indicates whether the client should perform + // target resolution even when a proxy is enabled. TargetResolutionEnabled bool // UseProxy specifies if a proxy should be used. UseProxy bool @@ -387,7 +387,7 @@ func WithNoProxy() DialOption { } // WithTargetResolutionEnabled returns a DialOption which enables target -// resolution on client. +// resolution on client. This is ignored if WithNoProxy is used. func WithTargetResolutionEnabled() DialOption { return newFuncDialOption(func(o *dialOptions) { o.TargetResolutionEnabled = true diff --git a/internal/resolver/delegatingresolver/delegatingresolver.go b/internal/resolver/delegatingresolver/delegatingresolver.go index bc490f6c6ce8..93e130f78fad 100644 --- a/internal/resolver/delegatingresolver/delegatingresolver.go +++ b/internal/resolver/delegatingresolver/delegatingresolver.go @@ -36,8 +36,9 @@ import ( var ( // HTTPSProxyFromEnvironment will be used and overwritten in the tests. httpProxyFromEnvironmentFunc = http.ProxyFromEnvironment - - logger = grpclog.Component("delegating-resolver") + // ProxyScheme will be ovwewritten in tests + ProxyScheme = "dns" + logger = grpclog.Component("delegating-resolver") ) // delegatingResolver manages both target URI and proxy address resolution by @@ -85,6 +86,12 @@ func parsedURLForProxy(address string) (*url.URL, error) { return url, nil } +// OnClientResolution is a no-op function in non-test code. In tests, it can +// be overwritten to send a signal to a channel, indicating that client-side +// name resolution was triggered. This enables tests to verify that resolution +// on client side is bypassed when a proxy is in use. +var OnClientResolution = func(int) { /* no-op */ } + // New creates a new delegating resolver that can create up to two child // resolvers: // - one to resolve the proxy address specified using the supported @@ -111,6 +118,7 @@ func New(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOpti // proxy is not configured or proxy address excluded using `NO_PROXY` env // var, so only target resolver is used. if r.proxyURL == nil { + OnClientResolution(1) return targetResolverBuilder.Build(target, cc, opts) } @@ -125,6 +133,7 @@ func New(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOpti r.targetAddrs = []resolver.Address{{Addr: target.Endpoint()}} r.targetResolverReady = true } else { + OnClientResolution(1) wcc := &wrappingClientConn{ parent: r, resolverType: targetResolverType, @@ -144,7 +153,7 @@ func New(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOpti // "dns" scheme. It adjusts the proxyURL to conform to the "dns:///" format and // builds a resolver with a wrappingClientConn to capture resolved addresses. func (r *delegatingResolver) proxyURIResolver(opts resolver.BuildOptions) (resolver.Resolver, error) { - proxyBuilder := resolver.Get("dns") + proxyBuilder := resolver.Get(ProxyScheme) if proxyBuilder == nil { panic(fmt.Sprintln("delegating_resolver: resolver for proxy not found for scheme dns")) } diff --git a/internal/testutils/proxy.go b/internal/testutils/proxy.go index 93655447491c..3ebf5d3ec3ee 100644 --- a/internal/testutils/proxy.go +++ b/internal/testutils/proxy.go @@ -30,15 +30,15 @@ import ( const defaultTestTimeout = 10 * time.Second -// ProxyServer is a proxy server that is used for testing. +// ProxyServer represents a test proxy server. type ProxyServer struct { lis net.Listener - in net.Conn - out net.Conn - requestCheck func(*http.Request) error + in net.Conn // The connection from the client to the proxy. + out net.Conn // The connection from the proxy to the backend. + requestCheck func(*http.Request) error // The function to check the request sent to proxy. } -// Stop functions stops and cleans up the proxy server. +// Stop closes the ProxyServer and its connectionsto client and server. func (p *ProxyServer) Stop() { p.lis.Close() if p.in != nil { @@ -50,10 +50,10 @@ func (p *ProxyServer) Stop() { } // NewProxyServer create and starts a proxy server. -func NewProxyServer(lis net.Listener, requestCheck func(*http.Request) error, errCh chan error, doneCh chan struct{}, backendAddr string, resolutionOnClient bool, proxyServerStarted func()) *ProxyServer { +func NewProxyServer(lis net.Listener, reqCheck func(*http.Request) error, errCh chan error, doneCh chan struct{}, backendAddr string, resOnClient bool, proxyStarted func()) *ProxyServer { p := &ProxyServer{ lis: lis, - requestCheck: requestCheck, + requestCheck: reqCheck, } // Start the proxy server. @@ -63,8 +63,8 @@ func NewProxyServer(lis net.Listener, requestCheck func(*http.Request) error, er return } p.in = in - if proxyServerStarted != nil { - proxyServerStarted() + if proxyStarted != nil { + proxyStarted() } req, err := http.ReadRequest(bufio.NewReader(in)) if err != nil { @@ -81,7 +81,7 @@ func NewProxyServer(lis net.Listener, requestCheck func(*http.Request) error, er var out net.Conn // if resolution is done on client,connect to address received in // CONNECT request or else connect to backend address directly. - if resolutionOnClient { + if resOnClient { out, err = net.Dial("tcp", req.URL.Host) } else { out, err = net.Dial("tcp", backendAddr) diff --git a/internal/transport/http2_client.go b/internal/transport/http2_client.go index be9ab69921d3..bc13dc77594e 100644 --- a/internal/transport/http2_client.go +++ b/internal/transport/http2_client.go @@ -157,9 +157,9 @@ type http2Client struct { func dial(ctx context.Context, fn func(context.Context, string) (net.Conn, error), addr resolver.Address, grpcUA string) (net.Conn, error) { address := addr.Addr - // If the ProxyConnectAddr is set in the attribute, the proxy is set and we - // do a proxy dial. - if proxyattributes.ProxyConnectAddr(addr) != "" { + // A non-empty ConnectAddr attribute indicates that a proxy is configured, + // so initiate a proxy dial. + if proxyattributes.ConnectAddr(addr) != "" { return proxyDial(ctx, addr, grpcUA) } networkType, ok := networktype.Get(addr) diff --git a/internal/transport/proxy.go b/internal/transport/proxy.go index a0198df9902b..d94d30fe5dba 100644 --- a/internal/transport/proxy.go +++ b/internal/transport/proxy.go @@ -37,10 +37,9 @@ import ( const proxyAuthHeaderKey = "Proxy-Authorization" // To read a response from a net.Conn, http.ReadResponse() takes a bufio.Reader. -// It's possible that this reader reads more than what's need for the response and stores -// those bytes in the buffer. -// bufConn wraps the original net.Conn and the bufio.Reader to make sure we don't lose the -// bytes in the buffer. +// It's possible that this reader reads more than what's need for the response +// and stores those bytes in the buffer. bufConn wraps the original net.Conn +// and the bufio.Reader to make sure we don't lose the bytes in the buffer. type bufConn struct { net.Conn r io.Reader @@ -55,20 +54,20 @@ func basicAuth(username, password string) string { return base64.StdEncoding.EncodeToString([]byte(auth)) } -func doHTTPConnectHandshake(ctx context.Context, conn net.Conn, address resolver.Address, grpcUA string) (_ net.Conn, err error) { +func doHTTPConnectHandshake(ctx context.Context, conn net.Conn, addr resolver.Address, grpcUA string) (_ net.Conn, err error) { defer func() { if err != nil { conn.Close() } }() - backendAddr := proxyattributes.ProxyConnectAddr(address) + a := proxyattributes.ConnectAddr(addr) req := &http.Request{ Method: http.MethodConnect, - URL: &url.URL{Host: backendAddr}, + URL: &url.URL{Host: a}, Header: map[string][]string{"User-Agent": {grpcUA}}, } - if user := proxyattributes.User(address); user != nil { + if user := proxyattributes.User(addr); user != nil { u := user.Username() p, pSet := user.Password() if !pSet { @@ -104,14 +103,13 @@ func doHTTPConnectHandshake(ctx context.Context, conn net.Conn, address resolver return conn, nil } -// proxyDial dials, connecting to a proxy first. It dials, does the -// HTTP CONNECT handshake, and returns the connection. -func proxyDial(ctx context.Context, address resolver.Address, grpcUA string) (net.Conn, error) { - conn, err := internal.NetDialerWithTCPKeepalive().DialContext(ctx, "tcp", address.Addr) +// proxyDial establishes a TCP connection to the specified address and performs an HTTP CONNECT handshake. +func proxyDial(ctx context.Context, addr resolver.Address, grpcUA string) (net.Conn, error) { + conn, err := internal.NetDialerWithTCPKeepalive().DialContext(ctx, "tcp", addr.Addr) if err != nil { return nil, err } - return doHTTPConnectHandshake(ctx, conn, address, grpcUA) + return doHTTPConnectHandshake(ctx, conn, addr, grpcUA) } func sendHTTPRequest(ctx context.Context, req *http.Request, conn net.Conn) error { diff --git a/resolver_wrapper.go b/resolver_wrapper.go index fb9d0149bf42..fd49c0204344 100644 --- a/resolver_wrapper.go +++ b/resolver_wrapper.go @@ -79,11 +79,12 @@ func (ccr *ccResolverWrapper) start() error { Authority: ccr.cc.authority, } var err error - // The delegating resolver is used unless - // - A custom dialer is set using WithContextDialer dialoption. - // - Proxy usage is explicitly disabled using WithNoProxy dialoption. - // - Client-side resolution is explicitly enforced using WithTargetResolutionEnabled. - // In these cases, the normal name resolver determined by the scheme will be used directly. + // The delegating resolver is used unless: + // - A custom dialer is provided via WithContextDialer dialoption. + // - Proxy usage is disabled through WithNoProxy dialoption. + // - Client-side resolution is enforced with WithTargetResolutionEnabled. + // In these cases, the resolver is built based on the scheme of target, + // using the appropriate resolver builder. if ccr.cc.dopts.copts.Dialer != nil || !ccr.cc.dopts.UseProxy { ccr.resolver, err = ccr.cc.resolverBuilder.Build(ccr.cc.parsedTarget, ccr, opts) } else { diff --git a/test/proxy_test.go b/test/proxy_test.go index e0dc354a2d5e..e75e0884f8cd 100644 --- a/test/proxy_test.go +++ b/test/proxy_test.go @@ -40,18 +40,9 @@ import ( const ( unresolvedTargetURI = "example.com" - unresolvedProxyURI = "proxyExample.com" + unresolvedProxyURI = "proxyexample.com" ) -// overrideHTTPSProxyFromEnvironment function overwrites HTTPSProxyFromEnvironment and -// returns a function to restore the default values. -func overrideHTTPSProxyFromEnvironment(hpfe func(req *http.Request) (*url.URL, error)) func() { - internal.HTTPSProxyFromEnvironmentForTesting = hpfe - return func() { - internal.HTTPSProxyFromEnvironmentForTesting = nil - } -} - // createAndStartBackendServer creates and starts a test backend server, // registering a cleanup to stop it. func createAndStartBackendServer(t *testing.T) string { @@ -59,7 +50,7 @@ func createAndStartBackendServer(t *testing.T) string { EmptyCallF: func(context.Context, *testgrpc.Empty) (*testgrpc.Empty, error) { return &testgrpc.Empty{}, nil }, } if err := backend.StartServer(); err != nil { - t.Fatalf("Failed to start backend: %v", err) + t.Fatalf("failed to start backend: %v", err) } t.Logf("Started TestService backend at: %q", backend.Address) t.Cleanup(backend.Stop) @@ -69,19 +60,19 @@ func createAndStartBackendServer(t *testing.T) string { // setupDNS sets up a manual DNS resolver and registers it, returning the // builder for test customization. func setupDNS(t *testing.T) *manual.Resolver { - manualResolver := manual.NewBuilderWithScheme("dns") - originalResolver := resolver.Get("dns") - resolver.Register(manualResolver) + mr := manual.NewBuilderWithScheme("dns") + origRes := resolver.Get("dns") + resolver.Register(mr) t.Cleanup(func() { - resolver.Register(originalResolver) + resolver.Register(origRes) }) - return manualResolver + return mr } // setupProxy initializes and starts a proxy server, registers a cleanup to // stop it, and returns the proxy's listener and helper channels. func setupProxy(t *testing.T, backendAddr string, resolutionOnClient bool, reqCheck func(*http.Request) error) (net.Listener, chan error, chan struct{}, chan struct{}) { - proxyListener, err := net.Listen("tcp", "localhost:0") + pLis, err := net.Listen("tcp", "localhost:0") if err != nil { t.Fatalf("failed to listen: %v", err) } @@ -93,10 +84,10 @@ func setupProxy(t *testing.T, backendAddr string, resolutionOnClient bool, reqCh close(errCh) }) - proxyServer := testutils.NewProxyServer(proxyListener, reqCheck, errCh, doneCh, backendAddr, resolutionOnClient, func() { close(proxyStartedCh) }) + proxyServer := testutils.NewProxyServer(pLis, reqCheck, errCh, doneCh, backendAddr, resolutionOnClient, func() { close(proxyStartedCh) }) t.Cleanup(proxyServer.Stop) - return proxyListener, errCh, doneCh, proxyStartedCh + return pLis, errCh, doneCh, proxyStartedCh } // requestCheck returns a function that checks the HTTP CONNECT request for the @@ -113,30 +104,34 @@ func requestCheck(connectAddr string) func(*http.Request) error { } } -// Tests grpc.Dial using a proxy and default resolver in the target URI and -// verifies that it connects to the proxy server and sends unresolved target -// URI in the HTTP CONNECT request and then connects to the backend server. +// Tests the scenario where grpc.Dial is performed using a proxy with the +// default resolver in the target URI. The test verifies that the connection is +// established to the proxy server, sends the unresolved target URI in the HTTP +// CONNECT request, and is successfully connected to the backend server. func (s) TestGRPCDialWithProxy(t *testing.T) { backendAddr := createAndStartBackendServer(t) - proxyListener, errCh, doneCh, _ := setupProxy(t, backendAddr, false, requestCheck(unresolvedTargetURI)) + pLis, errCh, doneCh, _ := setupProxy(t, backendAddr, false, requestCheck(unresolvedTargetURI)) hpfe := func(req *http.Request) (*url.URL, error) { if req.URL.Host == unresolvedTargetURI { return &url.URL{ Scheme: "https", - Host: proxyListener.Addr().String(), + Host: pLis.Addr().String(), }, nil } return nil, nil } - defer overrideHTTPSProxyFromEnvironment(hpfe)() + orighpfe := internal.HTTPSProxyFromEnvironmentForTesting + internal.HTTPSProxyFromEnvironmentForTesting = hpfe + defer func() { + internal.HTTPSProxyFromEnvironmentForTesting = orighpfe + }() ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() - conn, err := grpc.Dial(unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { - t.Fatalf("grpc.Dial failed: %v", err) + t.Fatalf("grpc.Dial(%s) failed: %v", unresolvedTargetURI, err) } defer conn.Close() @@ -153,13 +148,15 @@ func (s) TestGRPCDialWithProxy(t *testing.T) { } } -// Tests grpc.Dial with a proxy and ensures DNS resolution of the proxy URI is -// performed. -func (s) TestGRPCDialWithProxyAndResolution(t *testing.T) { - // Create and start a backend server. +// Tests the scenario where `grpc.Dial` is performed with a proxy and the "dns" +// scheme for the target. The test verifies that the proxy URI is correctly +// resolved and that the target URI resolution on the client preserves the +// original behavior of `grpc.Dial`. It also ensures that a connection is +// established to the proxy server, with the resolved target URI sent in the +// HTTP CONNECT request, successfully connecting to the backend server. +func (s) TestGRPCDialWithDNSAndProxy(t *testing.T) { backendAddr := createAndStartBackendServer(t) - // Set up and start a proxy listener. - proxyLis, errCh, doneCh, _ := setupProxy(t, backendAddr, false, requestCheck(unresolvedTargetURI)) + pLis, errCh, doneCh, _ := setupProxy(t, backendAddr, false, requestCheck(backendAddr)) // Overwrite the default proxy function and restore it after the test. hpfe := func(req *http.Request) (*url.URL, error) { @@ -171,18 +168,29 @@ func (s) TestGRPCDialWithProxyAndResolution(t *testing.T) { } return nil, nil } - defer overrideHTTPSProxyFromEnvironment(hpfe)() + orighpfe := internal.HTTPSProxyFromEnvironmentForTesting + internal.HTTPSProxyFromEnvironmentForTesting = hpfe + defer func() { + internal.HTTPSProxyFromEnvironmentForTesting = orighpfe + }() - // Set up and update a manual resolver for proxy resolution. - mrProxy := setupDNS(t) - mrProxy.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: proxyLis.Addr().String()}}}) + // Configure manual resolvers for both proxy and target backends + mrTarget := setupDNS(t) + mrTarget.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) + // Temporarily modify ProxyScheme for this test. + origScheme := delegatingresolver.ProxyScheme + delegatingresolver.ProxyScheme = "whatever" + t.Cleanup(func() { delegatingresolver.ProxyScheme = origScheme }) + mrProxy := manual.NewBuilderWithScheme("whatever") + resolver.Register(mrProxy) + mrProxy.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: pLis.Addr().String()}}}) // Dial to the proxy server. ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() - conn, err := grpc.Dial(unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) + conn, err := grpc.Dial(mrTarget.Scheme()+":///"+unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { - t.Fatalf("grpc.Dial failed: %v", err) + t.Fatalf("grpc.Dial(%s) failed: %v", mrTarget.Scheme()+":///"+unresolvedTargetURI, err) } defer conn.Close() @@ -201,9 +209,11 @@ func (s) TestGRPCDialWithProxyAndResolution(t *testing.T) { } } -// Tests grpc.NewClient with default i.e DNS resolver for targetURI and a proxy -// and verifies that it connects to proxy server and sends unresolved target URI -// in the HTTP CONNECT req and connects to backend. +// Tests the scenario where grpc.NewClient is used with the default DNS +// resolver for the target URI and a proxy is configured. The test verifies +// that the client. resolves proxy URI, connects to the proxy server, sends the +// unresolved target URI in the HTTP CONNECT request, and successfully +// establishes a connection to the backend server. func (s) TestGRPCNewClientWithProxy(t *testing.T) { // Set up a channel to receive signals from OnClientResolution. resolutionCh := make(chan bool, 1) @@ -214,10 +224,8 @@ func (s) TestGRPCNewClientWithProxy(t *testing.T) { } t.Cleanup(func() { delegatingresolver.OnClientResolution = origOnClientResolution }) - // Create and start a backend server. backendAddr := createAndStartBackendServer(t) - // Set up and start the proxy server. - proxyLis, errCh, doneCh, _ := setupProxy(t, backendAddr, false, requestCheck(unresolvedTargetURI)) + pLis, errCh, doneCh, _ := setupProxy(t, backendAddr, false, requestCheck(unresolvedTargetURI)) // Overwrite the proxy resolution function and restore it afterward. hpfe := func(req *http.Request) (*url.URL, error) { @@ -229,13 +237,17 @@ func (s) TestGRPCNewClientWithProxy(t *testing.T) { } return nil, nil } - defer overrideHTTPSProxyFromEnvironment(hpfe)() + orighpfe := internal.HTTPSProxyFromEnvironmentForTesting + internal.HTTPSProxyFromEnvironmentForTesting = hpfe + defer func() { + internal.HTTPSProxyFromEnvironmentForTesting = orighpfe + }() // Set up and update a manual resolver for proxy resolution. mrProxy := setupDNS(t) mrProxy.InitialState(resolver.State{ Addresses: []resolver.Address{ - {Addr: proxyLis.Addr().String()}, + {Addr: pLis.Addr().String()}, }, }) @@ -265,16 +277,17 @@ func (s) TestGRPCNewClientWithProxy(t *testing.T) { // Verify if OnClientResolution was triggered. select { case <-resolutionCh: - t.Error("target resolution happened on client but shouldn't") + t.Fatal("target resolution occurred on client unexpectedly") default: - t.Logf("target resolution did not happen on client") + t.Log("target resolution did not occur on the client") } } -// Tests grpc.NewClient with a custom targetURI scheme with a proxy and -// verifies that the client connects to the proxy server, includes the resolved -// target URI in the HTTP CONNECT request, and successfully establishes a -// connection to the backend server. +// Tests the scenario where grpc.NewClient is used with a custom target URI +// scheme and a proxy is configured. The test verifies that the client +// successfully connects to the proxy server, resolves the proxy URI correctly, +// includes the resolved target URI in the HTTP CONNECT request, and +// establishes a connection to the backend server. func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { // Set up a channel to receive signals from OnClientResolution. resolutionCh := make(chan bool, 1) @@ -285,10 +298,9 @@ func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { } t.Cleanup(func() { delegatingresolver.OnClientResolution = origOnClientResolution }) - // Create and start a backend server. backendAddr := createAndStartBackendServer(t) // Set up and start the proxy server. - proxyLis, errCh, doneCh, _ := setupProxy(t, backendAddr, true, requestCheck(backendAddr)) + pLis, errCh, doneCh, _ := setupProxy(t, backendAddr, true, requestCheck(backendAddr)) // Overwrite the proxy resolution function and restore it afterward. hpfe := func(req *http.Request) (*url.URL, error) { @@ -300,7 +312,11 @@ func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { } return nil, nil } - defer overrideHTTPSProxyFromEnvironment(hpfe)() + orighpfe := internal.HTTPSProxyFromEnvironmentForTesting + internal.HTTPSProxyFromEnvironmentForTesting = hpfe + defer func() { + internal.HTTPSProxyFromEnvironmentForTesting = orighpfe + }() // Create and update a custom resolver for target URI. mrTarget := manual.NewBuilderWithScheme("whatever") @@ -308,14 +324,14 @@ func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { mrTarget.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) // Set up and update a manual resolver for proxy resolution. mrProxy := setupDNS(t) - mrProxy.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: proxyLis.Addr().String()}}}) + mrProxy.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: pLis.Addr().String()}}}) // Dial to the proxy server. ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() conn, err := grpc.NewClient(mrTarget.Scheme()+":///"+unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { - t.Fatalf("grpc.NewClient() failed: %v", err) + t.Fatalf("grpc.NewClient(%s) failed: %v", mrTarget.Scheme()+":///"+unresolvedTargetURI, err) } t.Cleanup(func() { conn.Close() }) @@ -336,21 +352,19 @@ func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { // Check if client-side resolution signal was sent to the channel. select { case <-resolutionCh: - t.Logf("target resolution happened on client") + t.Log("target resolution occurred on client") default: - t.Fatalf("target resolution did not happen on client") + t.Fatal("target resolution did not occur on the client unexpectedly") } } -// Tests grpc.NewClient with the default "dns" resolver and dial option -// grpc.WithTargetResolutionEnabled() set, enabling target resolution on the -// client and verifies that the resolution happens on client. +// Tests the scenario where grpc.NewClient is used with the default "dns" +// resolver and the dial option grpc.WithTargetResolutionEnabled() is set, +// enabling target resolution on the client. The test verifies that target +// resolution happens on the client by sending resolved target URi in HTTP +// CONNECT request, the proxy URI is resolved correctly, and the connection is +// successfully established with the backend server through the proxy. func (s) TestGRPCNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { - // Temporarily modify ProxyScheme for this test. - origProxyScheme := delegatingresolver.ProxyScheme - delegatingresolver.ProxyScheme = "whatever" - t.Cleanup(func() { delegatingresolver.ProxyScheme = origProxyScheme }) - // Set up a channel to receive signals from OnClientResolution. resolutionCh := make(chan bool, 1) // Overwrite OnClientResolution to send a signal to the channel. @@ -360,10 +374,8 @@ func (s) TestGRPCNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { } t.Cleanup(func() { delegatingresolver.OnClientResolution = origOnClientResolution }) - // Create and start a backend server. backendAddr := createAndStartBackendServer(t) - // Set up the proxy server. - proxyLis, errCh, doneCh, _ := setupProxy(t, backendAddr, true, requestCheck(backendAddr)) + pLis, errCh, doneCh, _ := setupProxy(t, backendAddr, true, requestCheck(backendAddr)) // Overwrite the proxy resolution function and restore it afterward. hpfe := func(req *http.Request) (*url.URL, error) { @@ -375,14 +387,22 @@ func (s) TestGRPCNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { } return nil, nil } - defer overrideHTTPSProxyFromEnvironment(hpfe)() + orighpfe := internal.HTTPSProxyFromEnvironmentForTesting + internal.HTTPSProxyFromEnvironmentForTesting = hpfe + defer func() { + internal.HTTPSProxyFromEnvironmentForTesting = orighpfe + }() // Configure manual resolvers for both proxy and target backends - targetResolver := setupDNS(t) - targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) - proxyResolver := manual.NewBuilderWithScheme("whatever") - resolver.Register(proxyResolver) - proxyResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: proxyLis.Addr().String()}}}) + mrTarget := setupDNS(t) + mrTarget.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) + // Temporarily modify ProxyScheme for this test. + origScheme := delegatingresolver.ProxyScheme + delegatingresolver.ProxyScheme = "whatever" + t.Cleanup(func() { delegatingresolver.ProxyScheme = origScheme }) + mrProxy := manual.NewBuilderWithScheme("whatever") + resolver.Register(mrProxy) + mrProxy.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: pLis.Addr().String()}}}) // Dial options with target resolution enabled. dopts := []grpc.DialOption{ @@ -394,7 +414,7 @@ func (s) TestGRPCNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { defer cancel() conn, err := grpc.NewClient(unresolvedTargetURI, dopts...) if err != nil { - t.Fatalf("grpc.NewClient() failed: %v", err) + t.Fatalf("grpc.NewClient(%s) failed: %v", unresolvedTargetURI, err) } t.Cleanup(func() { conn.Close() }) @@ -415,21 +435,23 @@ func (s) TestGRPCNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { // Check if client-side resolution signal was sent to the channel. select { case <-resolutionCh: - t.Logf("target resolution happened on client") + t.Log("target resolution occured on client") default: - t.Fatalf("target resolution did not happen on client") + t.Fatal("target resolution did not occur on client unexpectedly") } } -// Tests grpc.NewClient with grpc.WithNoProxy() set and verifies that it does -// not dail to proxy, but directly to backend. +// Tests the scenario where grpc.NewClient is used with grpc.WithNoProxy() set, +// explicitly disabling proxy usage. The test verifies that the client does not +// dial the proxy but directly connects to the backend server. It also checks +// that the proxy resolution function is not called and that the proxy server +// never receives a connection request. func (s) TestGRPCNewClientWithNoProxy(t *testing.T) { - // Create and start a backend server. backendAddr := createAndStartBackendServer(t) - // Set up and start a proxy server. _, _, _, proxyStartedCh := setupProxy(t, backendAddr, true, func(req *http.Request) error { - return fmt.Errorf("Proxy server received Connect req %v, but should not", req) + return fmt.Errorf("proxy server should not have received a Connect request: %v", req) }) + proxyCalled := false hpfe := func(_ *http.Request) (*url.URL, error) { proxyCalled = true @@ -439,9 +461,14 @@ func (s) TestGRPCNewClientWithNoProxy(t *testing.T) { }, nil } - defer overrideHTTPSProxyFromEnvironment(hpfe)() - targetResolver := setupDNS(t) - targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) + orighpfe := internal.HTTPSProxyFromEnvironmentForTesting + internal.HTTPSProxyFromEnvironmentForTesting = hpfe + defer func() { + internal.HTTPSProxyFromEnvironmentForTesting = orighpfe + }() + + mrTarget := setupDNS(t) + mrTarget.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) // Dial options with proxy explicitly disabled. dopts := []grpc.DialOption{ @@ -452,7 +479,7 @@ func (s) TestGRPCNewClientWithNoProxy(t *testing.T) { defer cancel() conn, err := grpc.NewClient(unresolvedTargetURI, dopts...) if err != nil { - t.Fatalf("grpc.NewClient() failed: %v", err) + t.Fatalf("grpc.NewClient(%s) failed: %v", unresolvedTargetURI, err) } defer conn.Close() @@ -463,25 +490,26 @@ func (s) TestGRPCNewClientWithNoProxy(t *testing.T) { } if proxyCalled { - t.Error("http.ProxyFromEnvironmentfunction was called, but it shouldn't have been") + t.Error("http.ProxyFromEnvironment function was unexpectedly called") } // Verify that the proxy was not dialed. select { case <-proxyStartedCh: - t.Error("Proxy server was dialed, but it shouldn't have been") + t.Fatal("unexpected dial to proxy server") default: - t.Logf("Proxy server was not dialed") + t.Log("proxy server was not dialed") } - } -// Tests grpc.NewClient with grpc.WithContextDialer() set and verifies that it -// does not dial to proxy but rather uses the custom dialer. +// Tests the scenario where grpc.NewClient is used with grpc.WithContextDialer() +// set. The test verifies that the client bypasses proxy dialing and uses the +// custom dialer instead. It ensures that the proxy server is never dialed, the +// proxy resolution function is not triggered, and the custom dialer is invoked +// as expected. func (s) TestGRPCNewClientWithContextDialer(t *testing.T) { backendAddr := createAndStartBackendServer(t) - // Set up and start a proxy server. _, _, _, proxyStartedCh := setupProxy(t, backendAddr, true, func(req *http.Request) error { - return fmt.Errorf("Proxy server received Connect req %v, but should not", req) + return fmt.Errorf("proxy server should not have received a Connect request: %v", req) }) proxyCalled := false @@ -493,7 +521,12 @@ func (s) TestGRPCNewClientWithContextDialer(t *testing.T) { }, nil } - defer overrideHTTPSProxyFromEnvironment(hpfe)() + orighpfe := internal.HTTPSProxyFromEnvironmentForTesting + internal.HTTPSProxyFromEnvironmentForTesting = hpfe + defer func() { + internal.HTTPSProxyFromEnvironmentForTesting = orighpfe + }() + // Create a custom dialer that directly dials the backend. We'll use this // to bypass any proxy logic. dialerCalled := make(chan bool, 1) @@ -502,8 +535,8 @@ func (s) TestGRPCNewClientWithContextDialer(t *testing.T) { return net.Dial("tcp", backendAddr) } - targetResolver := setupDNS(t) - targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) + mrTarget := setupDNS(t) + mrTarget.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) dopts := []grpc.DialOption{ grpc.WithTransportCredentials(insecure.NewCredentials()), @@ -513,7 +546,7 @@ func (s) TestGRPCNewClientWithContextDialer(t *testing.T) { defer cancel() conn, err := grpc.NewClient(unresolvedTargetURI, dopts...) if err != nil { - t.Fatalf("grpc.NewClient() failed: %v", err) + t.Fatalf("grpc.NewClient(%s) failed: %v", unresolvedTargetURI, err) } defer conn.Close() @@ -521,28 +554,33 @@ func (s) TestGRPCNewClientWithContextDialer(t *testing.T) { if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { t.Errorf("EmptyCall() failed: %v", err) } + if proxyCalled { - t.Error("http.ProxyFromEnvironmentfunction was called, but it shouldn't have been") + t.Error("http.ProxyFromEnvironment function was unexpectedly called") } + select { case <-dialerCalled: - t.Logf("custom dialer was called by grpc.NewClient()") + t.Log("custom dialer was invoked") default: - t.Errorf("custom dialer was not called by grpc.NewClient()") + t.Error("custom dialer was not invoked") } select { case <-proxyStartedCh: - t.Fatalf("Proxy Server was dialled") + t.Fatal("unexpected dial to proxy server") default: - t.Logf("Proxy Server was not dialled") + t.Log("proxy server was not dialled") } } -// Tests grpc.NewClient with default i.e DNS resolver for targetURI and a proxy -// and verifies that it connects to proxy server and sends unresolved target -// URI in the HTTP CONNECT req and connects to backend. Also verifies that -// correct user info is sent in the CONNECT. +// Tests the scenario where grpc.NewClient is used with the default DNS resolver +// for targetURI and a proxy. The test verifies that the client connects to the +// proxy server, sends the unresolved target URI in the HTTP CONNECT request, +// and successfully connects to the backend. Additionally, it checks that the +// correct user information is included in the Proxy-Authorization header of +// the CONNECT request. The test also ensures that target resolution does not +// happen on the client. func (s) TestBasicAuthInGrpcNewClientWithProxy(t *testing.T) { // Set up a channel to receive signals from OnClientResolution. resolutionCh := make(chan bool, 1) @@ -553,14 +591,12 @@ func (s) TestBasicAuthInGrpcNewClientWithProxy(t *testing.T) { } t.Cleanup(func() { delegatingresolver.OnClientResolution = origOnClientResolution }) - // Create and start a backend server. backendAddr := createAndStartBackendServer(t) const ( user = "notAUser" password = "notAPassword" ) - // Set up and start the proxy server. - proxyLis, errCh, doneCh, _ := setupProxy(t, backendAddr, false, func(req *http.Request) error { + pLis, errCh, doneCh, _ := setupProxy(t, backendAddr, false, func(req *http.Request) error { if req.Method != http.MethodConnect { return fmt.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) } @@ -571,7 +607,7 @@ func (s) TestBasicAuthInGrpcNewClientWithProxy(t *testing.T) { if got := req.Header.Get("Proxy-Authorization"); got != wantProxyAuthStr { gotDecoded, err := base64.StdEncoding.DecodeString(got) if err != nil { - return fmt.Errorf("filaeed to decode Proxy-Authorization header %s with error: %v", got, err) + return fmt.Errorf("failed to decode Proxy-Authorization header: %v", err) } wantDecoded, _ := base64.StdEncoding.DecodeString(wantProxyAuthStr) return fmt.Errorf("unexpected auth %q (%q), want %q (%q)", got, gotDecoded, wantProxyAuthStr, wantDecoded) @@ -591,13 +627,17 @@ func (s) TestBasicAuthInGrpcNewClientWithProxy(t *testing.T) { } return nil, nil } - defer overrideHTTPSProxyFromEnvironment(hpfe)() + orighpfe := internal.HTTPSProxyFromEnvironmentForTesting + internal.HTTPSProxyFromEnvironmentForTesting = hpfe + defer func() { + internal.HTTPSProxyFromEnvironmentForTesting = orighpfe + }() // Set up and update a manual resolver for proxy resolution. mrProxy := setupDNS(t) mrProxy.InitialState(resolver.State{ Addresses: []resolver.Address{ - {Addr: proxyLis.Addr().String()}, + {Addr: pLis.Addr().String()}, }, }) @@ -605,7 +645,7 @@ func (s) TestBasicAuthInGrpcNewClientWithProxy(t *testing.T) { defer cancel() conn, err := grpc.NewClient(unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { - t.Fatalf("grpc.NewClient failed: %v", err) + t.Fatalf("grpc.NewClient(%s) failed: %v", unresolvedTargetURI, err) } defer conn.Close() @@ -626,8 +666,8 @@ func (s) TestBasicAuthInGrpcNewClientWithProxy(t *testing.T) { // Verify if OnClientResolution was triggered. select { case <-resolutionCh: - t.Error("target resolution happened on client but shouldn't") + t.Fatal("target resolution occurred on client unexpectedly") default: - t.Logf("target resolution didn't happen on client") + t.Log("target resolution did not occur on the client") } } From 34b902f0c2860f10a52bae360197f367cb2a60e6 Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Sun, 8 Dec 2024 17:16:51 +0530 Subject: [PATCH 11/53] improve code --- test/proxy_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/proxy_test.go b/test/proxy_test.go index e75e0884f8cd..106ed6fbf08d 100644 --- a/test/proxy_test.go +++ b/test/proxy_test.go @@ -435,7 +435,7 @@ func (s) TestGRPCNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { // Check if client-side resolution signal was sent to the channel. select { case <-resolutionCh: - t.Log("target resolution occured on client") + t.Log("target resolution occurred on client") default: t.Fatal("target resolution did not occur on client unexpectedly") } From 11c8fb16b9f866e559fd7b75f30cf9c60186c9ee Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Tue, 10 Dec 2024 00:49:50 +0530 Subject: [PATCH 12/53] improve --- dialoptions.go | 21 +++++++++------- internal/internal.go | 4 +++- internal/proxyattributes/proxyattributes.go | 20 +++++++++------- .../proxyattributes/proxyattributes_test.go | 21 +++++++++------- .../delegatingresolver/delegatingresolver.go | 17 +++++++++---- .../delegatingresolver_ext_test.go | 5 +++- resolver_wrapper.go | 4 ++-- test/proxy_test.go | 24 +++++++++---------- 8 files changed, 70 insertions(+), 46 deletions(-) diff --git a/dialoptions.go b/dialoptions.go index bd814a3eecad..3950cc96853a 100644 --- a/dialoptions.go +++ b/dialoptions.go @@ -94,11 +94,11 @@ type dialOptions struct { idleTimeout time.Duration defaultScheme string maxCallAttempts int - // TargetResolutionEnabled indicates whether the client should perform + // targetResolutionEnabled indicates whether the client should perform // target resolution even when a proxy is enabled. - TargetResolutionEnabled bool - // UseProxy specifies if a proxy should be used. - UseProxy bool + targetResolutionEnabled bool + // useProxy specifies if a proxy should be used. + useProxy bool } // DialOption configures how we set up the connection. @@ -382,15 +382,20 @@ func WithInsecure() DialOption { // later release. func WithNoProxy() DialOption { return newFuncDialOption(func(o *dialOptions) { - o.UseProxy = false + o.useProxy = false }) } // WithTargetResolutionEnabled returns a DialOption which enables target // resolution on client. This is ignored if WithNoProxy is used. +// +// # Experimental +// +// Notice: This API is EXPERIMENTAL and may be changed or removed in a +// later release. func WithTargetResolutionEnabled() DialOption { return newFuncDialOption(func(o *dialOptions) { - o.TargetResolutionEnabled = true + o.targetResolutionEnabled = true }) } @@ -682,8 +687,8 @@ func defaultDialOptions() dialOptions { idleTimeout: 30 * time.Minute, defaultScheme: "dns", maxCallAttempts: defaultMaxCallAttempts, - UseProxy: true, - TargetResolutionEnabled: false, + useProxy: true, + targetResolutionEnabled: false, } } diff --git a/internal/internal.go b/internal/internal.go index 1098a837a06d..134c11ea7869 100644 --- a/internal/internal.go +++ b/internal/internal.go @@ -22,6 +22,8 @@ package internal import ( "context" + "net/http" + "net/url" "time" "google.golang.org/grpc/connectivity" @@ -242,7 +244,7 @@ var ( // HTTPSProxyFromEnvironmentForTesting returns the URL of the proxy to use // for testing purposes. It is used to override the `http.ProxyFromEnvironment` // function for testing purposes. - HTTPSProxyFromEnvironmentForTesting any // func(*http.Request) (*url.URL, error) + HTTPSProxyFromEnvironmentForTesting func(*http.Request) (*url.URL, error) ) // HealthChecker defines the signature of the client-side LB channel health diff --git a/internal/proxyattributes/proxyattributes.go b/internal/proxyattributes/proxyattributes.go index ed5e027b4383..e5e22b287ba4 100644 --- a/internal/proxyattributes/proxyattributes.go +++ b/internal/proxyattributes/proxyattributes.go @@ -28,27 +28,29 @@ import ( type keyType string -const userAndConnectAddrKey = keyType("grpc.resolver.delegatingresolver.userAndConnectAddr") +const proxyOptionsKey = keyType("grpc.resolver.delegatingresolver.proxyOptions") -type userAndConnectAddr struct { - user *url.Userinfo +// Options holds the proxy connection details needed during the CONNECT +// handshake. It includes the user information and the connect address. +type Options struct { + User *url.Userinfo ConnectAddr string } // Populate returns a copy of addr with attributes containing the provided user // and connect address, which are needed during the CONNECT handshake for a // proxy connection. -func Populate(addr resolver.Address, user *url.Userinfo, connectAddr string) resolver.Address { - addr.Attributes = addr.Attributes.WithValue(userAndConnectAddrKey, userAndConnectAddr{user: user, ConnectAddr: connectAddr}) +func Populate(addr resolver.Address, opts Options) resolver.Address { + addr.Attributes = addr.Attributes.WithValue(proxyOptionsKey, opts) return addr } // ConnectAddr returns the proxy connect address in resolver.Address, or nil if // not present. The returned data should not be mutated. func ConnectAddr(addr resolver.Address) string { - a := addr.Attributes.Value(userAndConnectAddrKey) + a := addr.Attributes.Value(proxyOptionsKey) if a != nil { - return a.(userAndConnectAddr).ConnectAddr + return a.(Options).ConnectAddr } return "" } @@ -56,9 +58,9 @@ func ConnectAddr(addr resolver.Address) string { // User returns the user info in the resolver.Address, or nil if not present. // The returned data should not be mutated. func User(addr resolver.Address) *url.Userinfo { - a := addr.Attributes.Value(userAndConnectAddrKey) + a := addr.Attributes.Value(proxyOptionsKey) if a != nil { - return a.(userAndConnectAddr).user + return a.(Options).User } return nil } diff --git a/internal/proxyattributes/proxyattributes_test.go b/internal/proxyattributes/proxyattributes_test.go index eefcd2dc8ab4..e76f3792e39c 100644 --- a/internal/proxyattributes/proxyattributes_test.go +++ b/internal/proxyattributes/proxyattributes_test.go @@ -46,8 +46,8 @@ func (s) TestConnectAddr(t *testing.T) { name: "connect address in attribute", addr: resolver.Address{ Addr: "test-address", - Attributes: attributes.New(userAndConnectAddrKey, userAndConnectAddr{ - user: nil, + Attributes: attributes.New(proxyOptionsKey, Options{ + User: nil, ConnectAddr: "proxy-address", }), }, @@ -70,7 +70,7 @@ func (s) TestConnectAddr(t *testing.T) { } // Tests that User returns the correct user in the attribute. -func TestUser(t *testing.T) { +func (s) TestUser(t *testing.T) { user := url.UserPassword("username", "password") tests := []struct { name string @@ -81,7 +81,8 @@ func TestUser(t *testing.T) { name: "user in attribute", addr: resolver.Address{ Addr: "test-address", - Attributes: attributes.New(userAndConnectAddrKey, userAndConnectAddr{user: user, + Attributes: attributes.New(proxyOptionsKey, Options{ + User: user, ConnectAddr: "proxy-address", })}, want: user, @@ -106,18 +107,20 @@ func TestUser(t *testing.T) { // user and connect address. func (s) TestPopulate(t *testing.T) { addr := resolver.Address{Addr: "test-address"} - user := url.UserPassword("username", "password") - connectAddr := "proxy-address" + pOpts := Options{ + User: url.UserPassword("username", "password"), + ConnectAddr: "proxy-address", + } // Call Populate and validate attributes - populatedAddr := Populate(addr, user, connectAddr) + populatedAddr := Populate(addr, pOpts) // Verify that the returned address is updated correctly - if got, want := ConnectAddr(populatedAddr), connectAddr; got != want { + if got, want := ConnectAddr(populatedAddr), pOpts.ConnectAddr; got != want { t.Errorf("Unexpected ConnectAddr proxy atrribute = %v, want %v", got, want) } - if got, want := User(populatedAddr), user; got != want { + if got, want := User(populatedAddr), pOpts.User; got != want { t.Errorf("unexpected User proxy attribute = %v, want %v", got, want) } } diff --git a/internal/resolver/delegatingresolver/delegatingresolver.go b/internal/resolver/delegatingresolver/delegatingresolver.go index 93e130f78fad..fdb119b97942 100644 --- a/internal/resolver/delegatingresolver/delegatingresolver.go +++ b/internal/resolver/delegatingresolver/delegatingresolver.go @@ -72,7 +72,7 @@ type delegatingResolver struct { func parsedURLForProxy(address string) (*url.URL, error) { proxyFunc := httpProxyFromEnvironmentFunc if pf := internal.HTTPSProxyFromEnvironmentForTesting; pf != nil { - proxyFunc = pf.(func(*http.Request) (*url.URL, error)) + proxyFunc = pf } req := &http.Request{URL: &url.URL{ @@ -196,7 +196,10 @@ func (r *delegatingResolver) generateCombinedAddressesLocked() ([]resolver.Addre for _, proxyAddr := range r.proxyAddrs { for _, targetAddr := range r.targetAddrs { newAddr := resolver.Address{Addr: proxyAddr.Addr} - newAddr = proxyattributes.Populate(newAddr, r.proxyURL.User, targetAddr.Addr) + newAddr = proxyattributes.Populate(newAddr, proxyattributes.Options{ + User: r.proxyURL.User, + ConnectAddr: targetAddr.Addr, + }) addresses = append(addresses, newAddr) } } @@ -211,7 +214,10 @@ func (r *delegatingResolver) generateCombinedAddressesLocked() ([]resolver.Addre var addrs []resolver.Address for _, targetAddr := range endpt.Addresses { newAddr := resolver.Address{Addr: proxyAddr.Addr} - newAddr = proxyattributes.Populate(newAddr, r.proxyURL.User, targetAddr.Addr) + newAddr = proxyattributes.Populate(newAddr, proxyattributes.Options{ + User: r.proxyURL.User, + ConnectAddr: targetAddr.Addr, + }) addrs = append(addrs, newAddr) } endpoints = append(endpoints, resolver.Endpoint{Addresses: addrs}) @@ -240,7 +246,10 @@ type wrappingClientConn struct { resolverType resolverType // represents the type of resolver (target or proxy) } -// UpdateState intercepts state updates from the target and proxy resolvers. +// UpdateState processes updates from the target or proxy resolver. It is called +// twice: once by the target resolver and once by the proxy resolver. It logs +// received addresses, and combines addresses from both resolvers once updates +// from both are received, sending the final state to the parent ClientConn. func (wcc *wrappingClientConn) UpdateState(state resolver.State) error { wcc.parent.mu.Lock() defer wcc.parent.mu.Unlock() diff --git a/internal/resolver/delegatingresolver/delegatingresolver_ext_test.go b/internal/resolver/delegatingresolver/delegatingresolver_ext_test.go index 734cd791d4e4..a450ee36f481 100644 --- a/internal/resolver/delegatingresolver/delegatingresolver_ext_test.go +++ b/internal/resolver/delegatingresolver/delegatingresolver_ext_test.go @@ -135,7 +135,10 @@ func setupDNS(t *testing.T) *manual.Resolver { // adding the target address as an attribute. func proxyAddressWithTargetAttribute(proxyAddr string, targetAddr string) resolver.Address { addr := resolver.Address{Addr: proxyAddr} - addr = proxyattributes.Populate(addr, nil, targetAddr) + addr = proxyattributes.Populate(addr, proxyattributes.Options{ + User: nil, + ConnectAddr: targetAddr, + }) return addr } diff --git a/resolver_wrapper.go b/resolver_wrapper.go index fd49c0204344..c987aff6d6d5 100644 --- a/resolver_wrapper.go +++ b/resolver_wrapper.go @@ -85,10 +85,10 @@ func (ccr *ccResolverWrapper) start() error { // - Client-side resolution is enforced with WithTargetResolutionEnabled. // In these cases, the resolver is built based on the scheme of target, // using the appropriate resolver builder. - if ccr.cc.dopts.copts.Dialer != nil || !ccr.cc.dopts.UseProxy { + if ccr.cc.dopts.copts.Dialer != nil || !ccr.cc.dopts.useProxy { ccr.resolver, err = ccr.cc.resolverBuilder.Build(ccr.cc.parsedTarget, ccr, opts) } else { - ccr.resolver, err = delegatingresolver.New(ccr.cc.parsedTarget, ccr, opts, ccr.cc.resolverBuilder, ccr.cc.dopts.TargetResolutionEnabled) + ccr.resolver, err = delegatingresolver.New(ccr.cc.parsedTarget, ccr, opts, ccr.cc.resolverBuilder, ccr.cc.dopts.targetResolutionEnabled) } errCh <- err }) diff --git a/test/proxy_test.go b/test/proxy_test.go index 106ed6fbf08d..7c567e8d6f21 100644 --- a/test/proxy_test.go +++ b/test/proxy_test.go @@ -216,11 +216,11 @@ func (s) TestGRPCDialWithDNSAndProxy(t *testing.T) { // establishes a connection to the backend server. func (s) TestGRPCNewClientWithProxy(t *testing.T) { // Set up a channel to receive signals from OnClientResolution. - resolutionCh := make(chan bool, 1) + resCh := make(chan bool, 1) // Overwrite OnClientResolution to send a signal to the channel. origOnClientResolution := delegatingresolver.OnClientResolution delegatingresolver.OnClientResolution = func(int) { - resolutionCh <- true + resCh <- true } t.Cleanup(func() { delegatingresolver.OnClientResolution = origOnClientResolution }) @@ -276,7 +276,7 @@ func (s) TestGRPCNewClientWithProxy(t *testing.T) { // Verify if OnClientResolution was triggered. select { - case <-resolutionCh: + case <-resCh: t.Fatal("target resolution occurred on client unexpectedly") default: t.Log("target resolution did not occur on the client") @@ -290,11 +290,11 @@ func (s) TestGRPCNewClientWithProxy(t *testing.T) { // establishes a connection to the backend server. func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { // Set up a channel to receive signals from OnClientResolution. - resolutionCh := make(chan bool, 1) + resCh := make(chan bool, 1) // Overwrite OnClientResolution to send a signal to the channel. origOnClientResolution := delegatingresolver.OnClientResolution delegatingresolver.OnClientResolution = func(int) { - resolutionCh <- true + resCh <- true } t.Cleanup(func() { delegatingresolver.OnClientResolution = origOnClientResolution }) @@ -351,7 +351,7 @@ func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { // Check if client-side resolution signal was sent to the channel. select { - case <-resolutionCh: + case <-resCh: t.Log("target resolution occurred on client") default: t.Fatal("target resolution did not occur on the client unexpectedly") @@ -366,11 +366,11 @@ func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { // successfully established with the backend server through the proxy. func (s) TestGRPCNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { // Set up a channel to receive signals from OnClientResolution. - resolutionCh := make(chan bool, 1) + resCh := make(chan bool, 1) // Overwrite OnClientResolution to send a signal to the channel. origOnClientResolution := delegatingresolver.OnClientResolution delegatingresolver.OnClientResolution = func(int) { - resolutionCh <- true + resCh <- true } t.Cleanup(func() { delegatingresolver.OnClientResolution = origOnClientResolution }) @@ -434,7 +434,7 @@ func (s) TestGRPCNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { // Check if client-side resolution signal was sent to the channel. select { - case <-resolutionCh: + case <-resCh: t.Log("target resolution occurred on client") default: t.Fatal("target resolution did not occur on client unexpectedly") @@ -583,11 +583,11 @@ func (s) TestGRPCNewClientWithContextDialer(t *testing.T) { // happen on the client. func (s) TestBasicAuthInGrpcNewClientWithProxy(t *testing.T) { // Set up a channel to receive signals from OnClientResolution. - resolutionCh := make(chan bool, 1) + resCh := make(chan bool, 1) // Overwrite OnClientResolution to send a signal to the channel. origOnClientResolution := delegatingresolver.OnClientResolution delegatingresolver.OnClientResolution = func(int) { - resolutionCh <- true + resCh <- true } t.Cleanup(func() { delegatingresolver.OnClientResolution = origOnClientResolution }) @@ -665,7 +665,7 @@ func (s) TestBasicAuthInGrpcNewClientWithProxy(t *testing.T) { // Verify if OnClientResolution was triggered. select { - case <-resolutionCh: + case <-resCh: t.Fatal("target resolution occurred on client unexpectedly") default: t.Log("target resolution did not occur on the client") From b4c9980a72e43d8abd487a49207b1b329edc3386 Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Thu, 19 Dec 2024 04:42:16 +0530 Subject: [PATCH 13/53] correct tests --- internal/internal.go | 7 - internal/proxyattributes/proxyattributes.go | 29 +- .../proxyattributes/proxyattributes_test.go | 96 +- .../delegatingresolver/delegatingresolver.go | 268 +++--- .../delegatingresolver_ext_test.go | 843 +++++++++++------- .../delegatingresolver_test.go | 25 +- internal/transport/http2_client.go | 2 +- internal/transport/proxy.go | 18 +- test/proxy_test.go | 230 +---- 9 files changed, 775 insertions(+), 743 deletions(-) diff --git a/internal/internal.go b/internal/internal.go index 134c11ea7869..7fbfaacde9bf 100644 --- a/internal/internal.go +++ b/internal/internal.go @@ -22,8 +22,6 @@ package internal import ( "context" - "net/http" - "net/url" "time" "google.golang.org/grpc/connectivity" @@ -240,11 +238,6 @@ var ( // SetBufferPoolingThresholdForTesting updates the buffer pooling threshold, for // testing purposes. SetBufferPoolingThresholdForTesting any // func(int) - - // HTTPSProxyFromEnvironmentForTesting returns the URL of the proxy to use - // for testing purposes. It is used to override the `http.ProxyFromEnvironment` - // function for testing purposes. - HTTPSProxyFromEnvironmentForTesting func(*http.Request) (*url.URL, error) ) // HealthChecker defines the signature of the client-side LB channel health diff --git a/internal/proxyattributes/proxyattributes.go b/internal/proxyattributes/proxyattributes.go index e5e22b287ba4..2f90ddf8d3c6 100644 --- a/internal/proxyattributes/proxyattributes.go +++ b/internal/proxyattributes/proxyattributes.go @@ -33,34 +33,23 @@ const proxyOptionsKey = keyType("grpc.resolver.delegatingresolver.proxyOptions") // Options holds the proxy connection details needed during the CONNECT // handshake. It includes the user information and the connect address. type Options struct { - User *url.Userinfo + User url.Userinfo ConnectAddr string } -// Populate returns a copy of addr with attributes containing the provided user +// SetOptions returns a copy of addr with attributes containing the provided user // and connect address, which are needed during the CONNECT handshake for a // proxy connection. -func Populate(addr resolver.Address, opts Options) resolver.Address { +func SetOptions(addr resolver.Address, opts Options) resolver.Address { addr.Attributes = addr.Attributes.WithValue(proxyOptionsKey, opts) return addr } -// ConnectAddr returns the proxy connect address in resolver.Address, or nil if -// not present. The returned data should not be mutated. -func ConnectAddr(addr resolver.Address) string { - a := addr.Attributes.Value(proxyOptionsKey) - if a != nil { - return a.(Options).ConnectAddr +// ExtractOptions returns the Options for the proxy [resolver.Address] and a boolean +// value representing if the attribute is present or not. +func ExtractOptions(addr resolver.Address) (Options, bool) { + if a := addr.Attributes.Value(proxyOptionsKey); a != nil { + return a.(Options), true } - return "" -} - -// User returns the user info in the resolver.Address, or nil if not present. -// The returned data should not be mutated. -func User(addr resolver.Address) *url.Userinfo { - a := addr.Attributes.Value(proxyOptionsKey) - if a != nil { - return a.(Options).User - } - return nil + return Options{}, false } diff --git a/internal/proxyattributes/proxyattributes_test.go b/internal/proxyattributes/proxyattributes_test.go index e76f3792e39c..86c00953c13d 100644 --- a/internal/proxyattributes/proxyattributes_test.go +++ b/internal/proxyattributes/proxyattributes_test.go @@ -35,92 +35,82 @@ func Test(t *testing.T) { grpctest.RunSubTests(t, s{}) } -// Tests that ConnectAddr returns the correct connect address in the attribute. -func (s) TestConnectAddr(t *testing.T) { +// Tests that ExtractOptions returns a valid proxy attribute. +func (s) TestExtractOptions(t *testing.T) { + user := url.UserPassword("username", "password") tests := []struct { - name string - addr resolver.Address - want string + name string + addr resolver.Address + wantConnectAddr string + wantUser url.Userinfo + wantAttrPresent bool }{ { - name: "connect address in attribute", + name: "connect_address_in_attribute", addr: resolver.Address{ Addr: "test-address", Attributes: attributes.New(proxyOptionsKey, Options{ - User: nil, ConnectAddr: "proxy-address", }), }, - want: "proxy-address", + wantConnectAddr: "proxy-address", + wantAttrPresent: true, }, { - name: "no attribute", - addr: resolver.Address{Addr: "test-address"}, - want: "", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Validate ConnectAddr returns the correct connect address in the attribute. - if got := ConnectAddr(tt.addr); got != tt.want { - t.Errorf("ConnetAddr(%v) = %v, want %v ", tt.addr, got, tt.want) - } - }) - } -} - -// Tests that User returns the correct user in the attribute. -func (s) TestUser(t *testing.T) { - user := url.UserPassword("username", "password") - tests := []struct { - name string - addr resolver.Address - want *url.Userinfo - }{ - { - name: "user in attribute", + name: "user_in_attribute", addr: resolver.Address{ Addr: "test-address", Attributes: attributes.New(proxyOptionsKey, Options{ - User: user, - ConnectAddr: "proxy-address", - })}, - want: user, + User: *user, + }), + }, + wantUser: *user, + wantAttrPresent: true, }, { - name: "no attribute", - addr: resolver.Address{Addr: "test-address"}, - want: nil, + name: "no_attribute", + addr: resolver.Address{Addr: "test-address"}, + wantAttrPresent: false, }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Validate User returns the correct user in the attribute. - if got := User(tt.addr); got != tt.want { - t.Errorf("User(%v) = %v, want %v ", tt.addr, got, tt.want) + gotOption, attrPresent := ExtractOptions(tt.addr) + if attrPresent != tt.wantAttrPresent { + t.Errorf("ExtractOptions(%v) = %v, want %v", tt.addr, attrPresent, tt.wantAttrPresent) + } + + if gotOption.ConnectAddr != tt.wantConnectAddr { + t.Errorf("ConnectAddr(%v) = %v, want %v", tt.addr, gotOption.ConnectAddr, tt.wantConnectAddr) + } + + if gotOption.User != tt.wantUser { + t.Errorf("User(%v) = %v, want %v", tt.addr, gotOption.User, tt.wantUser) } }) } } -// Tests that Populate returns a copy of addr with attributes containing correct +// Tests that SetOptions returns a copy of addr with attributes containing correct // user and connect address. -func (s) TestPopulate(t *testing.T) { +func (s) TestSetOptions(t *testing.T) { addr := resolver.Address{Addr: "test-address"} pOpts := Options{ - User: url.UserPassword("username", "password"), + User: *url.UserPassword("username", "password"), ConnectAddr: "proxy-address", } - // Call Populate and validate attributes - populatedAddr := Populate(addr, pOpts) - - // Verify that the returned address is updated correctly - if got, want := ConnectAddr(populatedAddr), pOpts.ConnectAddr; got != want { + // Call SetOptions and validate attributes + populatedAddr := SetOptions(addr, pOpts) + gotOption, attrPresent := ExtractOptions(populatedAddr) + if !attrPresent { + t.Errorf("ExtractOptions(%v) = %v, want %v ", populatedAddr, attrPresent, true) + } + if got, want := gotOption.ConnectAddr, pOpts.ConnectAddr; got != want { t.Errorf("Unexpected ConnectAddr proxy atrribute = %v, want %v", got, want) } - - if got, want := User(populatedAddr), pOpts.User; got != want { + if got, want := gotOption.User, pOpts.User; got != want { t.Errorf("unexpected User proxy attribute = %v, want %v", got, want) } } diff --git a/internal/resolver/delegatingresolver/delegatingresolver.go b/internal/resolver/delegatingresolver/delegatingresolver.go index fdb119b97942..2f54578ec94a 100644 --- a/internal/resolver/delegatingresolver/delegatingresolver.go +++ b/internal/resolver/delegatingresolver/delegatingresolver.go @@ -27,23 +27,22 @@ import ( "sync" "google.golang.org/grpc/grpclog" - "google.golang.org/grpc/internal" "google.golang.org/grpc/internal/proxyattributes" "google.golang.org/grpc/resolver" "google.golang.org/grpc/serviceconfig" ) var ( - // HTTPSProxyFromEnvironment will be used and overwritten in the tests. - httpProxyFromEnvironmentFunc = http.ProxyFromEnvironment - // ProxyScheme will be ovwewritten in tests - ProxyScheme = "dns" - logger = grpclog.Component("delegating-resolver") + logger = grpclog.Component("delegating-resolver") + //HTTPSProxyFromEnvironment will be overwritten in the tests + HTTPSProxyFromEnvironment = http.ProxyFromEnvironment + // ProxyScheme will be overwritten in tests + ProxyScheme="dns" ) // delegatingResolver manages both target URI and proxy address resolution by // delegating these tasks to separate child resolvers. Essentially, it acts as -// a middleman between the gRPC ClientConn and the child resolvers. +// a intermediary between the gRPC ClientConn and the child resolvers. // // It implements the [resolver.Resolver] interface. type delegatingResolver struct { @@ -51,17 +50,17 @@ type delegatingResolver struct { cc resolver.ClientConn // gRPC ClientConn 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 + proxyURL *url.URL // proxy URL, derived from proxy environment and target - mu sync.Mutex // protects access to the resolver state and addresses during updates - targetAddrs []resolver.Address // resolved or unresolved target addresses, depending on proxy configuration - targetEndpt []resolver.Endpoint // resolved target endpoint - proxyAddrs []resolver.Address // resolved proxy addresses; empty if no proxy is configured - proxyURL *url.URL // proxy URL, derived from proxy environment and target - targetResolverReady bool // indicates if an update from the target resolver has been received - proxyResolverReady bool // indicates if an update from the proxy resolver has been received + 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 + curState resolver.State // current resolver state + targetResolverReady bool // indicates if an update from the target resolver has been received + proxyResolverReady bool // indicates if an update from the proxy resolver has been received } -// parsedURLForProxy determines the proxy URL for the given address based on +// 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 @@ -69,29 +68,14 @@ type delegatingResolver struct { // - 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 parsedURLForProxy(address string) (*url.URL, error) { - proxyFunc := httpProxyFromEnvironmentFunc - if pf := internal.HTTPSProxyFromEnvironmentForTesting; pf != nil { - proxyFunc = pf - } - +func proxyURLForTarget(address string) (*url.URL, error) { req := &http.Request{URL: &url.URL{ Scheme: "https", Host: address, }} - url, err := proxyFunc(req) - if err != nil { - return nil, err - } - return url, nil + return HTTPSProxyFromEnvironment(req) } -// OnClientResolution is a no-op function in non-test code. In tests, it can -// be overwritten to send a signal to a channel, indicating that client-side -// name resolution was triggered. This enables tests to verify that resolution -// on client side is bypassed when a proxy is in use. -var OnClientResolution = func(int) { /* no-op */ } - // New creates a new delegating resolver that can create up to two child // resolvers: // - one to resolve the proxy address specified using the supported @@ -110,7 +94,7 @@ func New(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOpti } var err error - r.proxyURL, err = parsedURLForProxy(target.Endpoint()) + r.proxyURL, err = proxyURLForTarget(target.Endpoint()) if err != nil { return nil, fmt.Errorf("delegating_resolver: failed to determine proxy URL for target %s: %v", target, err) } @@ -118,7 +102,6 @@ func New(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOpti // proxy is not configured or proxy address excluded using `NO_PROXY` env // var, so only target resolver is used. if r.proxyURL == nil { - OnClientResolution(1) return targetResolverBuilder.Build(target, cc, opts) } @@ -130,13 +113,13 @@ func New(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOpti // resolution should be handled by the proxy, not the client. Therefore, we // bypass the target resolver and store the unresolved target address. if target.URL.Scheme == "dns" && !targetResolutionEnabled { - r.targetAddrs = []resolver.Address{{Addr: target.Endpoint()}} + r.targetResolverState.Addresses = []resolver.Address{{Addr: target.Endpoint()}} + r.targetResolverState.Endpoints = []resolver.Endpoint{{Addresses: []resolver.Address{{Addr: target.Endpoint()}}}} r.targetResolverReady = true } else { - OnClientResolution(1) wcc := &wrappingClientConn{ - parent: r, - resolverType: targetResolverType, + stateListener: r.updateTargetResolverState, + parent: r, } if r.targetResolver, err = targetResolverBuilder.Build(target, wcc, opts); err != nil { return nil, fmt.Errorf("delegating_resolver: unable to build the resolver for target %s: %v", target, err) @@ -155,16 +138,17 @@ func New(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOpti func (r *delegatingResolver) proxyURIResolver(opts resolver.BuildOptions) (resolver.Resolver, error) { proxyBuilder := resolver.Get(ProxyScheme) if proxyBuilder == nil { - panic(fmt.Sprintln("delegating_resolver: resolver for proxy not found for scheme dns")) + panic("delegating_resolver: resolver for proxy not found for scheme dns") } - r.proxyURL.Scheme = "dns" - r.proxyURL.Path = "/" + r.proxyURL.Host - r.proxyURL.Host = "" // Clear the Host field to conform to the "dns:///" format + url := *r.proxyURL + url.Scheme = "dns" + url.Path = "/" + r.proxyURL.Host + url.Host = "" // Clear the Host field to conform to the "dns:///" format - proxyTarget := resolver.Target{URL: *r.proxyURL} + proxyTarget := resolver.Target{URL: url} wcc := &wrappingClientConn{ - parent: r, - resolverType: proxyResolverType, + stateListener: r.updateProxyResolverState, + parent: r, } return proxyBuilder.Build(proxyTarget, wcc, opts) } @@ -181,111 +165,153 @@ func (r *delegatingResolver) ResolveNow(o resolver.ResolveNowOptions) { func (r *delegatingResolver) Close() { if r.targetResolver != nil { r.targetResolver.Close() + r.targetResolver = nil } if r.proxyResolver != nil { r.proxyResolver.Close() + r.proxyResolver = nil } } -// generateCombinedAddressesLocked creates a list of combined addresses by +// combineAndUpdateClientConnStateLocked creates a list of combined addresses by // pairing each proxy address with every target address. For each pair, it // generates a new [resolver.Address] using the proxy address, and adding the -// target address as the attribute along with user info. -func (r *delegatingResolver) generateCombinedAddressesLocked() ([]resolver.Address, []resolver.Endpoint) { - var addresses []resolver.Address - for _, proxyAddr := range r.proxyAddrs { - for _, targetAddr := range r.targetAddrs { - newAddr := resolver.Address{Addr: proxyAddr.Addr} - newAddr = proxyattributes.Populate(newAddr, proxyattributes.Options{ - User: r.proxyURL.User, - ConnectAddr: targetAddr.Addr, - }) - addresses = append(addresses, newAddr) - } +// target address as the attribute along with user info. It also returns a +// boolen representing if updates from both the resolvers have been received. +func (r *delegatingResolver) combineAndUpdateClientConnStateLocked() error { + if !r.targetResolverReady || !r.proxyResolverReady { + return nil } - if r.targetEndpt == nil { - return addresses, 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 proxyAddr resolver.Address + if len(r.proxyAddrs) == 1 { + proxyAddr = r.proxyAddrs[0] + } else { + proxyAddr = resolver.Address{Addr: r.proxyURL.Host} + } + var addresses []resolver.Address + var user url.Userinfo + if r.proxyURL.User != nil { + user = *r.proxyURL.User + } + for _, targetAddr := range r.targetResolverState.Addresses { + addresses = append(addresses, proxyattributes.SetOptions(proxyAddr, proxyattributes.Options{ + User: user, + ConnectAddr: targetAddr.Addr, + })) } + // Create a list of combined endpoints by pairing all proxy endpoints + // with every target endpoint. Each time, it constructs a new + // [resolver.Endpoint] using the all address from all the proxy endpoint + // and the target addresses from one endpoint. The target address and user + // information from the proxy URL are added as attributes to the proxy + // address.The resulting list of addresses is then grouped into endpoints, + // covering all combinations of proxy and target endpoints. var endpoints []resolver.Endpoint - for _, proxyAddr := range r.proxyAddrs { - for _, endpt := range r.targetEndpt { - var addrs []resolver.Address + for _, endpt := range r.targetResolverState.Endpoints { + var addrs []resolver.Address + var user url.Userinfo + if r.proxyURL.User != nil { + user = *r.proxyURL.User + } + for _, proxyAddr := range r.proxyAddrs { for _, targetAddr := range endpt.Addresses { - newAddr := resolver.Address{Addr: proxyAddr.Addr} - newAddr = proxyattributes.Populate(newAddr, proxyattributes.Options{ - User: r.proxyURL.User, + addrs = append(addrs, proxyattributes.SetOptions(proxyAddr, proxyattributes.Options{ + User: user, ConnectAddr: targetAddr.Addr, - }) - addrs = append(addrs, newAddr) + })) } - endpoints = append(endpoints, resolver.Endpoint{Addresses: addrs}) } + endpoints = append(endpoints, resolver.Endpoint{Addresses: addrs}) } - return addresses, endpoints + r.curState.Addresses = addresses + r.curState.Endpoints = endpoints + return r.cc.UpdateState(r.curState) } -// resolverType is an enum representing the type of resolver (target or proxy). -type resolverType int +// 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 (r *delegatingResolver) updateProxyResolverState(state resolver.State) error { + r.mu.Lock() + defer r.mu.Unlock() + if logger.V(2) { + logger.Infof("Addresses received from proxy resolver: %s", state.Addresses) + } + if state.Endpoints != nil { + r.proxyAddrs = []resolver.Address{} + for _, endpoint := range state.Endpoints { + r.proxyAddrs = append(r.proxyAddrs, endpoint.Addresses...) + } + } else { + r.proxyAddrs = state.Addresses + } + r.proxyResolverReady = true + err := r.combineAndUpdateClientConnStateLocked() + // 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 err != nil { + r.targetResolver.ResolveNow(resolver.ResolveNowOptions{}) + } + return err +} -const ( - targetResolverType resolverType = iota - proxyResolverType -) +// updateTargetResolverState updates the target resolver state by storing target +// addresses, endpoints, and service config, marking the resolver as ready, and +// triggering a state update if both resolvers are ready. If the ClientConn +// returns a non-nil error, it calls `ResolveNow()` on the proxy resolver. It +// is a StateListener function of wrappingClientConn passed to the target resolver. +func (r *delegatingResolver) updateTargetResolverState(state resolver.State) error { + r.mu.Lock() + defer r.mu.Unlock() + + if logger.V(2) { + logger.Infof("Addresses received from target resolver: %v", state.Addresses) + } + r.targetResolverState = state + r.targetResolverReady = true + // Update curState to include other state information, such as the + // service config, provided by the target resolver. This ensures + // curState contains all necessary information when passed to + // UpdateState. 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. + r.curState = state + err := r.combineAndUpdateClientConnStateLocked() + if err != nil { + r.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. -// -// Its primary function is to aggregate addresses returned by the child -// resolvers before passing them to the parent ClientConn. Any errors returned -// by the child resolvers are propagated verbatim to the parent ClientConn. type wrappingClientConn struct { - parent *delegatingResolver - resolverType resolverType // represents the type of resolver (target or proxy) + // Callback to deliver resolver state updates + stateListener func(state resolver.State) error + parent *delegatingResolver } -// UpdateState processes updates from the target or proxy resolver. It is called -// twice: once by the target resolver and once by the proxy resolver. It logs -// received addresses, and combines addresses from both resolvers once updates -// from both are received, sending the final state to the parent ClientConn. +// UpdateState receives resolver state updates and forwards them to the +// appropriate listener function (either for the proxy or target resolver). func (wcc *wrappingClientConn) UpdateState(state resolver.State) error { - wcc.parent.mu.Lock() - defer wcc.parent.mu.Unlock() - - var curState resolver.State - if wcc.resolverType == targetResolverType { - if logger.V(2) { - logger.Infof("Addresses received from target resolver: %v", state.Addresses) - } - wcc.parent.targetAddrs = state.Addresses - wcc.parent.targetEndpt = state.Endpoints - wcc.parent.targetResolverReady = true - // Update curState to include other state information, such as the - // service config, provided by the target resolver. This ensures - // curState contains all necessary information when passed to - // UpdateState. 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. - curState = state - } - // The proxy resolver is always "dns," so only the address will be - // received, not an endpoint. - if wcc.resolverType == proxyResolverType { - if logger.V(2) { - logger.Infof("Addresses received from proxy resolver: %s", state.Addresses) - } - wcc.parent.proxyAddrs = state.Addresses - wcc.parent.proxyResolverReady = true - } - - // Proceed only if updates from both resolvers have been received. - if !wcc.parent.targetResolverReady || !wcc.parent.proxyResolverReady { - return nil - } - curState.Addresses, curState.Endpoints = wcc.parent.generateCombinedAddressesLocked() - return wcc.parent.cc.UpdateState(curState) + return wcc.stateListener(state) } // ReportError intercepts errors from the child resolvers and passes them to ClientConn. diff --git a/internal/resolver/delegatingresolver/delegatingresolver_ext_test.go b/internal/resolver/delegatingresolver/delegatingresolver_ext_test.go index a450ee36f481..9a2c7c1f6325 100644 --- a/internal/resolver/delegatingresolver/delegatingresolver_ext_test.go +++ b/internal/resolver/delegatingresolver/delegatingresolver_ext_test.go @@ -16,325 +16,524 @@ * */ -package delegatingresolver_test - -import ( - "net/http" - "net/url" - "testing" - "time" - - "github.com/google/go-cmp/cmp" - "google.golang.org/grpc/internal" - "google.golang.org/grpc/internal/grpctest" - "google.golang.org/grpc/internal/pretty" - "google.golang.org/grpc/internal/proxyattributes" - "google.golang.org/grpc/internal/resolver/delegatingresolver" - "google.golang.org/grpc/internal/testutils" - "google.golang.org/grpc/resolver" - _ "google.golang.org/grpc/resolver/dns" // To register dns resolver. - "google.golang.org/grpc/resolver/manual" - "google.golang.org/grpc/serviceconfig" -) - -type s struct { - grpctest.Tester -} - -func Test(t *testing.T) { - grpctest.RunSubTests(t, s{}) -} - -const ( - targetTestAddr = "test.com" - resolvedTargetTestAddr1 = "1.2.3.4:8080" - resolvedTargetTestAddr2 = "1.2.3.5:8080" - envProxyAddr = "proxytest.com" - resolvedProxyTestAddr1 = "2.3.4.5:7687" - resolvedProxyTestAddr2 = "2.3.4.6:7687" - defaultTestTimeout = 10 * time.Second - defaultTestShortTimeout = 10 * time.Millisecond -) - -// createTestResolverClientConn initializes a [testutils.ResolverClientConn] and -// returns it along with channels for resolver state updates and errors. -func createTestResolverClientConn(t *testing.T) (*testutils.ResolverClientConn, chan resolver.State, chan error) { - stateCh := make(chan resolver.State, 1) - errCh := make(chan error, 1) - - tcc := &testutils.ResolverClientConn{ - Logger: t, - UpdateStateF: func(s resolver.State) error { stateCh <- s; return nil }, - ReportErrorF: func(err error) { errCh <- err }, - } - return tcc, stateCh, errCh -} - -// Tests the scenario where no proxy environment variables are set or proxying -// is disabled by the `NO_PROXY` environment variable. The test verifies that -// the delegating resolver creates only a target resolver and that the -// addresses returned by the delegating resolver exactly match those returned -// by the target resolver. -func (s) TestDelegatingResolverNoProxyEnvVarsSet(t *testing.T) { - hpfe := func(req *http.Request) (*url.URL, error) { return nil, nil } - originalhpfe := internal.HTTPSProxyFromEnvironmentForTesting - internal.HTTPSProxyFromEnvironmentForTesting = hpfe - defer func() { - internal.HTTPSProxyFromEnvironmentForTesting = originalhpfe - }() - - mr := manual.NewBuilderWithScheme("test") // Set up a manual resolver to control the address resolution. - target := mr.Scheme() + ":///" + targetTestAddr - - // Create a delegating resolver with no proxy configuration - tcc, stateCh, _ := createTestResolverClientConn(t) - if _, err := delegatingresolver.New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, mr, false); err != nil { - t.Fatalf("Failed to create delegating resolver: %v", err) - } - - // Update the manual resolver with a test address. - mr.UpdateState(resolver.State{ - Addresses: []resolver.Address{{Addr: resolvedTargetTestAddr1}}, - ServiceConfig: &serviceconfig.ParseResult{}, - }) - - // Verify that the delegating resolver outputs the same addresses, as returned - // by the target resolver. - wantState := resolver.State{ - Addresses: []resolver.Address{{Addr: resolvedTargetTestAddr1}}, - ServiceConfig: &serviceconfig.ParseResult{}, - } - - var gotState resolver.State - select { - case gotState = <-stateCh: - case <-time.After(defaultTestTimeout): - t.Fatal("Timeout when waiting for a state update from the delegating resolver") - } - - if !cmp.Equal(gotState, wantState) { - t.Fatalf("Unexpected state from delegating resolver: %s, want %s", pretty.ToJSON(gotState), pretty.ToJSON(wantState)) - } -} - -// setupDNS registers a new manual resolver for the DNS scheme, effectively -// overwriting the previously registered DNS resolver. This allows the test to -// mock the DNS resolution for the proxy resolver. It also registers the -// original DNS resolver after the test is done. -func setupDNS(t *testing.T) *manual.Resolver { - mr := manual.NewBuilderWithScheme("dns") - - dnsResolverBuilder := resolver.Get("dns") - resolver.Register(mr) - - t.Cleanup(func() { resolver.Register(dnsResolverBuilder) }) - return mr -} - -// proxyAddressWithTargetAttribute creates a resolver.Address for the proxy, -// adding the target address as an attribute. -func proxyAddressWithTargetAttribute(proxyAddr string, targetAddr string) resolver.Address { - addr := resolver.Address{Addr: proxyAddr} - addr = proxyattributes.Populate(addr, proxyattributes.Options{ - User: nil, - ConnectAddr: targetAddr, - }) - return addr -} - -// Tests the scenario where proxy is configured and the target URI contains the -// "dns" scheme and target resolution is enabled. The test verifies that the -// addresses returned by the delegating resolver combines the addresses -// returned by the proxy resolver and the target resolver. -func (s) TestDelegatingResolverwithDNSAndProxyWithTargetResolution(t *testing.T) { - hpfe := func(req *http.Request) (*url.URL, error) { - if req.URL.Host == targetTestAddr { - return &url.URL{ - Scheme: "https", - Host: envProxyAddr, - }, nil - } - return nil, nil - } - originalhpfe := internal.HTTPSProxyFromEnvironmentForTesting - internal.HTTPSProxyFromEnvironmentForTesting = hpfe - defer func() { - internal.HTTPSProxyFromEnvironmentForTesting = originalhpfe - }() - - mrTarget := setupDNS(t) // Manual resolver to control the target resolution. - target := mrTarget.Scheme() + ":///" + targetTestAddr - mrProxy := setupDNS(t) // Set up a manual DNS resolver to control the proxy address resolution. - - tcc, stateCh, _ := createTestResolverClientConn(t) - if _, err := delegatingresolver.New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, mrTarget, true); err != nil { - t.Fatalf("Failed to create delegating resolver: %v", err) - } - - mrProxy.UpdateState(resolver.State{ - Addresses: []resolver.Address{ - {Addr: resolvedProxyTestAddr1}, - {Addr: resolvedProxyTestAddr2}, - }, - ServiceConfig: &serviceconfig.ParseResult{}, - }) - - select { - case <-stateCh: - t.Fatalf("Delegating resolver invoked UpdateState before both the proxy and target resolvers had updated their states.") - case <-time.After(defaultTestShortTimeout): - } - - mrTarget.UpdateState(resolver.State{ - Addresses: []resolver.Address{ - {Addr: resolvedTargetTestAddr1}, - {Addr: resolvedTargetTestAddr2}, - }, - ServiceConfig: &serviceconfig.ParseResult{}, - }) - - // Verify that the delegating resolver outputs the expected address. - wantState := resolver.State{Addresses: []resolver.Address{ - proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, resolvedTargetTestAddr1), - proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, resolvedTargetTestAddr2), - proxyAddressWithTargetAttribute(resolvedProxyTestAddr2, resolvedTargetTestAddr1), - proxyAddressWithTargetAttribute(resolvedProxyTestAddr2, resolvedTargetTestAddr2), - }, - ServiceConfig: &serviceconfig.ParseResult{}, - } - var gotState resolver.State - select { - case gotState = <-stateCh: - case <-time.After(defaultTestTimeout): - t.Fatal("Timeout when waiting for a state update from the delegating resolver") - } - - if !cmp.Equal(gotState, wantState) { - t.Fatalf("Unexpected state from delegating resolver: %s, want %s", pretty.ToJSON(gotState), pretty.ToJSON(wantState)) - } -} - -// Tests the scenario where a proxy is configured, the target URI contains the -// "dns" scheme, and target resolution is disabled(default behavior). The test -// verifies that the addresses returned by the delegating resolver include the -// proxy resolver's addresses, with the unresolved target URI as an attribute -// of the proxy address. -func (s) TestDelegatingResolverwithDNSAndProxyWithNoTargetResolution(t *testing.T) { - hpfe := func(req *http.Request) (*url.URL, error) { - if req.URL.Host == targetTestAddr { - return &url.URL{ - Scheme: "https", - Host: envProxyAddr, - }, nil - } - return nil, nil - } - originalhpfe := internal.HTTPSProxyFromEnvironmentForTesting - internal.HTTPSProxyFromEnvironmentForTesting = hpfe - defer func() { - internal.HTTPSProxyFromEnvironmentForTesting = originalhpfe - }() - - mrTarget := manual.NewBuilderWithScheme("test") // Manual resolver to control the target resolution. - target := "dns:///" + targetTestAddr - mrProxy := setupDNS(t) // Set up a manual DNS resolver to control the proxy address resolution. - - tcc, stateCh, _ := createTestResolverClientConn(t) - if _, err := delegatingresolver.New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, mrTarget, false); err != nil { - t.Fatalf("Failed to create delegating resolver: %v", err) - } - - mrProxy.UpdateState(resolver.State{ - Addresses: []resolver.Address{ - {Addr: resolvedProxyTestAddr1}, - {Addr: resolvedProxyTestAddr2}, - }, - }) - - wantState := resolver.State{Addresses: []resolver.Address{ - proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, targetTestAddr), - proxyAddressWithTargetAttribute(resolvedProxyTestAddr2, targetTestAddr), - }} - - var gotState resolver.State - select { - case gotState = <-stateCh: - case <-time.After(defaultTestTimeout): - t.Fatal("Timeout when waiting for a state update from the delegating resolver") - } - - if !cmp.Equal(gotState, wantState) { - t.Fatalf("Unexpected state from delegating resolver: %s, want %s", pretty.ToJSON(gotState), pretty.ToJSON(wantState)) - } -} - -// Tests the scenario where a proxy is configured, and the target URI scheme is -// not "dns". The test verifies that the addresses returned by the delegating -// resolver include the resolved proxy address and the custom resolved target -// address as attributes of the proxy address. -func (s) TestDelegatingResolverwithCustomResolverAndProxy(t *testing.T) { - hpfe := func(req *http.Request) (*url.URL, error) { - if req.URL.Host == targetTestAddr { - return &url.URL{ - Scheme: "https", - Host: envProxyAddr, - }, nil - } - return nil, nil - } - originalhpfe := internal.HTTPSProxyFromEnvironmentForTesting - internal.HTTPSProxyFromEnvironmentForTesting = hpfe - defer func() { - internal.HTTPSProxyFromEnvironmentForTesting = originalhpfe - }() - - mrTarget := manual.NewBuilderWithScheme("test") // Manual resolver to control the target resolution. - target := mrTarget.Scheme() + ":///" + targetTestAddr - mrProxy := setupDNS(t) // Set up a manual DNS resolver to control the proxy address resolution. - - tcc, stateCh, _ := createTestResolverClientConn(t) - if _, err := delegatingresolver.New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, mrTarget, false); err != nil { - t.Fatalf("Failed to create delegating resolver: %v", err) - } - - mrProxy.UpdateState(resolver.State{ - Addresses: []resolver.Address{ - {Addr: resolvedProxyTestAddr1}, - {Addr: resolvedProxyTestAddr2}, - }, - ServiceConfig: &serviceconfig.ParseResult{}, - }) - - select { - case <-stateCh: - t.Fatalf("Delegating resolver invoked UpdateState before both the proxy and target resolvers had updated their states.") - case <-time.After(defaultTestShortTimeout): - } - - mrTarget.UpdateState(resolver.State{ - Addresses: []resolver.Address{ - {Addr: resolvedTargetTestAddr1}, - {Addr: resolvedTargetTestAddr2}, - }, - ServiceConfig: &serviceconfig.ParseResult{}, - }) - - wantState := resolver.State{Addresses: []resolver.Address{ - proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, resolvedTargetTestAddr1), - proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, resolvedTargetTestAddr2), - proxyAddressWithTargetAttribute(resolvedProxyTestAddr2, resolvedTargetTestAddr1), - proxyAddressWithTargetAttribute(resolvedProxyTestAddr2, resolvedTargetTestAddr2), - }, - ServiceConfig: &serviceconfig.ParseResult{}, - } - var gotState resolver.State - select { - case gotState = <-stateCh: - case <-time.After(defaultTestTimeout): - t.Fatal("Timeout when waiting for a state update from the delegating resolver") - } - - if !cmp.Equal(gotState, wantState) { - t.Fatalf("Unexpected state from delegating resolver: %s, want %s", pretty.ToJSON(gotState), pretty.ToJSON(wantState)) - } -} + package delegatingresolver_test + + import ( + "net/http" + "net/url" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "google.golang.org/grpc/internal/grpctest" + "google.golang.org/grpc/internal/proxyattributes" + "google.golang.org/grpc/internal/resolver/delegatingresolver" + "google.golang.org/grpc/internal/testutils" + "google.golang.org/grpc/resolver" + "google.golang.org/grpc/resolver/manual" + "google.golang.org/grpc/serviceconfig" + + _ "google.golang.org/grpc/resolver/dns" // To register dns resolver. + ) + + type s struct { + grpctest.Tester + } + + func Test(t *testing.T) { + grpctest.RunSubTests(t, s{}) + } + + const ( + defaultTestTimeout = 10 * time.Second + defaultTestShortTimeout = 10 * time.Millisecond + ) + + // createTestResolverClientConn initializes a [testutils.ResolverClientConn] and + // returns it along with channels for resolver state updates and errors. + func createTestResolverClientConn(t *testing.T) (*testutils.ResolverClientConn, chan resolver.State, chan error) { + stateCh := make(chan resolver.State, 1) + errCh := make(chan error, 1) + + tcc := &testutils.ResolverClientConn{ + Logger: t, + UpdateStateF: func(s resolver.State) error { stateCh <- s; return nil }, + ReportErrorF: func(err error) { errCh <- err }, + } + return tcc, stateCh, errCh + } + + // Tests the scenario where no proxy environment variables are set or proxying + // is disabled by the `NO_PROXY` environment variable. The test verifies that + // the delegating resolver creates only a target resolver and that the + // addresses returned by the delegating resolver exactly match those returned + // by the target resolver. + func (s) TestDelegatingResolverNoProxyEnvVarsSet(t *testing.T) { + hpfe := func(req *http.Request) (*url.URL, error) { return nil, nil } + originalhpfe := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = hpfe + defer func() { + delegatingresolver.HTTPSProxyFromEnvironment = originalhpfe + }() + + const ( + targetTestAddr = "test.com" + resolvedTargetTestAddr1 = "1.1.1.1:8080" + resolvedTargetTestAddr2 = "2.2.2.2:8080" + ) + targetResolver := manual.NewBuilderWithScheme("test") // Set up a manual resolver to control the address resolution. + target := targetResolver.Scheme() + ":///" + targetTestAddr + + // Create a delegating resolver with no proxy configuration + tcc, stateCh, _ := createTestResolverClientConn(t) + if _, err := delegatingresolver.New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, targetResolver, false); err != nil { + t.Fatalf("Failed to create delegating resolver: %v", err) + } + + // Update the manual resolver with a test address. + targetResolver.UpdateState(resolver.State{ + Addresses: []resolver.Address{ + {Addr: resolvedTargetTestAddr1}, + {Addr: resolvedTargetTestAddr2}, + }, + ServiceConfig: &serviceconfig.ParseResult{}, + }) + + // Verify that the delegating resolver outputs the same addresses, as returned + // by the target resolver. + wantState := resolver.State{ + Addresses: []resolver.Address{ + {Addr: resolvedTargetTestAddr1}, + {Addr: resolvedTargetTestAddr2}, + }, + ServiceConfig: &serviceconfig.ParseResult{}, + } + + var gotState resolver.State + select { + case gotState = <-stateCh: + case <-time.After(defaultTestTimeout): + t.Fatal("Timeout when waiting for a state update from the delegating resolver") + } + + if diff := cmp.Diff(gotState, wantState); diff != "" { + t.Fatalf("Unexpected state from delegating resolver. Diff (-got +want):\n%v", diff) + } + } + + // setupDNS registers a new manual resolver for the DNS scheme, effectively + // overwriting the previously registered DNS resolver. This allows the test to + // mock the DNS resolution for the proxy resolver. It also registers the + // original DNS resolver after the test is done. + func setupDNS(t *testing.T) *manual.Resolver { + mr := manual.NewBuilderWithScheme("dns") + + dnsResolverBuilder := resolver.Get("dns") + resolver.Register(mr) + + t.Cleanup(func() { resolver.Register(dnsResolverBuilder) }) + return mr + } + + // proxyAddressWithTargetAttribute creates a resolver.Address for the proxy, + // adding the target address as an attribute. + func proxyAddressWithTargetAttribute(proxyAddr string, targetAddr string) resolver.Address { + addr := resolver.Address{Addr: proxyAddr} + addr = proxyattributes.SetOptions(addr, proxyattributes.Options{ConnectAddr: targetAddr}) + return addr + } + + // Tests the scenario where proxy is configured and the target URI contains the + // "dns" scheme and target resolution is enabled. The test verifies that the + // addresses returned by the delegating resolver combines the addresses + // returned by the proxy resolver and the target resolver. + func (s) TestDelegatingResolverwithDNSAndProxyWithTargetResolution(t *testing.T) { + const ( + targetTestAddr = "test.com" + resolvedTargetTestAddr1 = "1.1.1.1:8080" + resolvedTargetTestAddr2 = "2.2.2.2:8080" + envProxyAddr = "proxytest.com" + resolvedProxyTestAddr1 = "11.11.11.11:7687" + ) + hpfe := func(req *http.Request) (*url.URL, error) { + if req.URL.Host == targetTestAddr { + return &url.URL{ + Scheme: "https", + Host: envProxyAddr, + }, nil + } + t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, targetTestAddr) + return nil, nil + } + originalhpfe := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = hpfe + defer func() { + delegatingresolver.HTTPSProxyFromEnvironment = originalhpfe + }() + + targetResolver := manual.NewBuilderWithScheme("dns") // Manual resolver to control the target resolution. + target := targetResolver.Scheme() + ":///" + targetTestAddr + proxyResolver := setupDNS(t) // Set up a manual DNS resolver to control the proxy address resolution. + + tcc, stateCh, _ := createTestResolverClientConn(t) + if _, err := delegatingresolver.New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, targetResolver, true); err != nil { + t.Fatalf("Failed to create delegating resolver: %v", err) + } + + proxyResolver.UpdateState(resolver.State{ + Addresses: []resolver.Address{{Addr: resolvedProxyTestAddr1}}, + ServiceConfig: &serviceconfig.ParseResult{}, + }) + + select { + case <-stateCh: + t.Fatalf("Delegating resolver invoked UpdateState before both the proxy and target resolvers had updated their states.") + case <-time.After(defaultTestShortTimeout): + } + + targetResolver.UpdateState(resolver.State{ + Addresses: []resolver.Address{ + {Addr: resolvedTargetTestAddr1}, + {Addr: resolvedTargetTestAddr2}, + }, + ServiceConfig: &serviceconfig.ParseResult{}, + }) + + // Verify that the delegating resolver outputs the expected address. + wantState := resolver.State{ + Addresses: []resolver.Address{ + proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, resolvedTargetTestAddr1), + proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, resolvedTargetTestAddr2), + }, + ServiceConfig: &serviceconfig.ParseResult{}, + } + var gotState resolver.State + select { + case gotState = <-stateCh: + case <-time.After(defaultTestTimeout): + t.Fatal("Timeout when waiting for a state update from the delegating resolver") + } + + if diff := cmp.Diff(gotState, wantState); diff != "" { + t.Fatalf("Unexpected state from delegating resolver. Diff (-got +want):\n%v", diff) + } + } + + // Tests the scenario where a proxy is configured, the target URI contains the + // "dns" scheme, and target resolution is disabled(default behavior). The test + // verifies that the addresses returned by the delegating resolver include the + // proxy resolver's addresses, with the unresolved target URI as an attribute + // of the proxy address. + func (s) TestDelegatingResolverwithDNSAndProxyWithNoTargetResolution(t *testing.T) { + const ( + targetTestAddr = "test.com" + envProxyAddr = "proxytest.com" + resolvedProxyTestAddr1 = "11.11.11.11:7687" + ) + hpfe := func(req *http.Request) (*url.URL, error) { + if req.URL.Host == targetTestAddr { + return &url.URL{ + Scheme: "https", + Host: envProxyAddr, + }, nil + } + t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, targetTestAddr) + return nil, nil + } + originalhpfe := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = hpfe + defer func() { + delegatingresolver.HTTPSProxyFromEnvironment = originalhpfe + }() + + targetResolver := manual.NewBuilderWithScheme("dns") + target := targetResolver.Scheme() + ":///" + targetTestAddr + proxyResolver := setupDNS(t) // Set up a manual DNS resolver to control the proxy address resolution. + + tcc, stateCh, _ := createTestResolverClientConn(t) + if _, err := delegatingresolver.New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, targetResolver, false); err != nil { + t.Fatalf("Failed to create delegating resolver: %v", err) + } + + proxyResolver.UpdateState(resolver.State{ + Addresses: []resolver.Address{ + {Addr: resolvedProxyTestAddr1}, + }, + }) + + wantState := resolver.State{ + Addresses: []resolver.Address{proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, targetTestAddr)}, + Endpoints: []resolver.Endpoint{{Addresses: []resolver.Address{proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, targetTestAddr)}}}, + } + + var gotState resolver.State + select { + case gotState = <-stateCh: + case <-time.After(defaultTestTimeout): + t.Fatal("Timeout when waiting for a state update from the delegating resolver") + } + + if diff := cmp.Diff(gotState, wantState); diff != "" { + t.Fatalf("Unexpected state from delegating resolver. Diff (-got +want):\n%v", diff) + } + } + + // Tests the scenario where a proxy is configured, and the target URI scheme is + // not "dns". The test verifies that the addresses returned by the delegating + // resolver include the resolved proxy address and the custom resolved target + // address as attributes of the proxy address. + func (s) TestDelegatingResolverwithCustomResolverAndProxy(t *testing.T) { + const ( + targetTestAddr = "test.com" + resolvedTargetTestAddr1 = "1.1.1.1:8080" + resolvedTargetTestAddr2 = "2.2.2.2:8080" + envProxyAddr = "proxytest.com" + resolvedProxyTestAddr1 = "11.11.11.11:7687" + ) + hpfe := func(req *http.Request) (*url.URL, error) { + if req.URL.Host == targetTestAddr { + return &url.URL{ + Scheme: "https", + Host: envProxyAddr, + }, nil + } + t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, targetTestAddr) + return nil, nil + } + originalhpfe := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = hpfe + defer func() { + delegatingresolver.HTTPSProxyFromEnvironment = originalhpfe + }() + + targetResolver := manual.NewBuilderWithScheme("test") // Manual resolver to control the target resolution. + target := targetResolver.Scheme() + ":///" + targetTestAddr + proxyResolver := setupDNS(t) // Set up a manual DNS resolver to control the proxy address resolution. + + tcc, stateCh, _ := createTestResolverClientConn(t) + if _, err := delegatingresolver.New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, targetResolver, false); err != nil { + t.Fatalf("Failed to create delegating resolver: %v", err) + } + + proxyResolver.UpdateState(resolver.State{ + Addresses: []resolver.Address{{Addr: resolvedProxyTestAddr1}}, + ServiceConfig: &serviceconfig.ParseResult{}, + }) + + select { + case <-stateCh: + t.Fatalf("Delegating resolver invoked UpdateState before both the proxy and target resolvers had updated their states.") + case <-time.After(defaultTestShortTimeout): + } + + targetResolver.UpdateState(resolver.State{ + Addresses: []resolver.Address{ + {Addr: resolvedTargetTestAddr1}, + {Addr: resolvedTargetTestAddr2}, + }, + ServiceConfig: &serviceconfig.ParseResult{}, + }) + + wantState := resolver.State{ + Addresses: []resolver.Address{ + proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, resolvedTargetTestAddr1), + proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, resolvedTargetTestAddr2), + }, + ServiceConfig: &serviceconfig.ParseResult{}, + } + var gotState resolver.State + select { + case gotState = <-stateCh: + case <-time.After(defaultTestTimeout): + t.Fatal("Timeout when waiting for a state update from the delegating resolver") + } + + if diff := cmp.Diff(gotState, wantState); diff != "" { + t.Fatalf("Unexpected state from delegating resolver. Diff (-got +want):\n%v", diff) + } + } + + // Tests the scenario where a proxy is configured, the target URI scheme is not + // "dns," and both the proxy and target resolvers return endpoints. The test + // verifies that the delegating resolver combines resolved proxy and target + // addresses correctly, returning endpoints with the proxy address populated + // and the target address included as an attribute of the proxy address for + // each combination of proxy and target endpoints. + func (s) TestDelegatingResolverForEndpointsWithProxy(t *testing.T) { + const ( + targetTestAddr = "test.com" + resolvedTargetTestAddr1 = "1.1.1.1:8080" + resolvedTargetTestAddr2 = "2.2.2.2:8080" + resolvedTargetTestAddr3 = "3.3.3.3:8080" + resolvedTargetTestAddr4 = "4.4.4.4:8080" + envProxyAddr = "proxytest.com" + resolvedProxyTestAddr1 = "11.11.11.11:7687" + resolvedProxyTestAddr2 = "22.22.22.22:7687" + ) + hpfe := func(req *http.Request) (*url.URL, error) { + if req.URL.Host == targetTestAddr { + return &url.URL{ + Scheme: "https", + Host: envProxyAddr, + }, nil + } + t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, targetTestAddr) + return nil, nil + } + originalhpfe := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = hpfe + defer func() { + delegatingresolver.HTTPSProxyFromEnvironment = originalhpfe + }() + + targetResolver := manual.NewBuilderWithScheme("test") // Manual resolver to control the target resolution. + target := targetResolver.Scheme() + ":///" + targetTestAddr + proxyResolver := setupDNS(t) // Set up a manual DNS resolver to control the proxy address resolution. + + tcc, stateCh, _ := createTestResolverClientConn(t) + if _, err := delegatingresolver.New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, targetResolver, false); err != nil { + t.Fatalf("Failed to create delegating resolver: %v", err) + } + + proxyResolver.UpdateState(resolver.State{ + Endpoints: []resolver.Endpoint{ + {Addresses: []resolver.Address{{Addr: resolvedProxyTestAddr1}}}, + {Addresses: []resolver.Address{{Addr: resolvedProxyTestAddr2}}}, + }, + ServiceConfig: &serviceconfig.ParseResult{}, + }) + + select { + case <-stateCh: + t.Fatalf("Delegating resolver invoked UpdateState before both the proxy and target resolvers had updated their states.") + case <-time.After(defaultTestShortTimeout): + } + targetResolver.UpdateState(resolver.State{ + Endpoints: []resolver.Endpoint{ + { + Addresses: []resolver.Address{ + {Addr: resolvedTargetTestAddr1}, + {Addr: resolvedTargetTestAddr2}}, + }, + { + Addresses: []resolver.Address{ + {Addr: resolvedTargetTestAddr3}, + {Addr: resolvedTargetTestAddr4}}, + }, + }, + ServiceConfig: &serviceconfig.ParseResult{}, + }) + + wantState := resolver.State{ + Endpoints: []resolver.Endpoint{ + { + Addresses: []resolver.Address{ + proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, resolvedTargetTestAddr1), + proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, resolvedTargetTestAddr2), + proxyAddressWithTargetAttribute(resolvedProxyTestAddr2, resolvedTargetTestAddr1), + proxyAddressWithTargetAttribute(resolvedProxyTestAddr2, resolvedTargetTestAddr2), + }, + }, + { + Addresses: []resolver.Address{ + proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, resolvedTargetTestAddr3), + proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, resolvedTargetTestAddr4), + proxyAddressWithTargetAttribute(resolvedProxyTestAddr2, resolvedTargetTestAddr3), + proxyAddressWithTargetAttribute(resolvedProxyTestAddr2, resolvedTargetTestAddr4), + }, + }, + }, + ServiceConfig: &serviceconfig.ParseResult{}, + } + var gotState resolver.State + select { + case gotState = <-stateCh: + case <-time.After(defaultTestTimeout): + t.Fatal("Timeout when waiting for a state update from the delegating resolver") + } + + if diff := cmp.Diff(gotState, wantState); diff != "" { + t.Fatalf("Unexpected state from delegating resolver. Diff (-got +want):\n%v", diff) + } + } + + // Tests the scenario where a proxy is configured, the target URI scheme is not + // "dns," and both the proxy and target resolvers return multiple addresses. + // The test verifies that the delegating resolver combines unresolved proxy + // host and target addresses correctly, returning addresses with the proxy host + // populated and the target address included as an attribute. + func (s) TestDelegatingResolverForMutipleProxyAddress(t *testing.T) { + const ( + targetTestAddr = "test.com" + resolvedTargetTestAddr1 = "1.1.1.1:8080" + resolvedTargetTestAddr2 = "2.2.2.2:8080" + envProxyAddr = "proxytest.com" + resolvedProxyTestAddr1 = "11.11.11.11:7687" + resolvedProxyTestAddr2 = "22.22.22.22:7687" + ) + hpfe := func(req *http.Request) (*url.URL, error) { + if req.URL.Host == targetTestAddr { + return &url.URL{ + Scheme: "https", + Host: envProxyAddr, + }, nil + } + t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, targetTestAddr) + return nil, nil + } + originalhpfe := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = hpfe + defer func() { + delegatingresolver.HTTPSProxyFromEnvironment = originalhpfe + }() + + targetResolver := manual.NewBuilderWithScheme("test") // Manual resolver to control the target resolution. + target := targetResolver.Scheme() + ":///" + targetTestAddr + proxyResolver := setupDNS(t) // Set up a manual DNS resolver to control the proxy address resolution. + + tcc, stateCh, _ := createTestResolverClientConn(t) + if _, err := delegatingresolver.New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, targetResolver, false); err != nil { + t.Fatalf("Failed to create delegating resolver: %v", err) + } + + proxyResolver.UpdateState(resolver.State{ + Addresses: []resolver.Address{ + {Addr: resolvedProxyTestAddr1}, + {Addr: resolvedProxyTestAddr2}, + }, + ServiceConfig: &serviceconfig.ParseResult{}, + }) + + select { + case <-stateCh: + t.Fatalf("Delegating resolver invoked UpdateState before both the proxy and target resolvers had updated their states.") + case <-time.After(defaultTestShortTimeout): + } + + targetResolver.UpdateState(resolver.State{ + Addresses: []resolver.Address{ + {Addr: resolvedTargetTestAddr1}, + {Addr: resolvedTargetTestAddr2}, + }, + ServiceConfig: &serviceconfig.ParseResult{}, + }) + + wantState := resolver.State{ + Addresses: []resolver.Address{ + proxyAddressWithTargetAttribute(envProxyAddr, resolvedTargetTestAddr1), + proxyAddressWithTargetAttribute(envProxyAddr, resolvedTargetTestAddr2), + }, + ServiceConfig: &serviceconfig.ParseResult{}, + } + var gotState resolver.State + select { + case gotState = <-stateCh: + case <-time.After(defaultTestTimeout): + t.Fatal("Timeout when waiting for a state update from the delegating resolver") + } + + if diff := cmp.Diff(gotState, wantState); diff != "" { + t.Fatalf("Unexpected state from delegating resolver. Diff (-got +want):\n%v", diff) + } + } + \ No newline at end of file diff --git a/internal/resolver/delegatingresolver/delegatingresolver_test.go b/internal/resolver/delegatingresolver/delegatingresolver_test.go index 55eaef79a954..c99afff422e6 100644 --- a/internal/resolver/delegatingresolver/delegatingresolver_test.go +++ b/internal/resolver/delegatingresolver/delegatingresolver_test.go @@ -25,9 +25,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "google.golang.org/grpc/internal" "google.golang.org/grpc/internal/grpctest" - _ "google.golang.org/grpc/resolver/dns" // To register dns resolver. ) type s struct { @@ -46,15 +44,15 @@ const ( // overrideHTTPSProxyFromEnvironment function overwrites HTTPSProxyFromEnvironment and // returns a function to restore the default values. func overrideHTTPSProxyFromEnvironment(hpfe func(req *http.Request) (*url.URL, error)) func() { - internal.HTTPSProxyFromEnvironmentForTesting = hpfe + HTTPSProxyFromEnvironment = hpfe return func() { - internal.HTTPSProxyFromEnvironmentForTesting = nil + HTTPSProxyFromEnvironment = nil } } -// Tests that the parsedURLForProxy function correctly resolves the proxy URL +// Tests that the proxyURLForTarget function correctly resolves the proxy URL // for a given target address. Tests all the possible output cases. -func (s) TestParsedURLForProxyEnv(t *testing.T) { +func (s) TestproxyURLForTargetEnv(t *testing.T) { err := errors.New("invalid proxy url") tests := []struct { name string @@ -63,7 +61,7 @@ func (s) TestParsedURLForProxyEnv(t *testing.T) { wantErr error }{ { - name: "valid proxy url and nil error", + name: "valid_proxy_url_and_nil_error", hpfeFunc: func(_ *http.Request) (*url.URL, error) { return &url.URL{ Scheme: "https", @@ -74,32 +72,33 @@ func (s) TestParsedURLForProxyEnv(t *testing.T) { Scheme: "https", Host: "proxy.example.com", }, - wantErr: nil, }, { - name: "invalid proxy url and non-nil error", + name: "invalid_proxy_url_and_non-nil_error", hpfeFunc: func(_ *http.Request) (*url.URL, error) { return &url.URL{ Scheme: "https", Host: "notproxy.example.com", }, err }, - wantURL: nil, + wantURL: &url.URL{ + Scheme: "https", + Host: "notproxy.example.com", + }, wantErr: err, }, { - name: "nil proxy url and nil error", + name: "nil_proxy_url_and_nil_error", hpfeFunc: func(_ *http.Request) (*url.URL, error) { return nil, nil }, wantURL: nil, - wantErr: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { defer overrideHTTPSProxyFromEnvironment(tt.hpfeFunc)() - got, err := parsedURLForProxy(targetTestAddr) + got, err := proxyURLForTarget(targetTestAddr) if err != tt.wantErr { t.Errorf("parsedProxyURLForProxy(%v) failed with error :%v, want %v\n", targetTestAddr, err, tt.wantErr) } diff --git a/internal/transport/http2_client.go b/internal/transport/http2_client.go index bc13dc77594e..3ef04a9461ec 100644 --- a/internal/transport/http2_client.go +++ b/internal/transport/http2_client.go @@ -159,7 +159,7 @@ func dial(ctx context.Context, fn func(context.Context, string) (net.Conn, error // A non-empty ConnectAddr attribute indicates that a proxy is configured, // so initiate a proxy dial. - if proxyattributes.ConnectAddr(addr) != "" { + if _,present:=proxyattributes.ExtractOptions(addr);present ==true { return proxyDial(ctx, addr, grpcUA) } networkType, ok := networktype.Get(addr) diff --git a/internal/transport/proxy.go b/internal/transport/proxy.go index d94d30fe5dba..7aa142694dc5 100644 --- a/internal/transport/proxy.go +++ b/internal/transport/proxy.go @@ -61,21 +61,21 @@ func doHTTPConnectHandshake(ctx context.Context, conn net.Conn, addr resolver.Ad } }() - a := proxyattributes.ConnectAddr(addr) + a, _ := proxyattributes.ExtractOptions(addr) req := &http.Request{ Method: http.MethodConnect, - URL: &url.URL{Host: a}, + URL: &url.URL{Host: a.ConnectAddr}, Header: map[string][]string{"User-Agent": {grpcUA}}, } - if user := proxyattributes.User(addr); user != nil { - u := user.Username() - p, pSet := user.Password() - if !pSet { - logger.Warningf("password not set for basic authentication for proxy dialing") - } - req.Header.Add(proxyAuthHeaderKey, "Basic "+basicAuth(u, p)) + user := a.User + u := user.Username() + p, pSet := user.Password() + if !pSet { + logger.Warningf("password not set for basic authentication for proxy dialing") } + req.Header.Add(proxyAuthHeaderKey, "Basic "+basicAuth(u, p)) + if err := sendHTTPRequest(ctx, req, conn); err != nil { return nil, fmt.Errorf("failed to write the HTTP request: %v", err) } diff --git a/test/proxy_test.go b/test/proxy_test.go index 7c567e8d6f21..c32b74db9863 100644 --- a/test/proxy_test.go +++ b/test/proxy_test.go @@ -24,12 +24,11 @@ import ( "fmt" "net" "net/http" - "net/url" + "os" "testing" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" - "google.golang.org/grpc/internal" "google.golang.org/grpc/internal/resolver/delegatingresolver" "google.golang.org/grpc/internal/stubserver" "google.golang.org/grpc/internal/testutils" @@ -112,20 +111,10 @@ func (s) TestGRPCDialWithProxy(t *testing.T) { backendAddr := createAndStartBackendServer(t) pLis, errCh, doneCh, _ := setupProxy(t, backendAddr, false, requestCheck(unresolvedTargetURI)) - hpfe := func(req *http.Request) (*url.URL, error) { - if req.URL.Host == unresolvedTargetURI { - return &url.URL{ - Scheme: "https", - Host: pLis.Addr().String(), - }, nil - } - return nil, nil - } - orighpfe := internal.HTTPSProxyFromEnvironmentForTesting - internal.HTTPSProxyFromEnvironmentForTesting = hpfe - defer func() { - internal.HTTPSProxyFromEnvironmentForTesting = orighpfe - }() + // Overwrite the proxy environment and restore it after the test. + proxyEnv := os.Getenv("HTTPS_PROXY") + os.Setenv("HTTPS_PROXY", pLis.Addr().String()) + defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() @@ -158,21 +147,10 @@ func (s) TestGRPCDialWithDNSAndProxy(t *testing.T) { backendAddr := createAndStartBackendServer(t) pLis, errCh, doneCh, _ := setupProxy(t, backendAddr, false, requestCheck(backendAddr)) - // Overwrite the default proxy function and restore it after the test. - hpfe := func(req *http.Request) (*url.URL, error) { - if req.URL.Host == unresolvedTargetURI { - return &url.URL{ - Scheme: "https", - Host: unresolvedProxyURI, - }, nil - } - return nil, nil - } - orighpfe := internal.HTTPSProxyFromEnvironmentForTesting - internal.HTTPSProxyFromEnvironmentForTesting = hpfe - defer func() { - internal.HTTPSProxyFromEnvironmentForTesting = orighpfe - }() + // Overwrite the proxy environment and restore it after the test. + proxyEnv := os.Getenv("HTTPS_PROXY") + os.Setenv("HTTPS_PROXY", unresolvedProxyURI) + defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() // Configure manual resolvers for both proxy and target backends mrTarget := setupDNS(t) @@ -215,33 +193,13 @@ func (s) TestGRPCDialWithDNSAndProxy(t *testing.T) { // unresolved target URI in the HTTP CONNECT request, and successfully // establishes a connection to the backend server. func (s) TestGRPCNewClientWithProxy(t *testing.T) { - // Set up a channel to receive signals from OnClientResolution. - resCh := make(chan bool, 1) - // Overwrite OnClientResolution to send a signal to the channel. - origOnClientResolution := delegatingresolver.OnClientResolution - delegatingresolver.OnClientResolution = func(int) { - resCh <- true - } - t.Cleanup(func() { delegatingresolver.OnClientResolution = origOnClientResolution }) - backendAddr := createAndStartBackendServer(t) pLis, errCh, doneCh, _ := setupProxy(t, backendAddr, false, requestCheck(unresolvedTargetURI)) - // Overwrite the proxy resolution function and restore it afterward. - hpfe := func(req *http.Request) (*url.URL, error) { - if req.URL.Host == unresolvedTargetURI { - return &url.URL{ - Scheme: "https", - Host: unresolvedProxyURI, - }, nil - } - return nil, nil - } - orighpfe := internal.HTTPSProxyFromEnvironmentForTesting - internal.HTTPSProxyFromEnvironmentForTesting = hpfe - defer func() { - internal.HTTPSProxyFromEnvironmentForTesting = orighpfe - }() + // Overwrite the proxy environment and restore it after the test. + proxyEnv := os.Getenv("HTTPS_PROXY") + os.Setenv("HTTPS_PROXY", unresolvedProxyURI) + defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() // Set up and update a manual resolver for proxy resolution. mrProxy := setupDNS(t) @@ -273,14 +231,6 @@ func (s) TestGRPCNewClientWithProxy(t *testing.T) { case <-doneCh: t.Logf("proxy server succeeded") } - - // Verify if OnClientResolution was triggered. - select { - case <-resCh: - t.Fatal("target resolution occurred on client unexpectedly") - default: - t.Log("target resolution did not occur on the client") - } } // Tests the scenario where grpc.NewClient is used with a custom target URI @@ -289,34 +239,14 @@ func (s) TestGRPCNewClientWithProxy(t *testing.T) { // includes the resolved target URI in the HTTP CONNECT request, and // establishes a connection to the backend server. func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { - // Set up a channel to receive signals from OnClientResolution. - resCh := make(chan bool, 1) - // Overwrite OnClientResolution to send a signal to the channel. - origOnClientResolution := delegatingresolver.OnClientResolution - delegatingresolver.OnClientResolution = func(int) { - resCh <- true - } - t.Cleanup(func() { delegatingresolver.OnClientResolution = origOnClientResolution }) - backendAddr := createAndStartBackendServer(t) // Set up and start the proxy server. pLis, errCh, doneCh, _ := setupProxy(t, backendAddr, true, requestCheck(backendAddr)) - // Overwrite the proxy resolution function and restore it afterward. - hpfe := func(req *http.Request) (*url.URL, error) { - if req.URL.Host == unresolvedTargetURI { - return &url.URL{ - Scheme: "https", - Host: unresolvedProxyURI, - }, nil - } - return nil, nil - } - orighpfe := internal.HTTPSProxyFromEnvironmentForTesting - internal.HTTPSProxyFromEnvironmentForTesting = hpfe - defer func() { - internal.HTTPSProxyFromEnvironmentForTesting = orighpfe - }() + // Overwrite the proxy environment and restore it after the test. + proxyEnv := os.Getenv("HTTPS_PROXY") + os.Setenv("HTTPS_PROXY", unresolvedProxyURI) + defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() // Create and update a custom resolver for target URI. mrTarget := manual.NewBuilderWithScheme("whatever") @@ -348,14 +278,6 @@ func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { case <-doneCh: t.Logf("proxy server succeeded") } - - // Check if client-side resolution signal was sent to the channel. - select { - case <-resCh: - t.Log("target resolution occurred on client") - default: - t.Fatal("target resolution did not occur on the client unexpectedly") - } } // Tests the scenario where grpc.NewClient is used with the default "dns" @@ -365,33 +287,13 @@ func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { // CONNECT request, the proxy URI is resolved correctly, and the connection is // successfully established with the backend server through the proxy. func (s) TestGRPCNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { - // Set up a channel to receive signals from OnClientResolution. - resCh := make(chan bool, 1) - // Overwrite OnClientResolution to send a signal to the channel. - origOnClientResolution := delegatingresolver.OnClientResolution - delegatingresolver.OnClientResolution = func(int) { - resCh <- true - } - t.Cleanup(func() { delegatingresolver.OnClientResolution = origOnClientResolution }) - backendAddr := createAndStartBackendServer(t) pLis, errCh, doneCh, _ := setupProxy(t, backendAddr, true, requestCheck(backendAddr)) - // Overwrite the proxy resolution function and restore it afterward. - hpfe := func(req *http.Request) (*url.URL, error) { - if req.URL.Host == unresolvedTargetURI { - return &url.URL{ - Scheme: "https", - Host: unresolvedProxyURI, - }, nil - } - return nil, nil - } - orighpfe := internal.HTTPSProxyFromEnvironmentForTesting - internal.HTTPSProxyFromEnvironmentForTesting = hpfe - defer func() { - internal.HTTPSProxyFromEnvironmentForTesting = orighpfe - }() + // Overwrite the proxy environment and restore it after the test. + proxyEnv := os.Getenv("HTTPS_PROXY") + os.Setenv("HTTPS_PROXY", unresolvedProxyURI) + defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() // Configure manual resolvers for both proxy and target backends mrTarget := setupDNS(t) @@ -431,14 +333,6 @@ func (s) TestGRPCNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { case <-doneCh: t.Logf("proxy server succeeded") } - - // Check if client-side resolution signal was sent to the channel. - select { - case <-resCh: - t.Log("target resolution occurred on client") - default: - t.Fatal("target resolution did not occur on client unexpectedly") - } } // Tests the scenario where grpc.NewClient is used with grpc.WithNoProxy() set, @@ -452,20 +346,10 @@ func (s) TestGRPCNewClientWithNoProxy(t *testing.T) { return fmt.Errorf("proxy server should not have received a Connect request: %v", req) }) - proxyCalled := false - hpfe := func(_ *http.Request) (*url.URL, error) { - proxyCalled = true - return &url.URL{ - Scheme: "https", - Host: unresolvedProxyURI, - }, nil - - } - orighpfe := internal.HTTPSProxyFromEnvironmentForTesting - internal.HTTPSProxyFromEnvironmentForTesting = hpfe - defer func() { - internal.HTTPSProxyFromEnvironmentForTesting = orighpfe - }() + // Overwrite the proxy environment and restore it after the test. + proxyEnv := os.Getenv("HTTPS_PROXY") + os.Setenv("HTTPS_PROXY", unresolvedProxyURI) + defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() mrTarget := setupDNS(t) mrTarget.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) @@ -489,9 +373,6 @@ func (s) TestGRPCNewClientWithNoProxy(t *testing.T) { t.Errorf("EmptyCall() failed: %v", err) } - if proxyCalled { - t.Error("http.ProxyFromEnvironment function was unexpectedly called") - } // Verify that the proxy was not dialed. select { case <-proxyStartedCh: @@ -512,20 +393,10 @@ func (s) TestGRPCNewClientWithContextDialer(t *testing.T) { return fmt.Errorf("proxy server should not have received a Connect request: %v", req) }) - proxyCalled := false - hpfe := func(_ *http.Request) (*url.URL, error) { - proxyCalled = true - return &url.URL{ - Scheme: "https", - Host: unresolvedProxyURI, - }, nil - - } - orighpfe := internal.HTTPSProxyFromEnvironmentForTesting - internal.HTTPSProxyFromEnvironmentForTesting = hpfe - defer func() { - internal.HTTPSProxyFromEnvironmentForTesting = orighpfe - }() + // Overwrite the proxy environment and restore it after the test. + proxyEnv := os.Getenv("HTTPS_PROXY") + os.Setenv("HTTPS_PROXY", unresolvedProxyURI) + defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() // Create a custom dialer that directly dials the backend. We'll use this // to bypass any proxy logic. @@ -555,10 +426,6 @@ func (s) TestGRPCNewClientWithContextDialer(t *testing.T) { t.Errorf("EmptyCall() failed: %v", err) } - if proxyCalled { - t.Error("http.ProxyFromEnvironment function was unexpectedly called") - } - select { case <-dialerCalled: t.Log("custom dialer was invoked") @@ -582,15 +449,6 @@ func (s) TestGRPCNewClientWithContextDialer(t *testing.T) { // the CONNECT request. The test also ensures that target resolution does not // happen on the client. func (s) TestBasicAuthInGrpcNewClientWithProxy(t *testing.T) { - // Set up a channel to receive signals from OnClientResolution. - resCh := make(chan bool, 1) - // Overwrite OnClientResolution to send a signal to the channel. - origOnClientResolution := delegatingresolver.OnClientResolution - delegatingresolver.OnClientResolution = func(int) { - resCh <- true - } - t.Cleanup(func() { delegatingresolver.OnClientResolution = origOnClientResolution }) - backendAddr := createAndStartBackendServer(t) const ( user = "notAUser" @@ -615,24 +473,10 @@ func (s) TestBasicAuthInGrpcNewClientWithProxy(t *testing.T) { return nil }) - // Overwrite the proxy resolution function and restore it afterward. - hpfe := func(req *http.Request) (*url.URL, error) { - if req.URL.Host == unresolvedTargetURI { - u := url.URL{ - Scheme: "https", - Host: unresolvedProxyURI, - } - u.User = url.UserPassword(user, password) - return &u, nil - } - return nil, nil - } - orighpfe := internal.HTTPSProxyFromEnvironmentForTesting - internal.HTTPSProxyFromEnvironmentForTesting = hpfe - defer func() { - internal.HTTPSProxyFromEnvironmentForTesting = orighpfe - }() - + // Overwrite the proxy environment and restore it after the test. + proxyEnv := os.Getenv("HTTPS_PROXY") + os.Setenv("HTTPS_PROXY", "https://"+user+":"+password+"@"+unresolvedProxyURI) + defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() // Set up and update a manual resolver for proxy resolution. mrProxy := setupDNS(t) mrProxy.InitialState(resolver.State{ @@ -662,12 +506,4 @@ func (s) TestBasicAuthInGrpcNewClientWithProxy(t *testing.T) { case <-doneCh: t.Logf("proxy server succeeded") } - - // Verify if OnClientResolution was triggered. - select { - case <-resCh: - t.Fatal("target resolution occurred on client unexpectedly") - default: - t.Log("target resolution did not occur on the client") - } } From 93fc3e13ad6465d69ed643429e9dd09bab6d47bd Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Thu, 19 Dec 2024 05:18:11 +0530 Subject: [PATCH 14/53] trying tests --- output.txt | 62 ++++++++++++++++++++++++++++++++++++++++++++++ test/proxy_test.go | 15 +++++++++++ 2 files changed, 77 insertions(+) create mode 100644 output.txt diff --git a/output.txt b/output.txt new file mode 100644 index 000000000000..0efae30d662e --- /dev/null +++ b/output.txt @@ -0,0 +1,62 @@ +=== RUN Test +=== RUN Test/GRPCDialWithProxy + tlogger.go:116: INFO server.go:685 [core] [Server #1]Server created (t=+2.616834ms) + proxy_test.go:52: Started TestService backend at: "127.0.0.1:58638" + tlogger.go:116: INFO server.go:881 [core] [Server #1 ListenSocket #2]ListenSocket created (t=+2.91925ms) + tlogger.go:116: INFO clientconn.go:1654 [core] original dial target is: "example.com" (t=+3.284125ms) + tlogger.go:116: INFO clientconn.go:314 [core] [Channel #3]Channel created (t=+3.36ms) + tlogger.go:116: INFO clientconn.go:193 [core] [Channel #3]parsed dial target is: resolver.Target{URL:url.URL{Scheme:"passthrough", Opaque:"", User:(*url.Userinfo)(nil), Host:"", Path:"/example.com", RawPath:"", OmitHost:false, ForceQuery:false, RawQuery:"", Fragment:"", RawFragment:""}} (t=+3.41775ms) + tlogger.go:116: INFO clientconn.go:194 [core] [Channel #3]Channel authority set to "example.com" (t=+3.448084ms) + tlogger.go:116: INFO resolver_wrapper.go:211 [core] [Channel #3]Resolver state updated: { + "Addresses": [ + { + "Addr": "127.0.0.1:58639", + "ServerName": "", + "Attributes": { + "\u003c%!p(proxyattributes.keyType=grpc.resolver.delegatingresolver.proxyOptions)\u003e": "\u003c%!p(proxyattributes.Options={{ false} example.com})\u003e" + }, + "BalancerAttributes": null, + "Metadata": null + } + ], + "Endpoints": [ + { + "Addresses": [ + { + "Addr": "127.0.0.1:58639", + "ServerName": "", + "Attributes": { + "\u003c%!p(proxyattributes.keyType=grpc.resolver.delegatingresolver.proxyOptions)\u003e": "\u003c%!p(proxyattributes.Options={{ false} example.com})\u003e" + }, + "BalancerAttributes": null, + "Metadata": null + } + ], + "Attributes": null + } + ], + "ServiceConfig": null, + "Attributes": null + } (resolver returned new addresses) (t=+4.045584ms) + tlogger.go:116: INFO balancer_wrapper.go:109 [core] [Channel #3]Channel switches to new LB policy "pick_first" (t=+4.126709ms) + tlogger.go:116: INFO clientconn.go:838 [core] [Channel #3 SubChannel #4]Subchannel created (t=+4.180167ms) + tlogger.go:116: INFO clientconn.go:544 [core] [Channel #3]Channel Connectivity change to CONNECTING (t=+4.218167ms) + tlogger.go:116: INFO clientconn.go:314 [core] [Channel #3]Channel exiting idle mode (t=+4.2565ms) + tlogger.go:116: INFO clientconn.go:1199 [core] [Channel #3 SubChannel #4]Subchannel Connectivity change to CONNECTING (t=+4.288584ms) + tlogger.go:116: INFO clientconn.go:1319 [core] [Channel #3 SubChannel #4]Subchannel picks a new address "127.0.0.1:58639" to connect (t=+4.357875ms) + tlogger.go:116: WARNING proxy.go:74 [transport] password not set for basic authentication for proxy dialing (t=+5.617084ms) + tlogger.go:116: INFO syscall_nonlinux.go:39 [core] CPU time info is unavailable on non-linux environments. (t=+7.15175ms) + tlogger.go:116: INFO clientconn.go:1199 [core] [Channel #3 SubChannel #4]Subchannel Connectivity change to READY (t=+7.375292ms) + tlogger.go:116: INFO clientconn.go:544 [core] [Channel #3]Channel Connectivity change to READY (t=+7.509584ms) + proxy_test.go:149: proxy server succeeded + tlogger.go:116: INFO clientconn.go:544 [core] [Channel #3]Channel Connectivity change to SHUTDOWN (t=+8.258542ms) + tlogger.go:116: INFO resolver_wrapper.go:111 [core] [Channel #3]Closing the name resolver (t=+8.295792ms) + tlogger.go:116: INFO balancer_wrapper.go:147 [core] [Channel #3]ccBalancerWrapper: closing (t=+8.330042ms) + tlogger.go:116: INFO clientconn.go:1199 [core] [Channel #3 SubChannel #4]Subchannel Connectivity change to SHUTDOWN (t=+8.4035ms) + tlogger.go:116: INFO clientconn.go:1527 [core] [Channel #3 SubChannel #4]Subchannel deleted (t=+8.448125ms) + tlogger.go:116: INFO clientconn.go:314 [core] [Channel #3]Channel deleted (t=+8.58825ms) + tlogger.go:116: INFO server.go:817 [core] [Server #1 ListenSocket #2]ListenSocket deleted (t=+8.753375ms) +--- PASS: Test (0.02s) + --- PASS: Test/GRPCDialWithProxy (0.01s) +PASS +ok google.golang.org/grpc/test (cached) diff --git a/test/proxy_test.go b/test/proxy_test.go index c32b74db9863..9cd2faca416c 100644 --- a/test/proxy_test.go +++ b/test/proxy_test.go @@ -111,6 +111,21 @@ func (s) TestGRPCDialWithProxy(t *testing.T) { backendAddr := createAndStartBackendServer(t) pLis, errCh, doneCh, _ := setupProxy(t, backendAddr, false, requestCheck(unresolvedTargetURI)) + // hpfe := func(req *http.Request) (*url.URL, error) { + // if req.URL.Host == unresolvedTargetURI { + // return &url.URL{ + // Scheme: "https", + // Host: pLis.Addr().String(), + // }, nil + // } + // return nil, nil + // } + // orighpfe := delegatingresolver.HTTPSProxyFromEnvironment + // delegatingresolver.HTTPSProxyFromEnvironment = hpfe + // defer func() { + // delegatingresolver.HTTPSProxyFromEnvironment = orighpfe + // }() + // Overwrite the proxy environment and restore it after the test. proxyEnv := os.Getenv("HTTPS_PROXY") os.Setenv("HTTPS_PROXY", pLis.Addr().String()) From e22ad1d4afa9c04741ed09088b5eb96bb9d81119 Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Thu, 19 Dec 2024 18:00:56 +0530 Subject: [PATCH 15/53] tests --- internal/testutils/proxy.go | 9 ++--- test/proxy_test.go | 72 +++++++++++++++++++------------------ 2 files changed, 43 insertions(+), 38 deletions(-) diff --git a/internal/testutils/proxy.go b/internal/testutils/proxy.go index 3ebf5d3ec3ee..6559b6f54b82 100644 --- a/internal/testutils/proxy.go +++ b/internal/testutils/proxy.go @@ -79,8 +79,9 @@ func NewProxyServer(lis net.Listener, reqCheck func(*http.Request) error, errCh return } var out net.Conn - // if resolution is done on client,connect to address received in - // CONNECT request or else connect to backend address directly. + // If resolution is done on client,connect to address received in + // CONNECT request or else connect to backend address directly. This is + // to mimick the name resolution on proxy server. if resOnClient { out, err = net.Dial("tcp", req.URL.Host) } else { @@ -92,13 +93,13 @@ func NewProxyServer(lis net.Listener, reqCheck func(*http.Request) error, errCh return } out.SetDeadline(time.Now().Add(defaultTestTimeout)) - //response OK to client + // Response OK to client resp := http.Response{StatusCode: http.StatusOK, Proto: "HTTP/1.0"} var buf bytes.Buffer resp.Write(&buf) p.in.Write(buf.Bytes()) p.out = out - // perform the proxy function, i.e pass the data from client to server and server to client. + // Perform the proxy function, i.e pass the data from client to server and server to client. go io.Copy(p.in, p.out) go io.Copy(p.out, p.in) close(doneCh) diff --git a/test/proxy_test.go b/test/proxy_test.go index 9cd2faca416c..eebff9e19d96 100644 --- a/test/proxy_test.go +++ b/test/proxy_test.go @@ -168,22 +168,23 @@ func (s) TestGRPCDialWithDNSAndProxy(t *testing.T) { defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() // Configure manual resolvers for both proxy and target backends - mrTarget := setupDNS(t) - mrTarget.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) + targetResolver := setupDNS(t) + targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) + // Temporarily modify ProxyScheme for this test. origScheme := delegatingresolver.ProxyScheme - delegatingresolver.ProxyScheme = "whatever" + delegatingresolver.ProxyScheme = "test" t.Cleanup(func() { delegatingresolver.ProxyScheme = origScheme }) - mrProxy := manual.NewBuilderWithScheme("whatever") - resolver.Register(mrProxy) - mrProxy.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: pLis.Addr().String()}}}) - // Dial to the proxy server. + proxyResolver := manual.NewBuilderWithScheme("test") + resolver.Register(proxyResolver) + proxyResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: pLis.Addr().String()}}}) + ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() - conn, err := grpc.Dial(mrTarget.Scheme()+":///"+unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) + conn, err := grpc.Dial(targetResolver.Scheme()+":///"+unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { - t.Fatalf("grpc.Dial(%s) failed: %v", mrTarget.Scheme()+":///"+unresolvedTargetURI, err) + t.Fatalf("grpc.Dial(%s) failed: %v", targetResolver.Scheme()+":///"+unresolvedTargetURI, err) } defer conn.Close() @@ -204,7 +205,7 @@ func (s) TestGRPCDialWithDNSAndProxy(t *testing.T) { // Tests the scenario where grpc.NewClient is used with the default DNS // resolver for the target URI and a proxy is configured. The test verifies -// that the client. resolves proxy URI, connects to the proxy server, sends the +// that the client resolves proxy URI, connects to the proxy server, sends the // unresolved target URI in the HTTP CONNECT request, and successfully // establishes a connection to the backend server. func (s) TestGRPCNewClientWithProxy(t *testing.T) { @@ -217,8 +218,8 @@ func (s) TestGRPCNewClientWithProxy(t *testing.T) { defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() // Set up and update a manual resolver for proxy resolution. - mrProxy := setupDNS(t) - mrProxy.InitialState(resolver.State{ + proxyResolver := setupDNS(t) + proxyResolver.InitialState(resolver.State{ Addresses: []resolver.Address{ {Addr: pLis.Addr().String()}, }, @@ -264,19 +265,20 @@ func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() // Create and update a custom resolver for target URI. - mrTarget := manual.NewBuilderWithScheme("whatever") - resolver.Register(mrTarget) - mrTarget.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) + targetResolver := manual.NewBuilderWithScheme("test") + resolver.Register(targetResolver) + targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) + // Set up and update a manual resolver for proxy resolution. - mrProxy := setupDNS(t) - mrProxy.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: pLis.Addr().String()}}}) + proxyResolver := setupDNS(t) + proxyResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: pLis.Addr().String()}}}) // Dial to the proxy server. ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() - conn, err := grpc.NewClient(mrTarget.Scheme()+":///"+unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) + conn, err := grpc.NewClient(targetResolver.Scheme()+":///"+unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { - t.Fatalf("grpc.NewClient(%s) failed: %v", mrTarget.Scheme()+":///"+unresolvedTargetURI, err) + t.Fatalf("grpc.NewClient(%s) failed: %v", targetResolver.Scheme()+":///"+unresolvedTargetURI, err) } t.Cleanup(func() { conn.Close() }) @@ -298,7 +300,7 @@ func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { // Tests the scenario where grpc.NewClient is used with the default "dns" // resolver and the dial option grpc.WithTargetResolutionEnabled() is set, // enabling target resolution on the client. The test verifies that target -// resolution happens on the client by sending resolved target URi in HTTP +// resolution happens on the client by sending resolved target URI in HTTP // CONNECT request, the proxy URI is resolved correctly, and the connection is // successfully established with the backend server through the proxy. func (s) TestGRPCNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { @@ -311,15 +313,17 @@ func (s) TestGRPCNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() // Configure manual resolvers for both proxy and target backends - mrTarget := setupDNS(t) - mrTarget.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) + targetResolver := setupDNS(t) + targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) + // Temporarily modify ProxyScheme for this test. origScheme := delegatingresolver.ProxyScheme - delegatingresolver.ProxyScheme = "whatever" + delegatingresolver.ProxyScheme = "test" t.Cleanup(func() { delegatingresolver.ProxyScheme = origScheme }) - mrProxy := manual.NewBuilderWithScheme("whatever") - resolver.Register(mrProxy) - mrProxy.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: pLis.Addr().String()}}}) + + proxyResolver := manual.NewBuilderWithScheme("test") + resolver.Register(proxyResolver) + proxyResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: pLis.Addr().String()}}}) // Dial options with target resolution enabled. dopts := []grpc.DialOption{ @@ -366,8 +370,8 @@ func (s) TestGRPCNewClientWithNoProxy(t *testing.T) { os.Setenv("HTTPS_PROXY", unresolvedProxyURI) defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() - mrTarget := setupDNS(t) - mrTarget.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) + targetResolver := setupDNS(t) + targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) // Dial options with proxy explicitly disabled. dopts := []grpc.DialOption{ @@ -415,14 +419,14 @@ func (s) TestGRPCNewClientWithContextDialer(t *testing.T) { // Create a custom dialer that directly dials the backend. We'll use this // to bypass any proxy logic. - dialerCalled := make(chan bool, 1) + dialerCalled := make(chan struct{}) customDialer := func(_ context.Context, backendAddr string) (net.Conn, error) { - dialerCalled <- true + close(dialerCalled) return net.Dial("tcp", backendAddr) } - mrTarget := setupDNS(t) - mrTarget.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) + targetResolver := setupDNS(t) + targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) dopts := []grpc.DialOption{ grpc.WithTransportCredentials(insecure.NewCredentials()), @@ -493,8 +497,8 @@ func (s) TestBasicAuthInGrpcNewClientWithProxy(t *testing.T) { os.Setenv("HTTPS_PROXY", "https://"+user+":"+password+"@"+unresolvedProxyURI) defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() // Set up and update a manual resolver for proxy resolution. - mrProxy := setupDNS(t) - mrProxy.InitialState(resolver.State{ + proxyResolver := setupDNS(t) + proxyResolver.InitialState(resolver.State{ Addresses: []resolver.Address{ {Addr: pLis.Addr().String()}, }, From b5155658fe36bc515cd220e060b508f9ee5a6dab Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Fri, 20 Dec 2024 03:20:43 +0530 Subject: [PATCH 16/53] test --- test/proxy_test.go | 202 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 170 insertions(+), 32 deletions(-) diff --git a/test/proxy_test.go b/test/proxy_test.go index eebff9e19d96..659d0c8ebc3a 100644 --- a/test/proxy_test.go +++ b/test/proxy_test.go @@ -24,9 +24,10 @@ import ( "fmt" "net" "net/http" - "os" + "net/url" "testing" + "golang.org/x/net/http/httpproxy" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/internal/resolver/delegatingresolver" @@ -45,6 +46,7 @@ const ( // createAndStartBackendServer creates and starts a test backend server, // registering a cleanup to stop it. func createAndStartBackendServer(t *testing.T) string { + t.Helper() backend := &stubserver.StubServer{ EmptyCallF: func(context.Context, *testgrpc.Empty) (*testgrpc.Empty, error) { return &testgrpc.Empty{}, nil }, } @@ -59,6 +61,7 @@ func createAndStartBackendServer(t *testing.T) string { // setupDNS sets up a manual DNS resolver and registers it, returning the // builder for test customization. func setupDNS(t *testing.T) *manual.Resolver { + t.Helper() mr := manual.NewBuilderWithScheme("dns") origRes := resolver.Get("dns") resolver.Register(mr) @@ -71,6 +74,7 @@ func setupDNS(t *testing.T) *manual.Resolver { // setupProxy initializes and starts a proxy server, registers a cleanup to // stop it, and returns the proxy's listener and helper channels. func setupProxy(t *testing.T, backendAddr string, resolutionOnClient bool, reqCheck func(*http.Request) error) (net.Listener, chan error, chan struct{}, chan struct{}) { + t.Helper() pLis, err := net.Listen("tcp", "localhost:0") if err != nil { t.Fatalf("failed to listen: %v", err) @@ -127,9 +131,23 @@ func (s) TestGRPCDialWithProxy(t *testing.T) { // }() // Overwrite the proxy environment and restore it after the test. - proxyEnv := os.Getenv("HTTPS_PROXY") - os.Setenv("HTTPS_PROXY", pLis.Addr().String()) - defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() + // proxyEnv := os.Getenv("HTTPS_PROXY") + // os.Setenv("HTTPS_PROXY", pLis.Addr().String()) + // defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() + + origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment + defer func() { + delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment + }() + + proxyConfig := httpproxy.Config{ + HTTPSProxy: pLis.Addr().String(), + NoProxy: "", + } + + delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { + return proxyConfig.ProxyFunc()(req.URL) + } ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() @@ -162,10 +180,19 @@ func (s) TestGRPCDialWithDNSAndProxy(t *testing.T) { backendAddr := createAndStartBackendServer(t) pLis, errCh, doneCh, _ := setupProxy(t, backendAddr, false, requestCheck(backendAddr)) - // Overwrite the proxy environment and restore it after the test. - proxyEnv := os.Getenv("HTTPS_PROXY") - os.Setenv("HTTPS_PROXY", unresolvedProxyURI) - defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() + origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment + defer func() { + delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment + }() + + proxyConfig := httpproxy.Config{ + HTTPSProxy: unresolvedProxyURI, + NoProxy: "", + } + + delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { + return proxyConfig.ProxyFunc()(req.URL) + } // Configure manual resolvers for both proxy and target backends targetResolver := setupDNS(t) @@ -212,10 +239,19 @@ func (s) TestGRPCNewClientWithProxy(t *testing.T) { backendAddr := createAndStartBackendServer(t) pLis, errCh, doneCh, _ := setupProxy(t, backendAddr, false, requestCheck(unresolvedTargetURI)) - // Overwrite the proxy environment and restore it after the test. - proxyEnv := os.Getenv("HTTPS_PROXY") - os.Setenv("HTTPS_PROXY", unresolvedProxyURI) - defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() + origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment + defer func() { + delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment + }() + + proxyConfig := httpproxy.Config{ + HTTPSProxy: unresolvedProxyURI, + NoProxy: "", + } + + delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { + return proxyConfig.ProxyFunc()(req.URL) + } // Set up and update a manual resolver for proxy resolution. proxyResolver := setupDNS(t) @@ -259,10 +295,19 @@ func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { // Set up and start the proxy server. pLis, errCh, doneCh, _ := setupProxy(t, backendAddr, true, requestCheck(backendAddr)) - // Overwrite the proxy environment and restore it after the test. - proxyEnv := os.Getenv("HTTPS_PROXY") - os.Setenv("HTTPS_PROXY", unresolvedProxyURI) - defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() + origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment + defer func() { + delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment + }() + + proxyConfig := httpproxy.Config{ + HTTPSProxy: unresolvedProxyURI, + NoProxy: "", + } + + delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { + return proxyConfig.ProxyFunc()(req.URL) + } // Create and update a custom resolver for target URI. targetResolver := manual.NewBuilderWithScheme("test") @@ -297,6 +342,62 @@ func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { } } +// Tests the scenario where grpc.NewClient is used with a custom target URI +// scheme and a proxy is configured and both the resolvers return endpoints. +// The test verifies that the client successfully connects to the proxy server, +// resolves the proxy URI correctly, includes the resolved target URI in the +// HTTP CONNECT request, and establishes a connection to the backend server. +func (s) TestGRPCNewClientWithProxyAndCustomResolverWithEndpoints(t *testing.T) { + backendAddr := createAndStartBackendServer(t) + // Set up and start the proxy server. + pLis, errCh, doneCh, _ := setupProxy(t, backendAddr, true, requestCheck(backendAddr)) + + origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment + defer func() { + delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment + }() + + proxyConfig := httpproxy.Config{ + HTTPSProxy: unresolvedProxyURI, + NoProxy: "", + } + + delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { + return proxyConfig.ProxyFunc()(req.URL) + } + + // Create and update a custom resolver for target URI. + targetResolver := manual.NewBuilderWithScheme("test") + resolver.Register(targetResolver) + targetResolver.InitialState(resolver.State{Endpoints: []resolver.Endpoint{{Addresses: []resolver.Address{{Addr: backendAddr}}}}}) + // Set up and update a manual resolver for proxy resolution. + proxyResolver := setupDNS(t) + proxyResolver.InitialState(resolver.State{Endpoints: []resolver.Endpoint{{Addresses: []resolver.Address{{Addr: pLis.Addr().String()}}}}}) + + // Dial to the proxy server. + ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) + defer cancel() + conn, err := grpc.NewClient(targetResolver.Scheme()+":///"+unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + t.Fatalf("grpc.NewClient(%s) failed: %v", targetResolver.Scheme()+":///"+unresolvedTargetURI, err) + } + t.Cleanup(func() { conn.Close() }) + + // Create a test service client and make an RPC call. + client := testgrpc.NewTestServiceClient(conn) + if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { + t.Errorf("EmptyCall() failed: %v", err) + } + + // Check if the proxy encountered any errors. + select { + case err := <-errCh: + t.Fatalf("proxy server encountered an error: %v", err) + case <-doneCh: + t.Logf("proxy server succeeded") + } +} + // Tests the scenario where grpc.NewClient is used with the default "dns" // resolver and the dial option grpc.WithTargetResolutionEnabled() is set, // enabling target resolution on the client. The test verifies that target @@ -307,10 +408,19 @@ func (s) TestGRPCNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { backendAddr := createAndStartBackendServer(t) pLis, errCh, doneCh, _ := setupProxy(t, backendAddr, true, requestCheck(backendAddr)) - // Overwrite the proxy environment and restore it after the test. - proxyEnv := os.Getenv("HTTPS_PROXY") - os.Setenv("HTTPS_PROXY", unresolvedProxyURI) - defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() + origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment + defer func() { + delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment + }() + + proxyConfig := httpproxy.Config{ + HTTPSProxy: unresolvedProxyURI, + NoProxy: "", + } + + delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { + return proxyConfig.ProxyFunc()(req.URL) + } // Configure manual resolvers for both proxy and target backends targetResolver := setupDNS(t) @@ -365,10 +475,19 @@ func (s) TestGRPCNewClientWithNoProxy(t *testing.T) { return fmt.Errorf("proxy server should not have received a Connect request: %v", req) }) - // Overwrite the proxy environment and restore it after the test. - proxyEnv := os.Getenv("HTTPS_PROXY") - os.Setenv("HTTPS_PROXY", unresolvedProxyURI) - defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() + origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment + defer func() { + delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment + }() + + proxyConfig := httpproxy.Config{ + HTTPSProxy: unresolvedProxyURI, + NoProxy: "", + } + + delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { + return proxyConfig.ProxyFunc()(req.URL) + } targetResolver := setupDNS(t) targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) @@ -412,10 +531,19 @@ func (s) TestGRPCNewClientWithContextDialer(t *testing.T) { return fmt.Errorf("proxy server should not have received a Connect request: %v", req) }) - // Overwrite the proxy environment and restore it after the test. - proxyEnv := os.Getenv("HTTPS_PROXY") - os.Setenv("HTTPS_PROXY", unresolvedProxyURI) - defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() + origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment + defer func() { + delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment + }() + + proxyConfig := httpproxy.Config{ + HTTPSProxy: unresolvedProxyURI, + NoProxy: "", + } + + delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { + return proxyConfig.ProxyFunc()(req.URL) + } // Create a custom dialer that directly dials the backend. We'll use this // to bypass any proxy logic. @@ -492,10 +620,20 @@ func (s) TestBasicAuthInGrpcNewClientWithProxy(t *testing.T) { return nil }) - // Overwrite the proxy environment and restore it after the test. - proxyEnv := os.Getenv("HTTPS_PROXY") - os.Setenv("HTTPS_PROXY", "https://"+user+":"+password+"@"+unresolvedProxyURI) - defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() + origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment + defer func() { + delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment + }() + + proxyConfig := httpproxy.Config{ + HTTPSProxy: user + ":" + password + "@" + unresolvedProxyURI, + NoProxy: "", + } + + delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { + return proxyConfig.ProxyFunc()(req.URL) + } + // Set up and update a manual resolver for proxy resolution. proxyResolver := setupDNS(t) proxyResolver.InitialState(resolver.State{ From 7ae2797a2343dc85c730550b8b31f3006088fe69 Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Fri, 20 Dec 2024 13:37:16 +0530 Subject: [PATCH 17/53] correct test environment --- .../delegatingresolver/delegatingresolver.go | 4 +- .../delegatingresolver_ext_test.go | 1041 ++++++++--------- internal/transport/http2_client.go | 2 +- internal/transport/proxy.go | 16 +- output.txt | 62 - test/proxy_test.go | 226 ++-- 6 files changed, 657 insertions(+), 694 deletions(-) delete mode 100644 output.txt diff --git a/internal/resolver/delegatingresolver/delegatingresolver.go b/internal/resolver/delegatingresolver/delegatingresolver.go index 2f54578ec94a..b7efe8107f2c 100644 --- a/internal/resolver/delegatingresolver/delegatingresolver.go +++ b/internal/resolver/delegatingresolver/delegatingresolver.go @@ -34,10 +34,10 @@ import ( var ( logger = grpclog.Component("delegating-resolver") - //HTTPSProxyFromEnvironment will be overwritten in the tests + // HTTPSProxyFromEnvironment will be overwritten in the tests HTTPSProxyFromEnvironment = http.ProxyFromEnvironment // ProxyScheme will be overwritten in tests - ProxyScheme="dns" + ProxyScheme = "dns" ) // delegatingResolver manages both target URI and proxy address resolution by diff --git a/internal/resolver/delegatingresolver/delegatingresolver_ext_test.go b/internal/resolver/delegatingresolver/delegatingresolver_ext_test.go index 9a2c7c1f6325..350cbc900289 100644 --- a/internal/resolver/delegatingresolver/delegatingresolver_ext_test.go +++ b/internal/resolver/delegatingresolver/delegatingresolver_ext_test.go @@ -16,524 +16,523 @@ * */ - package delegatingresolver_test - - import ( - "net/http" - "net/url" - "testing" - "time" - - "github.com/google/go-cmp/cmp" - "google.golang.org/grpc/internal/grpctest" - "google.golang.org/grpc/internal/proxyattributes" - "google.golang.org/grpc/internal/resolver/delegatingresolver" - "google.golang.org/grpc/internal/testutils" - "google.golang.org/grpc/resolver" - "google.golang.org/grpc/resolver/manual" - "google.golang.org/grpc/serviceconfig" - - _ "google.golang.org/grpc/resolver/dns" // To register dns resolver. - ) - - type s struct { - grpctest.Tester - } - - func Test(t *testing.T) { - grpctest.RunSubTests(t, s{}) - } - - const ( - defaultTestTimeout = 10 * time.Second - defaultTestShortTimeout = 10 * time.Millisecond - ) - - // createTestResolverClientConn initializes a [testutils.ResolverClientConn] and - // returns it along with channels for resolver state updates and errors. - func createTestResolverClientConn(t *testing.T) (*testutils.ResolverClientConn, chan resolver.State, chan error) { - stateCh := make(chan resolver.State, 1) - errCh := make(chan error, 1) - - tcc := &testutils.ResolverClientConn{ - Logger: t, - UpdateStateF: func(s resolver.State) error { stateCh <- s; return nil }, - ReportErrorF: func(err error) { errCh <- err }, - } - return tcc, stateCh, errCh - } - - // Tests the scenario where no proxy environment variables are set or proxying - // is disabled by the `NO_PROXY` environment variable. The test verifies that - // the delegating resolver creates only a target resolver and that the - // addresses returned by the delegating resolver exactly match those returned - // by the target resolver. - func (s) TestDelegatingResolverNoProxyEnvVarsSet(t *testing.T) { - hpfe := func(req *http.Request) (*url.URL, error) { return nil, nil } - originalhpfe := delegatingresolver.HTTPSProxyFromEnvironment - delegatingresolver.HTTPSProxyFromEnvironment = hpfe - defer func() { - delegatingresolver.HTTPSProxyFromEnvironment = originalhpfe - }() - - const ( - targetTestAddr = "test.com" - resolvedTargetTestAddr1 = "1.1.1.1:8080" - resolvedTargetTestAddr2 = "2.2.2.2:8080" - ) - targetResolver := manual.NewBuilderWithScheme("test") // Set up a manual resolver to control the address resolution. - target := targetResolver.Scheme() + ":///" + targetTestAddr - - // Create a delegating resolver with no proxy configuration - tcc, stateCh, _ := createTestResolverClientConn(t) - if _, err := delegatingresolver.New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, targetResolver, false); err != nil { - t.Fatalf("Failed to create delegating resolver: %v", err) - } - - // Update the manual resolver with a test address. - targetResolver.UpdateState(resolver.State{ - Addresses: []resolver.Address{ - {Addr: resolvedTargetTestAddr1}, - {Addr: resolvedTargetTestAddr2}, - }, - ServiceConfig: &serviceconfig.ParseResult{}, - }) - - // Verify that the delegating resolver outputs the same addresses, as returned - // by the target resolver. - wantState := resolver.State{ - Addresses: []resolver.Address{ - {Addr: resolvedTargetTestAddr1}, - {Addr: resolvedTargetTestAddr2}, - }, - ServiceConfig: &serviceconfig.ParseResult{}, - } - - var gotState resolver.State - select { - case gotState = <-stateCh: - case <-time.After(defaultTestTimeout): - t.Fatal("Timeout when waiting for a state update from the delegating resolver") - } - - if diff := cmp.Diff(gotState, wantState); diff != "" { - t.Fatalf("Unexpected state from delegating resolver. Diff (-got +want):\n%v", diff) - } - } - - // setupDNS registers a new manual resolver for the DNS scheme, effectively - // overwriting the previously registered DNS resolver. This allows the test to - // mock the DNS resolution for the proxy resolver. It also registers the - // original DNS resolver after the test is done. - func setupDNS(t *testing.T) *manual.Resolver { - mr := manual.NewBuilderWithScheme("dns") - - dnsResolverBuilder := resolver.Get("dns") - resolver.Register(mr) - - t.Cleanup(func() { resolver.Register(dnsResolverBuilder) }) - return mr - } - - // proxyAddressWithTargetAttribute creates a resolver.Address for the proxy, - // adding the target address as an attribute. - func proxyAddressWithTargetAttribute(proxyAddr string, targetAddr string) resolver.Address { - addr := resolver.Address{Addr: proxyAddr} - addr = proxyattributes.SetOptions(addr, proxyattributes.Options{ConnectAddr: targetAddr}) - return addr - } - - // Tests the scenario where proxy is configured and the target URI contains the - // "dns" scheme and target resolution is enabled. The test verifies that the - // addresses returned by the delegating resolver combines the addresses - // returned by the proxy resolver and the target resolver. - func (s) TestDelegatingResolverwithDNSAndProxyWithTargetResolution(t *testing.T) { - const ( - targetTestAddr = "test.com" - resolvedTargetTestAddr1 = "1.1.1.1:8080" - resolvedTargetTestAddr2 = "2.2.2.2:8080" - envProxyAddr = "proxytest.com" - resolvedProxyTestAddr1 = "11.11.11.11:7687" - ) - hpfe := func(req *http.Request) (*url.URL, error) { - if req.URL.Host == targetTestAddr { - return &url.URL{ - Scheme: "https", - Host: envProxyAddr, - }, nil - } - t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, targetTestAddr) - return nil, nil - } - originalhpfe := delegatingresolver.HTTPSProxyFromEnvironment - delegatingresolver.HTTPSProxyFromEnvironment = hpfe - defer func() { - delegatingresolver.HTTPSProxyFromEnvironment = originalhpfe - }() - - targetResolver := manual.NewBuilderWithScheme("dns") // Manual resolver to control the target resolution. - target := targetResolver.Scheme() + ":///" + targetTestAddr - proxyResolver := setupDNS(t) // Set up a manual DNS resolver to control the proxy address resolution. - - tcc, stateCh, _ := createTestResolverClientConn(t) - if _, err := delegatingresolver.New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, targetResolver, true); err != nil { - t.Fatalf("Failed to create delegating resolver: %v", err) - } - - proxyResolver.UpdateState(resolver.State{ - Addresses: []resolver.Address{{Addr: resolvedProxyTestAddr1}}, - ServiceConfig: &serviceconfig.ParseResult{}, - }) - - select { - case <-stateCh: - t.Fatalf("Delegating resolver invoked UpdateState before both the proxy and target resolvers had updated their states.") - case <-time.After(defaultTestShortTimeout): - } - - targetResolver.UpdateState(resolver.State{ - Addresses: []resolver.Address{ - {Addr: resolvedTargetTestAddr1}, - {Addr: resolvedTargetTestAddr2}, - }, - ServiceConfig: &serviceconfig.ParseResult{}, - }) - - // Verify that the delegating resolver outputs the expected address. - wantState := resolver.State{ - Addresses: []resolver.Address{ - proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, resolvedTargetTestAddr1), - proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, resolvedTargetTestAddr2), - }, - ServiceConfig: &serviceconfig.ParseResult{}, - } - var gotState resolver.State - select { - case gotState = <-stateCh: - case <-time.After(defaultTestTimeout): - t.Fatal("Timeout when waiting for a state update from the delegating resolver") - } - - if diff := cmp.Diff(gotState, wantState); diff != "" { - t.Fatalf("Unexpected state from delegating resolver. Diff (-got +want):\n%v", diff) - } - } - - // Tests the scenario where a proxy is configured, the target URI contains the - // "dns" scheme, and target resolution is disabled(default behavior). The test - // verifies that the addresses returned by the delegating resolver include the - // proxy resolver's addresses, with the unresolved target URI as an attribute - // of the proxy address. - func (s) TestDelegatingResolverwithDNSAndProxyWithNoTargetResolution(t *testing.T) { - const ( - targetTestAddr = "test.com" - envProxyAddr = "proxytest.com" - resolvedProxyTestAddr1 = "11.11.11.11:7687" - ) - hpfe := func(req *http.Request) (*url.URL, error) { - if req.URL.Host == targetTestAddr { - return &url.URL{ - Scheme: "https", - Host: envProxyAddr, - }, nil - } - t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, targetTestAddr) - return nil, nil - } - originalhpfe := delegatingresolver.HTTPSProxyFromEnvironment - delegatingresolver.HTTPSProxyFromEnvironment = hpfe - defer func() { - delegatingresolver.HTTPSProxyFromEnvironment = originalhpfe - }() - - targetResolver := manual.NewBuilderWithScheme("dns") - target := targetResolver.Scheme() + ":///" + targetTestAddr - proxyResolver := setupDNS(t) // Set up a manual DNS resolver to control the proxy address resolution. - - tcc, stateCh, _ := createTestResolverClientConn(t) - if _, err := delegatingresolver.New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, targetResolver, false); err != nil { - t.Fatalf("Failed to create delegating resolver: %v", err) - } - - proxyResolver.UpdateState(resolver.State{ - Addresses: []resolver.Address{ - {Addr: resolvedProxyTestAddr1}, - }, - }) - - wantState := resolver.State{ - Addresses: []resolver.Address{proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, targetTestAddr)}, - Endpoints: []resolver.Endpoint{{Addresses: []resolver.Address{proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, targetTestAddr)}}}, - } - - var gotState resolver.State - select { - case gotState = <-stateCh: - case <-time.After(defaultTestTimeout): - t.Fatal("Timeout when waiting for a state update from the delegating resolver") - } - - if diff := cmp.Diff(gotState, wantState); diff != "" { - t.Fatalf("Unexpected state from delegating resolver. Diff (-got +want):\n%v", diff) - } - } - - // Tests the scenario where a proxy is configured, and the target URI scheme is - // not "dns". The test verifies that the addresses returned by the delegating - // resolver include the resolved proxy address and the custom resolved target - // address as attributes of the proxy address. - func (s) TestDelegatingResolverwithCustomResolverAndProxy(t *testing.T) { - const ( - targetTestAddr = "test.com" - resolvedTargetTestAddr1 = "1.1.1.1:8080" - resolvedTargetTestAddr2 = "2.2.2.2:8080" - envProxyAddr = "proxytest.com" - resolvedProxyTestAddr1 = "11.11.11.11:7687" - ) - hpfe := func(req *http.Request) (*url.URL, error) { - if req.URL.Host == targetTestAddr { - return &url.URL{ - Scheme: "https", - Host: envProxyAddr, - }, nil - } - t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, targetTestAddr) - return nil, nil - } - originalhpfe := delegatingresolver.HTTPSProxyFromEnvironment - delegatingresolver.HTTPSProxyFromEnvironment = hpfe - defer func() { - delegatingresolver.HTTPSProxyFromEnvironment = originalhpfe - }() - - targetResolver := manual.NewBuilderWithScheme("test") // Manual resolver to control the target resolution. - target := targetResolver.Scheme() + ":///" + targetTestAddr - proxyResolver := setupDNS(t) // Set up a manual DNS resolver to control the proxy address resolution. - - tcc, stateCh, _ := createTestResolverClientConn(t) - if _, err := delegatingresolver.New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, targetResolver, false); err != nil { - t.Fatalf("Failed to create delegating resolver: %v", err) - } - - proxyResolver.UpdateState(resolver.State{ - Addresses: []resolver.Address{{Addr: resolvedProxyTestAddr1}}, - ServiceConfig: &serviceconfig.ParseResult{}, - }) - - select { - case <-stateCh: - t.Fatalf("Delegating resolver invoked UpdateState before both the proxy and target resolvers had updated their states.") - case <-time.After(defaultTestShortTimeout): - } - - targetResolver.UpdateState(resolver.State{ - Addresses: []resolver.Address{ - {Addr: resolvedTargetTestAddr1}, - {Addr: resolvedTargetTestAddr2}, - }, - ServiceConfig: &serviceconfig.ParseResult{}, - }) - - wantState := resolver.State{ - Addresses: []resolver.Address{ - proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, resolvedTargetTestAddr1), - proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, resolvedTargetTestAddr2), - }, - ServiceConfig: &serviceconfig.ParseResult{}, - } - var gotState resolver.State - select { - case gotState = <-stateCh: - case <-time.After(defaultTestTimeout): - t.Fatal("Timeout when waiting for a state update from the delegating resolver") - } - - if diff := cmp.Diff(gotState, wantState); diff != "" { - t.Fatalf("Unexpected state from delegating resolver. Diff (-got +want):\n%v", diff) - } - } - - // Tests the scenario where a proxy is configured, the target URI scheme is not - // "dns," and both the proxy and target resolvers return endpoints. The test - // verifies that the delegating resolver combines resolved proxy and target - // addresses correctly, returning endpoints with the proxy address populated - // and the target address included as an attribute of the proxy address for - // each combination of proxy and target endpoints. - func (s) TestDelegatingResolverForEndpointsWithProxy(t *testing.T) { - const ( - targetTestAddr = "test.com" - resolvedTargetTestAddr1 = "1.1.1.1:8080" - resolvedTargetTestAddr2 = "2.2.2.2:8080" - resolvedTargetTestAddr3 = "3.3.3.3:8080" - resolvedTargetTestAddr4 = "4.4.4.4:8080" - envProxyAddr = "proxytest.com" - resolvedProxyTestAddr1 = "11.11.11.11:7687" - resolvedProxyTestAddr2 = "22.22.22.22:7687" - ) - hpfe := func(req *http.Request) (*url.URL, error) { - if req.URL.Host == targetTestAddr { - return &url.URL{ - Scheme: "https", - Host: envProxyAddr, - }, nil - } - t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, targetTestAddr) - return nil, nil - } - originalhpfe := delegatingresolver.HTTPSProxyFromEnvironment - delegatingresolver.HTTPSProxyFromEnvironment = hpfe - defer func() { - delegatingresolver.HTTPSProxyFromEnvironment = originalhpfe - }() - - targetResolver := manual.NewBuilderWithScheme("test") // Manual resolver to control the target resolution. - target := targetResolver.Scheme() + ":///" + targetTestAddr - proxyResolver := setupDNS(t) // Set up a manual DNS resolver to control the proxy address resolution. - - tcc, stateCh, _ := createTestResolverClientConn(t) - if _, err := delegatingresolver.New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, targetResolver, false); err != nil { - t.Fatalf("Failed to create delegating resolver: %v", err) - } - - proxyResolver.UpdateState(resolver.State{ - Endpoints: []resolver.Endpoint{ - {Addresses: []resolver.Address{{Addr: resolvedProxyTestAddr1}}}, - {Addresses: []resolver.Address{{Addr: resolvedProxyTestAddr2}}}, - }, - ServiceConfig: &serviceconfig.ParseResult{}, - }) - - select { - case <-stateCh: - t.Fatalf("Delegating resolver invoked UpdateState before both the proxy and target resolvers had updated their states.") - case <-time.After(defaultTestShortTimeout): - } - targetResolver.UpdateState(resolver.State{ - Endpoints: []resolver.Endpoint{ - { - Addresses: []resolver.Address{ - {Addr: resolvedTargetTestAddr1}, - {Addr: resolvedTargetTestAddr2}}, - }, - { - Addresses: []resolver.Address{ - {Addr: resolvedTargetTestAddr3}, - {Addr: resolvedTargetTestAddr4}}, - }, - }, - ServiceConfig: &serviceconfig.ParseResult{}, - }) - - wantState := resolver.State{ - Endpoints: []resolver.Endpoint{ - { - Addresses: []resolver.Address{ - proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, resolvedTargetTestAddr1), - proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, resolvedTargetTestAddr2), - proxyAddressWithTargetAttribute(resolvedProxyTestAddr2, resolvedTargetTestAddr1), - proxyAddressWithTargetAttribute(resolvedProxyTestAddr2, resolvedTargetTestAddr2), - }, - }, - { - Addresses: []resolver.Address{ - proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, resolvedTargetTestAddr3), - proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, resolvedTargetTestAddr4), - proxyAddressWithTargetAttribute(resolvedProxyTestAddr2, resolvedTargetTestAddr3), - proxyAddressWithTargetAttribute(resolvedProxyTestAddr2, resolvedTargetTestAddr4), - }, - }, - }, - ServiceConfig: &serviceconfig.ParseResult{}, - } - var gotState resolver.State - select { - case gotState = <-stateCh: - case <-time.After(defaultTestTimeout): - t.Fatal("Timeout when waiting for a state update from the delegating resolver") - } - - if diff := cmp.Diff(gotState, wantState); diff != "" { - t.Fatalf("Unexpected state from delegating resolver. Diff (-got +want):\n%v", diff) - } - } - - // Tests the scenario where a proxy is configured, the target URI scheme is not - // "dns," and both the proxy and target resolvers return multiple addresses. - // The test verifies that the delegating resolver combines unresolved proxy - // host and target addresses correctly, returning addresses with the proxy host - // populated and the target address included as an attribute. - func (s) TestDelegatingResolverForMutipleProxyAddress(t *testing.T) { - const ( - targetTestAddr = "test.com" - resolvedTargetTestAddr1 = "1.1.1.1:8080" - resolvedTargetTestAddr2 = "2.2.2.2:8080" - envProxyAddr = "proxytest.com" - resolvedProxyTestAddr1 = "11.11.11.11:7687" - resolvedProxyTestAddr2 = "22.22.22.22:7687" - ) - hpfe := func(req *http.Request) (*url.URL, error) { - if req.URL.Host == targetTestAddr { - return &url.URL{ - Scheme: "https", - Host: envProxyAddr, - }, nil - } - t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, targetTestAddr) - return nil, nil - } - originalhpfe := delegatingresolver.HTTPSProxyFromEnvironment - delegatingresolver.HTTPSProxyFromEnvironment = hpfe - defer func() { - delegatingresolver.HTTPSProxyFromEnvironment = originalhpfe - }() - - targetResolver := manual.NewBuilderWithScheme("test") // Manual resolver to control the target resolution. - target := targetResolver.Scheme() + ":///" + targetTestAddr - proxyResolver := setupDNS(t) // Set up a manual DNS resolver to control the proxy address resolution. - - tcc, stateCh, _ := createTestResolverClientConn(t) - if _, err := delegatingresolver.New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, targetResolver, false); err != nil { - t.Fatalf("Failed to create delegating resolver: %v", err) - } - - proxyResolver.UpdateState(resolver.State{ - Addresses: []resolver.Address{ - {Addr: resolvedProxyTestAddr1}, - {Addr: resolvedProxyTestAddr2}, - }, - ServiceConfig: &serviceconfig.ParseResult{}, - }) - - select { - case <-stateCh: - t.Fatalf("Delegating resolver invoked UpdateState before both the proxy and target resolvers had updated their states.") - case <-time.After(defaultTestShortTimeout): - } - - targetResolver.UpdateState(resolver.State{ - Addresses: []resolver.Address{ - {Addr: resolvedTargetTestAddr1}, - {Addr: resolvedTargetTestAddr2}, - }, - ServiceConfig: &serviceconfig.ParseResult{}, - }) - - wantState := resolver.State{ - Addresses: []resolver.Address{ - proxyAddressWithTargetAttribute(envProxyAddr, resolvedTargetTestAddr1), - proxyAddressWithTargetAttribute(envProxyAddr, resolvedTargetTestAddr2), - }, - ServiceConfig: &serviceconfig.ParseResult{}, - } - var gotState resolver.State - select { - case gotState = <-stateCh: - case <-time.After(defaultTestTimeout): - t.Fatal("Timeout when waiting for a state update from the delegating resolver") - } - - if diff := cmp.Diff(gotState, wantState); diff != "" { - t.Fatalf("Unexpected state from delegating resolver. Diff (-got +want):\n%v", diff) - } - } - \ No newline at end of file +package delegatingresolver_test + +import ( + "net/http" + "net/url" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "google.golang.org/grpc/internal/grpctest" + "google.golang.org/grpc/internal/proxyattributes" + "google.golang.org/grpc/internal/resolver/delegatingresolver" + "google.golang.org/grpc/internal/testutils" + "google.golang.org/grpc/resolver" + "google.golang.org/grpc/resolver/manual" + "google.golang.org/grpc/serviceconfig" + + _ "google.golang.org/grpc/resolver/dns" // To register dns resolver. +) + +type s struct { + grpctest.Tester +} + +func Test(t *testing.T) { + grpctest.RunSubTests(t, s{}) +} + +const ( + defaultTestTimeout = 10 * time.Second + defaultTestShortTimeout = 10 * time.Millisecond +) + +// createTestResolverClientConn initializes a [testutils.ResolverClientConn] and +// returns it along with channels for resolver state updates and errors. +func createTestResolverClientConn(t *testing.T) (*testutils.ResolverClientConn, chan resolver.State, chan error) { + stateCh := make(chan resolver.State, 1) + errCh := make(chan error, 1) + + tcc := &testutils.ResolverClientConn{ + Logger: t, + UpdateStateF: func(s resolver.State) error { stateCh <- s; return nil }, + ReportErrorF: func(err error) { errCh <- err }, + } + return tcc, stateCh, errCh +} + +// Tests the scenario where no proxy environment variables are set or proxying +// is disabled by the `NO_PROXY` environment variable. The test verifies that +// the delegating resolver creates only a target resolver and that the +// addresses returned by the delegating resolver exactly match those returned +// by the target resolver. +func (s) TestDelegatingResolverNoProxyEnvVarsSet(t *testing.T) { + hpfe := func(req *http.Request) (*url.URL, error) { return nil, nil } + originalhpfe := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = hpfe + defer func() { + delegatingresolver.HTTPSProxyFromEnvironment = originalhpfe + }() + + const ( + targetTestAddr = "test.com" + resolvedTargetTestAddr1 = "1.1.1.1:8080" + resolvedTargetTestAddr2 = "2.2.2.2:8080" + ) + targetResolver := manual.NewBuilderWithScheme("test") // Set up a manual resolver to control the address resolution. + target := targetResolver.Scheme() + ":///" + targetTestAddr + + // Create a delegating resolver with no proxy configuration + tcc, stateCh, _ := createTestResolverClientConn(t) + if _, err := delegatingresolver.New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, targetResolver, false); err != nil { + t.Fatalf("Failed to create delegating resolver: %v", err) + } + + // Update the manual resolver with a test address. + targetResolver.UpdateState(resolver.State{ + Addresses: []resolver.Address{ + {Addr: resolvedTargetTestAddr1}, + {Addr: resolvedTargetTestAddr2}, + }, + ServiceConfig: &serviceconfig.ParseResult{}, + }) + + // Verify that the delegating resolver outputs the same addresses, as returned + // by the target resolver. + wantState := resolver.State{ + Addresses: []resolver.Address{ + {Addr: resolvedTargetTestAddr1}, + {Addr: resolvedTargetTestAddr2}, + }, + ServiceConfig: &serviceconfig.ParseResult{}, + } + + var gotState resolver.State + select { + case gotState = <-stateCh: + case <-time.After(defaultTestTimeout): + t.Fatal("Timeout when waiting for a state update from the delegating resolver") + } + + if diff := cmp.Diff(gotState, wantState); diff != "" { + t.Fatalf("Unexpected state from delegating resolver. Diff (-got +want):\n%v", diff) + } +} + +// setupDNS registers a new manual resolver for the DNS scheme, effectively +// overwriting the previously registered DNS resolver. This allows the test to +// mock the DNS resolution for the proxy resolver. It also registers the +// original DNS resolver after the test is done. +func setupDNS(t *testing.T) *manual.Resolver { + mr := manual.NewBuilderWithScheme("dns") + + dnsResolverBuilder := resolver.Get("dns") + resolver.Register(mr) + + t.Cleanup(func() { resolver.Register(dnsResolverBuilder) }) + return mr +} + +// proxyAddressWithTargetAttribute creates a resolver.Address for the proxy, +// adding the target address as an attribute. +func proxyAddressWithTargetAttribute(proxyAddr string, targetAddr string) resolver.Address { + addr := resolver.Address{Addr: proxyAddr} + addr = proxyattributes.SetOptions(addr, proxyattributes.Options{ConnectAddr: targetAddr}) + return addr +} + +// Tests the scenario where proxy is configured and the target URI contains the +// "dns" scheme and target resolution is enabled. The test verifies that the +// addresses returned by the delegating resolver combines the addresses +// returned by the proxy resolver and the target resolver. +func (s) TestDelegatingResolverwithDNSAndProxyWithTargetResolution(t *testing.T) { + const ( + targetTestAddr = "test.com" + resolvedTargetTestAddr1 = "1.1.1.1:8080" + resolvedTargetTestAddr2 = "2.2.2.2:8080" + envProxyAddr = "proxytest.com" + resolvedProxyTestAddr1 = "11.11.11.11:7687" + ) + hpfe := func(req *http.Request) (*url.URL, error) { + if req.URL.Host == targetTestAddr { + return &url.URL{ + Scheme: "https", + Host: envProxyAddr, + }, nil + } + t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, targetTestAddr) + return nil, nil + } + originalhpfe := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = hpfe + defer func() { + delegatingresolver.HTTPSProxyFromEnvironment = originalhpfe + }() + + targetResolver := manual.NewBuilderWithScheme("dns") // Manual resolver to control the target resolution. + target := targetResolver.Scheme() + ":///" + targetTestAddr + proxyResolver := setupDNS(t) // Set up a manual DNS resolver to control the proxy address resolution. + + tcc, stateCh, _ := createTestResolverClientConn(t) + if _, err := delegatingresolver.New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, targetResolver, true); err != nil { + t.Fatalf("Failed to create delegating resolver: %v", err) + } + + proxyResolver.UpdateState(resolver.State{ + Addresses: []resolver.Address{{Addr: resolvedProxyTestAddr1}}, + ServiceConfig: &serviceconfig.ParseResult{}, + }) + + select { + case <-stateCh: + t.Fatalf("Delegating resolver invoked UpdateState before both the proxy and target resolvers had updated their states.") + case <-time.After(defaultTestShortTimeout): + } + + targetResolver.UpdateState(resolver.State{ + Addresses: []resolver.Address{ + {Addr: resolvedTargetTestAddr1}, + {Addr: resolvedTargetTestAddr2}, + }, + ServiceConfig: &serviceconfig.ParseResult{}, + }) + + // Verify that the delegating resolver outputs the expected address. + wantState := resolver.State{ + Addresses: []resolver.Address{ + proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, resolvedTargetTestAddr1), + proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, resolvedTargetTestAddr2), + }, + ServiceConfig: &serviceconfig.ParseResult{}, + } + var gotState resolver.State + select { + case gotState = <-stateCh: + case <-time.After(defaultTestTimeout): + t.Fatal("Timeout when waiting for a state update from the delegating resolver") + } + + if diff := cmp.Diff(gotState, wantState); diff != "" { + t.Fatalf("Unexpected state from delegating resolver. Diff (-got +want):\n%v", diff) + } +} + +// Tests the scenario where a proxy is configured, the target URI contains the +// "dns" scheme, and target resolution is disabled(default behavior). The test +// verifies that the addresses returned by the delegating resolver include the +// proxy resolver's addresses, with the unresolved target URI as an attribute +// of the proxy address. +func (s) TestDelegatingResolverwithDNSAndProxyWithNoTargetResolution(t *testing.T) { + const ( + targetTestAddr = "test.com" + envProxyAddr = "proxytest.com" + resolvedProxyTestAddr1 = "11.11.11.11:7687" + ) + hpfe := func(req *http.Request) (*url.URL, error) { + if req.URL.Host == targetTestAddr { + return &url.URL{ + Scheme: "https", + Host: envProxyAddr, + }, nil + } + t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, targetTestAddr) + return nil, nil + } + originalhpfe := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = hpfe + defer func() { + delegatingresolver.HTTPSProxyFromEnvironment = originalhpfe + }() + + targetResolver := manual.NewBuilderWithScheme("dns") + target := targetResolver.Scheme() + ":///" + targetTestAddr + proxyResolver := setupDNS(t) // Set up a manual DNS resolver to control the proxy address resolution. + + tcc, stateCh, _ := createTestResolverClientConn(t) + if _, err := delegatingresolver.New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, targetResolver, false); err != nil { + t.Fatalf("Failed to create delegating resolver: %v", err) + } + + proxyResolver.UpdateState(resolver.State{ + Addresses: []resolver.Address{ + {Addr: resolvedProxyTestAddr1}, + }, + }) + + wantState := resolver.State{ + Addresses: []resolver.Address{proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, targetTestAddr)}, + Endpoints: []resolver.Endpoint{{Addresses: []resolver.Address{proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, targetTestAddr)}}}, + } + + var gotState resolver.State + select { + case gotState = <-stateCh: + case <-time.After(defaultTestTimeout): + t.Fatal("Timeout when waiting for a state update from the delegating resolver") + } + + if diff := cmp.Diff(gotState, wantState); diff != "" { + t.Fatalf("Unexpected state from delegating resolver. Diff (-got +want):\n%v", diff) + } +} + +// Tests the scenario where a proxy is configured, and the target URI scheme is +// not "dns". The test verifies that the addresses returned by the delegating +// resolver include the resolved proxy address and the custom resolved target +// address as attributes of the proxy address. +func (s) TestDelegatingResolverwithCustomResolverAndProxy(t *testing.T) { + const ( + targetTestAddr = "test.com" + resolvedTargetTestAddr1 = "1.1.1.1:8080" + resolvedTargetTestAddr2 = "2.2.2.2:8080" + envProxyAddr = "proxytest.com" + resolvedProxyTestAddr1 = "11.11.11.11:7687" + ) + hpfe := func(req *http.Request) (*url.URL, error) { + if req.URL.Host == targetTestAddr { + return &url.URL{ + Scheme: "https", + Host: envProxyAddr, + }, nil + } + t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, targetTestAddr) + return nil, nil + } + originalhpfe := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = hpfe + defer func() { + delegatingresolver.HTTPSProxyFromEnvironment = originalhpfe + }() + + targetResolver := manual.NewBuilderWithScheme("test") // Manual resolver to control the target resolution. + target := targetResolver.Scheme() + ":///" + targetTestAddr + proxyResolver := setupDNS(t) // Set up a manual DNS resolver to control the proxy address resolution. + + tcc, stateCh, _ := createTestResolverClientConn(t) + if _, err := delegatingresolver.New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, targetResolver, false); err != nil { + t.Fatalf("Failed to create delegating resolver: %v", err) + } + + proxyResolver.UpdateState(resolver.State{ + Addresses: []resolver.Address{{Addr: resolvedProxyTestAddr1}}, + ServiceConfig: &serviceconfig.ParseResult{}, + }) + + select { + case <-stateCh: + t.Fatalf("Delegating resolver invoked UpdateState before both the proxy and target resolvers had updated their states.") + case <-time.After(defaultTestShortTimeout): + } + + targetResolver.UpdateState(resolver.State{ + Addresses: []resolver.Address{ + {Addr: resolvedTargetTestAddr1}, + {Addr: resolvedTargetTestAddr2}, + }, + ServiceConfig: &serviceconfig.ParseResult{}, + }) + + wantState := resolver.State{ + Addresses: []resolver.Address{ + proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, resolvedTargetTestAddr1), + proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, resolvedTargetTestAddr2), + }, + ServiceConfig: &serviceconfig.ParseResult{}, + } + var gotState resolver.State + select { + case gotState = <-stateCh: + case <-time.After(defaultTestTimeout): + t.Fatal("Timeout when waiting for a state update from the delegating resolver") + } + + if diff := cmp.Diff(gotState, wantState); diff != "" { + t.Fatalf("Unexpected state from delegating resolver. Diff (-got +want):\n%v", diff) + } +} + +// Tests the scenario where a proxy is configured, the target URI scheme is not +// "dns," and both the proxy and target resolvers return endpoints. The test +// verifies that the delegating resolver combines resolved proxy and target +// addresses correctly, returning endpoints with the proxy address populated +// and the target address included as an attribute of the proxy address for +// each combination of proxy and target endpoints. +func (s) TestDelegatingResolverForEndpointsWithProxy(t *testing.T) { + const ( + targetTestAddr = "test.com" + resolvedTargetTestAddr1 = "1.1.1.1:8080" + resolvedTargetTestAddr2 = "2.2.2.2:8080" + resolvedTargetTestAddr3 = "3.3.3.3:8080" + resolvedTargetTestAddr4 = "4.4.4.4:8080" + envProxyAddr = "proxytest.com" + resolvedProxyTestAddr1 = "11.11.11.11:7687" + resolvedProxyTestAddr2 = "22.22.22.22:7687" + ) + hpfe := func(req *http.Request) (*url.URL, error) { + if req.URL.Host == targetTestAddr { + return &url.URL{ + Scheme: "https", + Host: envProxyAddr, + }, nil + } + t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, targetTestAddr) + return nil, nil + } + originalhpfe := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = hpfe + defer func() { + delegatingresolver.HTTPSProxyFromEnvironment = originalhpfe + }() + + targetResolver := manual.NewBuilderWithScheme("test") // Manual resolver to control the target resolution. + target := targetResolver.Scheme() + ":///" + targetTestAddr + proxyResolver := setupDNS(t) // Set up a manual DNS resolver to control the proxy address resolution. + + tcc, stateCh, _ := createTestResolverClientConn(t) + if _, err := delegatingresolver.New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, targetResolver, false); err != nil { + t.Fatalf("Failed to create delegating resolver: %v", err) + } + + proxyResolver.UpdateState(resolver.State{ + Endpoints: []resolver.Endpoint{ + {Addresses: []resolver.Address{{Addr: resolvedProxyTestAddr1}}}, + {Addresses: []resolver.Address{{Addr: resolvedProxyTestAddr2}}}, + }, + ServiceConfig: &serviceconfig.ParseResult{}, + }) + + select { + case <-stateCh: + t.Fatalf("Delegating resolver invoked UpdateState before both the proxy and target resolvers had updated their states.") + case <-time.After(defaultTestShortTimeout): + } + targetResolver.UpdateState(resolver.State{ + Endpoints: []resolver.Endpoint{ + { + Addresses: []resolver.Address{ + {Addr: resolvedTargetTestAddr1}, + {Addr: resolvedTargetTestAddr2}}, + }, + { + Addresses: []resolver.Address{ + {Addr: resolvedTargetTestAddr3}, + {Addr: resolvedTargetTestAddr4}}, + }, + }, + ServiceConfig: &serviceconfig.ParseResult{}, + }) + + wantState := resolver.State{ + Endpoints: []resolver.Endpoint{ + { + Addresses: []resolver.Address{ + proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, resolvedTargetTestAddr1), + proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, resolvedTargetTestAddr2), + proxyAddressWithTargetAttribute(resolvedProxyTestAddr2, resolvedTargetTestAddr1), + proxyAddressWithTargetAttribute(resolvedProxyTestAddr2, resolvedTargetTestAddr2), + }, + }, + { + Addresses: []resolver.Address{ + proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, resolvedTargetTestAddr3), + proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, resolvedTargetTestAddr4), + proxyAddressWithTargetAttribute(resolvedProxyTestAddr2, resolvedTargetTestAddr3), + proxyAddressWithTargetAttribute(resolvedProxyTestAddr2, resolvedTargetTestAddr4), + }, + }, + }, + ServiceConfig: &serviceconfig.ParseResult{}, + } + var gotState resolver.State + select { + case gotState = <-stateCh: + case <-time.After(defaultTestTimeout): + t.Fatal("Timeout when waiting for a state update from the delegating resolver") + } + + if diff := cmp.Diff(gotState, wantState); diff != "" { + t.Fatalf("Unexpected state from delegating resolver. Diff (-got +want):\n%v", diff) + } +} + +// Tests the scenario where a proxy is configured, the target URI scheme is not +// "dns," and both the proxy and target resolvers return multiple addresses. +// The test verifies that the delegating resolver combines unresolved proxy +// host and target addresses correctly, returning addresses with the proxy host +// populated and the target address included as an attribute. +func (s) TestDelegatingResolverForMutipleProxyAddress(t *testing.T) { + const ( + targetTestAddr = "test.com" + resolvedTargetTestAddr1 = "1.1.1.1:8080" + resolvedTargetTestAddr2 = "2.2.2.2:8080" + envProxyAddr = "proxytest.com" + resolvedProxyTestAddr1 = "11.11.11.11:7687" + resolvedProxyTestAddr2 = "22.22.22.22:7687" + ) + hpfe := func(req *http.Request) (*url.URL, error) { + if req.URL.Host == targetTestAddr { + return &url.URL{ + Scheme: "https", + Host: envProxyAddr, + }, nil + } + t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, targetTestAddr) + return nil, nil + } + originalhpfe := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = hpfe + defer func() { + delegatingresolver.HTTPSProxyFromEnvironment = originalhpfe + }() + + targetResolver := manual.NewBuilderWithScheme("test") // Manual resolver to control the target resolution. + target := targetResolver.Scheme() + ":///" + targetTestAddr + proxyResolver := setupDNS(t) // Set up a manual DNS resolver to control the proxy address resolution. + + tcc, stateCh, _ := createTestResolverClientConn(t) + if _, err := delegatingresolver.New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, targetResolver, false); err != nil { + t.Fatalf("Failed to create delegating resolver: %v", err) + } + + proxyResolver.UpdateState(resolver.State{ + Addresses: []resolver.Address{ + {Addr: resolvedProxyTestAddr1}, + {Addr: resolvedProxyTestAddr2}, + }, + ServiceConfig: &serviceconfig.ParseResult{}, + }) + + select { + case <-stateCh: + t.Fatalf("Delegating resolver invoked UpdateState before both the proxy and target resolvers had updated their states.") + case <-time.After(defaultTestShortTimeout): + } + + targetResolver.UpdateState(resolver.State{ + Addresses: []resolver.Address{ + {Addr: resolvedTargetTestAddr1}, + {Addr: resolvedTargetTestAddr2}, + }, + ServiceConfig: &serviceconfig.ParseResult{}, + }) + + wantState := resolver.State{ + Addresses: []resolver.Address{ + proxyAddressWithTargetAttribute(envProxyAddr, resolvedTargetTestAddr1), + proxyAddressWithTargetAttribute(envProxyAddr, resolvedTargetTestAddr2), + }, + ServiceConfig: &serviceconfig.ParseResult{}, + } + var gotState resolver.State + select { + case gotState = <-stateCh: + case <-time.After(defaultTestTimeout): + t.Fatal("Timeout when waiting for a state update from the delegating resolver") + } + + if diff := cmp.Diff(gotState, wantState); diff != "" { + t.Fatalf("Unexpected state from delegating resolver. Diff (-got +want):\n%v", diff) + } +} diff --git a/internal/transport/http2_client.go b/internal/transport/http2_client.go index 3ef04a9461ec..4938ef8ffa04 100644 --- a/internal/transport/http2_client.go +++ b/internal/transport/http2_client.go @@ -159,7 +159,7 @@ func dial(ctx context.Context, fn func(context.Context, string) (net.Conn, error // A non-empty ConnectAddr attribute indicates that a proxy is configured, // so initiate a proxy dial. - if _,present:=proxyattributes.ExtractOptions(addr);present ==true { + if _, present := proxyattributes.ExtractOptions(addr); present { return proxyDial(ctx, addr, grpcUA) } networkType, ok := networktype.Get(addr) diff --git a/internal/transport/proxy.go b/internal/transport/proxy.go index 7aa142694dc5..e0751d220ebc 100644 --- a/internal/transport/proxy.go +++ b/internal/transport/proxy.go @@ -67,15 +67,15 @@ func doHTTPConnectHandshake(ctx context.Context, conn net.Conn, addr resolver.Ad URL: &url.URL{Host: a.ConnectAddr}, Header: map[string][]string{"User-Agent": {grpcUA}}, } - user := a.User - u := user.Username() - p, pSet := user.Password() - if !pSet { - logger.Warningf("password not set for basic authentication for proxy dialing") - } - - req.Header.Add(proxyAuthHeaderKey, "Basic "+basicAuth(u, p)) + if user := a.User; user != (url.Userinfo{}) { + u := user.Username() + p, pSet := user.Password() + if !pSet { + logger.Warningf("password not set for basic authentication for proxy dialing") + } + req.Header.Add(proxyAuthHeaderKey, "Basic "+basicAuth(u, p)) + } if err := sendHTTPRequest(ctx, req, conn); err != nil { return nil, fmt.Errorf("failed to write the HTTP request: %v", err) } diff --git a/output.txt b/output.txt deleted file mode 100644 index 0efae30d662e..000000000000 --- a/output.txt +++ /dev/null @@ -1,62 +0,0 @@ -=== RUN Test -=== RUN Test/GRPCDialWithProxy - tlogger.go:116: INFO server.go:685 [core] [Server #1]Server created (t=+2.616834ms) - proxy_test.go:52: Started TestService backend at: "127.0.0.1:58638" - tlogger.go:116: INFO server.go:881 [core] [Server #1 ListenSocket #2]ListenSocket created (t=+2.91925ms) - tlogger.go:116: INFO clientconn.go:1654 [core] original dial target is: "example.com" (t=+3.284125ms) - tlogger.go:116: INFO clientconn.go:314 [core] [Channel #3]Channel created (t=+3.36ms) - tlogger.go:116: INFO clientconn.go:193 [core] [Channel #3]parsed dial target is: resolver.Target{URL:url.URL{Scheme:"passthrough", Opaque:"", User:(*url.Userinfo)(nil), Host:"", Path:"/example.com", RawPath:"", OmitHost:false, ForceQuery:false, RawQuery:"", Fragment:"", RawFragment:""}} (t=+3.41775ms) - tlogger.go:116: INFO clientconn.go:194 [core] [Channel #3]Channel authority set to "example.com" (t=+3.448084ms) - tlogger.go:116: INFO resolver_wrapper.go:211 [core] [Channel #3]Resolver state updated: { - "Addresses": [ - { - "Addr": "127.0.0.1:58639", - "ServerName": "", - "Attributes": { - "\u003c%!p(proxyattributes.keyType=grpc.resolver.delegatingresolver.proxyOptions)\u003e": "\u003c%!p(proxyattributes.Options={{ false} example.com})\u003e" - }, - "BalancerAttributes": null, - "Metadata": null - } - ], - "Endpoints": [ - { - "Addresses": [ - { - "Addr": "127.0.0.1:58639", - "ServerName": "", - "Attributes": { - "\u003c%!p(proxyattributes.keyType=grpc.resolver.delegatingresolver.proxyOptions)\u003e": "\u003c%!p(proxyattributes.Options={{ false} example.com})\u003e" - }, - "BalancerAttributes": null, - "Metadata": null - } - ], - "Attributes": null - } - ], - "ServiceConfig": null, - "Attributes": null - } (resolver returned new addresses) (t=+4.045584ms) - tlogger.go:116: INFO balancer_wrapper.go:109 [core] [Channel #3]Channel switches to new LB policy "pick_first" (t=+4.126709ms) - tlogger.go:116: INFO clientconn.go:838 [core] [Channel #3 SubChannel #4]Subchannel created (t=+4.180167ms) - tlogger.go:116: INFO clientconn.go:544 [core] [Channel #3]Channel Connectivity change to CONNECTING (t=+4.218167ms) - tlogger.go:116: INFO clientconn.go:314 [core] [Channel #3]Channel exiting idle mode (t=+4.2565ms) - tlogger.go:116: INFO clientconn.go:1199 [core] [Channel #3 SubChannel #4]Subchannel Connectivity change to CONNECTING (t=+4.288584ms) - tlogger.go:116: INFO clientconn.go:1319 [core] [Channel #3 SubChannel #4]Subchannel picks a new address "127.0.0.1:58639" to connect (t=+4.357875ms) - tlogger.go:116: WARNING proxy.go:74 [transport] password not set for basic authentication for proxy dialing (t=+5.617084ms) - tlogger.go:116: INFO syscall_nonlinux.go:39 [core] CPU time info is unavailable on non-linux environments. (t=+7.15175ms) - tlogger.go:116: INFO clientconn.go:1199 [core] [Channel #3 SubChannel #4]Subchannel Connectivity change to READY (t=+7.375292ms) - tlogger.go:116: INFO clientconn.go:544 [core] [Channel #3]Channel Connectivity change to READY (t=+7.509584ms) - proxy_test.go:149: proxy server succeeded - tlogger.go:116: INFO clientconn.go:544 [core] [Channel #3]Channel Connectivity change to SHUTDOWN (t=+8.258542ms) - tlogger.go:116: INFO resolver_wrapper.go:111 [core] [Channel #3]Closing the name resolver (t=+8.295792ms) - tlogger.go:116: INFO balancer_wrapper.go:147 [core] [Channel #3]ccBalancerWrapper: closing (t=+8.330042ms) - tlogger.go:116: INFO clientconn.go:1199 [core] [Channel #3 SubChannel #4]Subchannel Connectivity change to SHUTDOWN (t=+8.4035ms) - tlogger.go:116: INFO clientconn.go:1527 [core] [Channel #3 SubChannel #4]Subchannel deleted (t=+8.448125ms) - tlogger.go:116: INFO clientconn.go:314 [core] [Channel #3]Channel deleted (t=+8.58825ms) - tlogger.go:116: INFO server.go:817 [core] [Server #1 ListenSocket #2]ListenSocket deleted (t=+8.753375ms) ---- PASS: Test (0.02s) - --- PASS: Test/GRPCDialWithProxy (0.01s) -PASS -ok google.golang.org/grpc/test (cached) diff --git a/test/proxy_test.go b/test/proxy_test.go index 659d0c8ebc3a..c2459db98ec0 100644 --- a/test/proxy_test.go +++ b/test/proxy_test.go @@ -25,6 +25,7 @@ import ( "net" "net/http" "net/url" + "os" "testing" "golang.org/x/net/http/httpproxy" @@ -115,40 +116,25 @@ func (s) TestGRPCDialWithProxy(t *testing.T) { backendAddr := createAndStartBackendServer(t) pLis, errCh, doneCh, _ := setupProxy(t, backendAddr, false, requestCheck(unresolvedTargetURI)) - // hpfe := func(req *http.Request) (*url.URL, error) { - // if req.URL.Host == unresolvedTargetURI { - // return &url.URL{ - // Scheme: "https", - // Host: pLis.Addr().String(), - // }, nil - // } - // return nil, nil - // } - // orighpfe := delegatingresolver.HTTPSProxyFromEnvironment - // delegatingresolver.HTTPSProxyFromEnvironment = hpfe - // defer func() { - // delegatingresolver.HTTPSProxyFromEnvironment = orighpfe - // }() - // Overwrite the proxy environment and restore it after the test. - // proxyEnv := os.Getenv("HTTPS_PROXY") - // os.Setenv("HTTPS_PROXY", pLis.Addr().String()) - // defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() - + proxyEnv := os.Getenv("HTTPS_PROXY") + os.Setenv("HTTPS_PROXY", pLis.Addr().String()) + defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() + + // Use the httpproxy package instead of http.ProxyFromEnvironment because + // the latter reads proxy-related environment variables only once at + // initialization. This behavior causes issues when running multiple tests + // with different proxy configurations, as changes to environment variables + // during tests would be ignored. By using httpproxy.FromEnvironment(), we + // ensure proxy settings are read dynamically in each test execution. origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { + return httpproxy.FromEnvironment().ProxyFunc()(req.URL) + } defer func() { delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment }() - proxyConfig := httpproxy.Config{ - HTTPSProxy: pLis.Addr().String(), - NoProxy: "", - } - - delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { - return proxyConfig.ProxyFunc()(req.URL) - } - ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() conn, err := grpc.Dial(unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) @@ -180,20 +166,25 @@ func (s) TestGRPCDialWithDNSAndProxy(t *testing.T) { backendAddr := createAndStartBackendServer(t) pLis, errCh, doneCh, _ := setupProxy(t, backendAddr, false, requestCheck(backendAddr)) + // Overwrite the proxy environment and restore it after the test. + proxyEnv := os.Getenv("HTTPS_PROXY") + os.Setenv("HTTPS_PROXY", unresolvedProxyURI) + defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() + + // Use the httpproxy package instead of http.ProxyFromEnvironment because + // the latter reads proxy-related environment variables only once at + // initialization. This behavior causes issues when running multiple tests + // with different proxy configurations, as changes to environment variables + // during tests would be ignored. By using httpproxy.FromEnvironment(), we + // ensure proxy settings are read dynamically in each test execution. origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { + return httpproxy.FromEnvironment().ProxyFunc()(req.URL) + } defer func() { delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment }() - proxyConfig := httpproxy.Config{ - HTTPSProxy: unresolvedProxyURI, - NoProxy: "", - } - - delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { - return proxyConfig.ProxyFunc()(req.URL) - } - // Configure manual resolvers for both proxy and target backends targetResolver := setupDNS(t) targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) @@ -239,20 +230,25 @@ func (s) TestGRPCNewClientWithProxy(t *testing.T) { backendAddr := createAndStartBackendServer(t) pLis, errCh, doneCh, _ := setupProxy(t, backendAddr, false, requestCheck(unresolvedTargetURI)) + // Overwrite the proxy environment and restore it after the test. + proxyEnv := os.Getenv("HTTPS_PROXY") + os.Setenv("HTTPS_PROXY", unresolvedProxyURI) + defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() + + // Use the httpproxy package instead of http.ProxyFromEnvironment because + // the latter reads proxy-related environment variables only once at + // initialization. This behavior causes issues when running multiple tests + // with different proxy configurations, as changes to environment variables + // during tests would be ignored. By using httpproxy.FromEnvironment(), we + // ensure proxy settings are read dynamically in each test execution. origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { + return httpproxy.FromEnvironment().ProxyFunc()(req.URL) + } defer func() { delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment }() - proxyConfig := httpproxy.Config{ - HTTPSProxy: unresolvedProxyURI, - NoProxy: "", - } - - delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { - return proxyConfig.ProxyFunc()(req.URL) - } - // Set up and update a manual resolver for proxy resolution. proxyResolver := setupDNS(t) proxyResolver.InitialState(resolver.State{ @@ -295,20 +291,25 @@ func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { // Set up and start the proxy server. pLis, errCh, doneCh, _ := setupProxy(t, backendAddr, true, requestCheck(backendAddr)) + // Overwrite the proxy environment and restore it after the test. + proxyEnv := os.Getenv("HTTPS_PROXY") + os.Setenv("HTTPS_PROXY", unresolvedProxyURI) + defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() + + // Use the httpproxy package instead of http.ProxyFromEnvironment because + // the latter reads proxy-related environment variables only once at + // initialization. This behavior causes issues when running multiple tests + // with different proxy configurations, as changes to environment variables + // during tests would be ignored. By using httpproxy.FromEnvironment(), we + // ensure proxy settings are read dynamically in each test execution. origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { + return httpproxy.FromEnvironment().ProxyFunc()(req.URL) + } defer func() { delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment }() - proxyConfig := httpproxy.Config{ - HTTPSProxy: unresolvedProxyURI, - NoProxy: "", - } - - delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { - return proxyConfig.ProxyFunc()(req.URL) - } - // Create and update a custom resolver for target URI. targetResolver := manual.NewBuilderWithScheme("test") resolver.Register(targetResolver) @@ -352,20 +353,25 @@ func (s) TestGRPCNewClientWithProxyAndCustomResolverWithEndpoints(t *testing.T) // Set up and start the proxy server. pLis, errCh, doneCh, _ := setupProxy(t, backendAddr, true, requestCheck(backendAddr)) + // Overwrite the proxy environment and restore it after the test. + proxyEnv := os.Getenv("HTTPS_PROXY") + os.Setenv("HTTPS_PROXY", unresolvedProxyURI) + defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() + + // Use the httpproxy package instead of http.ProxyFromEnvironment because + // the latter reads proxy-related environment variables only once at + // initialization. This behavior causes issues when running multiple tests + // with different proxy configurations, as changes to environment variables + // during tests would be ignored. By using httpproxy.FromEnvironment(), we + // ensure proxy settings are read dynamically in each test execution. origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { + return httpproxy.FromEnvironment().ProxyFunc()(req.URL) + } defer func() { delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment }() - proxyConfig := httpproxy.Config{ - HTTPSProxy: unresolvedProxyURI, - NoProxy: "", - } - - delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { - return proxyConfig.ProxyFunc()(req.URL) - } - // Create and update a custom resolver for target URI. targetResolver := manual.NewBuilderWithScheme("test") resolver.Register(targetResolver) @@ -408,20 +414,25 @@ func (s) TestGRPCNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { backendAddr := createAndStartBackendServer(t) pLis, errCh, doneCh, _ := setupProxy(t, backendAddr, true, requestCheck(backendAddr)) + // Overwrite the proxy environment and restore it after the test. + proxyEnv := os.Getenv("HTTPS_PROXY") + os.Setenv("HTTPS_PROXY", unresolvedProxyURI) + defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() + + // Use the httpproxy package instead of http.ProxyFromEnvironment because + // the latter reads proxy-related environment variables only once at + // initialization. This behavior causes issues when running multiple tests + // with different proxy configurations, as changes to environment variables + // during tests would be ignored. By using httpproxy.FromEnvironment(), we + // ensure proxy settings are read dynamically in each test execution. origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { + return httpproxy.FromEnvironment().ProxyFunc()(req.URL) + } defer func() { delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment }() - proxyConfig := httpproxy.Config{ - HTTPSProxy: unresolvedProxyURI, - NoProxy: "", - } - - delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { - return proxyConfig.ProxyFunc()(req.URL) - } - // Configure manual resolvers for both proxy and target backends targetResolver := setupDNS(t) targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) @@ -475,20 +486,25 @@ func (s) TestGRPCNewClientWithNoProxy(t *testing.T) { return fmt.Errorf("proxy server should not have received a Connect request: %v", req) }) + // Overwrite the proxy environment and restore it after the test. + proxyEnv := os.Getenv("HTTPS_PROXY") + os.Setenv("HTTPS_PROXY", unresolvedProxyURI) + defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() + + // Use the httpproxy package instead of http.ProxyFromEnvironment because + // the latter reads proxy-related environment variables only once at + // initialization. This behavior causes issues when running multiple tests + // with different proxy configurations, as changes to environment variables + // during tests would be ignored. By using httpproxy.FromEnvironment(), we + // ensure proxy settings are read dynamically in each test execution. origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { + return httpproxy.FromEnvironment().ProxyFunc()(req.URL) + } defer func() { delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment }() - proxyConfig := httpproxy.Config{ - HTTPSProxy: unresolvedProxyURI, - NoProxy: "", - } - - delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { - return proxyConfig.ProxyFunc()(req.URL) - } - targetResolver := setupDNS(t) targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) @@ -531,20 +547,25 @@ func (s) TestGRPCNewClientWithContextDialer(t *testing.T) { return fmt.Errorf("proxy server should not have received a Connect request: %v", req) }) + // Overwrite the proxy environment and restore it after the test. + proxyEnv := os.Getenv("HTTPS_PROXY") + os.Setenv("HTTPS_PROXY", unresolvedProxyURI) + defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() + + // Use the httpproxy package instead of http.ProxyFromEnvironment because + // the latter reads proxy-related environment variables only once at + // initialization. This behavior causes issues when running multiple tests + // with different proxy configurations, as changes to environment variables + // during tests would be ignored. By using httpproxy.FromEnvironment(), we + // ensure proxy settings are read dynamically in each test execution. origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { + return httpproxy.FromEnvironment().ProxyFunc()(req.URL) + } defer func() { delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment }() - proxyConfig := httpproxy.Config{ - HTTPSProxy: unresolvedProxyURI, - NoProxy: "", - } - - delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { - return proxyConfig.ProxyFunc()(req.URL) - } - // Create a custom dialer that directly dials the backend. We'll use this // to bypass any proxy logic. dialerCalled := make(chan struct{}) @@ -620,20 +641,25 @@ func (s) TestBasicAuthInGrpcNewClientWithProxy(t *testing.T) { return nil }) + // Overwrite the proxy environment and restore it after the test. + proxyEnv := os.Getenv("HTTPS_PROXY") + os.Setenv("HTTPS_PROXY", user+":"+password+"@"+unresolvedProxyURI) + defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() + + // Use the httpproxy package instead of http.ProxyFromEnvironment because + // the latter reads proxy-related environment variables only once at + // initialization. This behavior causes issues when running multiple tests + // with different proxy configurations, as changes to environment variables + // during tests would be ignored. By using httpproxy.FromEnvironment(), we + // ensure proxy settings are read dynamically in each test execution. origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { + return httpproxy.FromEnvironment().ProxyFunc()(req.URL) + } defer func() { delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment }() - proxyConfig := httpproxy.Config{ - HTTPSProxy: user + ":" + password + "@" + unresolvedProxyURI, - NoProxy: "", - } - - delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { - return proxyConfig.ProxyFunc()(req.URL) - } - // Set up and update a manual resolver for proxy resolution. proxyResolver := setupDNS(t) proxyResolver.InitialState(resolver.State{ From 5ce46587cd32b02a871584d09b20fa1e6c130454 Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Mon, 23 Dec 2024 12:42:43 +0530 Subject: [PATCH 18/53] Correct pr --- clientconn.go | 7 +++ dialoptions.go | 7 +-- internal/testutils/proxy.go | 15 +++--- internal/transport/proxy.go | 6 +-- resolver_wrapper.go | 1 - test/proxy_test.go | 95 +++++++------------------------------ 6 files changed, 39 insertions(+), 92 deletions(-) diff --git a/clientconn.go b/clientconn.go index 3a98f0c2ac29..42950a9a1e92 100644 --- a/clientconn.go +++ b/clientconn.go @@ -225,6 +225,13 @@ func Dial(target string, opts ...DialOption) (*ClientConn, error) { func DialContext(ctx context.Context, target string, opts ...DialOption) (conn *ClientConn, err error) { // At the end of this method, we kick the channel out of idle, rather than // waiting for the first rpc. + // + // WithTargetResolutionEnabled in `grpc.Dial` ensures that it preserves + // behavior: when default scheme passthrough is used, skip hostname + // resolution, when any other scheme like "dns" is used for resolution, + // perform resolution on the client as expected. + opts = append([]DialOption{withDefaultScheme("passthrough"), WithTargetResolutionEnabled()}, opts...) + opts = append([]DialOption{withDefaultScheme("passthrough"), WithTargetResolutionEnabled()}, opts...) cc, err := NewClient(target, opts...) if err != nil { diff --git a/dialoptions.go b/dialoptions.go index 3950cc96853a..7abe6868cd4a 100644 --- a/dialoptions.go +++ b/dialoptions.go @@ -94,11 +94,8 @@ type dialOptions struct { idleTimeout time.Duration defaultScheme string maxCallAttempts int - // targetResolutionEnabled indicates whether the client should perform - // target resolution even when a proxy is enabled. - targetResolutionEnabled bool - // useProxy specifies if a proxy should be used. - useProxy bool + targetResolutionEnabled bool // Specifies if client should perform target resolution when proxy is enabled. + useProxy bool // Specifies if a proxy should be used. } // DialOption configures how we set up the connection. diff --git a/internal/testutils/proxy.go b/internal/testutils/proxy.go index 6559b6f54b82..8fa1238c0390 100644 --- a/internal/testutils/proxy.go +++ b/internal/testutils/proxy.go @@ -33,12 +33,12 @@ const defaultTestTimeout = 10 * time.Second // ProxyServer represents a test proxy server. type ProxyServer struct { lis net.Listener - in net.Conn // The connection from the client to the proxy. - out net.Conn // The connection from the proxy to the backend. - requestCheck func(*http.Request) error // The function to check the request sent to proxy. + in net.Conn // Connection from the client to the proxy. + out net.Conn // Connection from the proxy to the backend. + requestCheck func(*http.Request) error // Function to check the request sent to proxy. } -// Stop closes the ProxyServer and its connectionsto client and server. +// Stop closes the ProxyServer and its connections to client and server. func (p *ProxyServer) Stop() { p.lis.Close() if p.in != nil { @@ -63,6 +63,7 @@ func NewProxyServer(lis net.Listener, reqCheck func(*http.Request) error, errCh return } p.in = in + // This will be used in tests to check if the proxy server is started. if proxyStarted != nil { proxyStarted() } @@ -87,19 +88,21 @@ func NewProxyServer(lis net.Listener, reqCheck func(*http.Request) error, errCh } else { out, err = net.Dial("tcp", backendAddr) } - if err != nil { errCh <- fmt.Errorf("failed to dial to server: %v", err) return } out.SetDeadline(time.Now().Add(defaultTestTimeout)) + // Response OK to client resp := http.Response{StatusCode: http.StatusOK, Proto: "HTTP/1.0"} var buf bytes.Buffer resp.Write(&buf) p.in.Write(buf.Bytes()) p.out = out - // Perform the proxy function, i.e pass the data from client to server and server to client. + + // Perform the proxy function, i.e pass the data from client to server + // and server to client. go io.Copy(p.in, p.out) go io.Copy(p.out, p.in) close(doneCh) diff --git a/internal/transport/proxy.go b/internal/transport/proxy.go index e0751d220ebc..2e84d4b5e11e 100644 --- a/internal/transport/proxy.go +++ b/internal/transport/proxy.go @@ -61,14 +61,14 @@ func doHTTPConnectHandshake(ctx context.Context, conn net.Conn, addr resolver.Ad } }() - a, _ := proxyattributes.ExtractOptions(addr) + opts, _ := proxyattributes.ExtractOptions(addr) req := &http.Request{ Method: http.MethodConnect, - URL: &url.URL{Host: a.ConnectAddr}, + URL: &url.URL{Host: opts.ConnectAddr}, Header: map[string][]string{"User-Agent": {grpcUA}}, } - if user := a.User; user != (url.Userinfo{}) { + if user := opts.User; user != (url.Userinfo{}) { u := user.Username() p, pSet := user.Password() if !pSet { diff --git a/resolver_wrapper.go b/resolver_wrapper.go index c987aff6d6d5..7420ff5a215c 100644 --- a/resolver_wrapper.go +++ b/resolver_wrapper.go @@ -82,7 +82,6 @@ func (ccr *ccResolverWrapper) start() error { // The delegating resolver is used unless: // - A custom dialer is provided via WithContextDialer dialoption. // - Proxy usage is disabled through WithNoProxy dialoption. - // - Client-side resolution is enforced with WithTargetResolutionEnabled. // In these cases, the resolver is built based on the scheme of target, // using the appropriate resolver builder. if ccr.cc.dopts.copts.Dialer != nil || !ccr.cc.dopts.useProxy { diff --git a/test/proxy_test.go b/test/proxy_test.go index c2459db98ec0..97ac3223bada 100644 --- a/test/proxy_test.go +++ b/test/proxy_test.go @@ -44,8 +44,6 @@ const ( unresolvedProxyURI = "proxyexample.com" ) -// createAndStartBackendServer creates and starts a test backend server, -// registering a cleanup to stop it. func createAndStartBackendServer(t *testing.T) string { t.Helper() backend := &stubserver.StubServer{ @@ -121,11 +119,11 @@ func (s) TestGRPCDialWithProxy(t *testing.T) { os.Setenv("HTTPS_PROXY", pLis.Addr().String()) defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() - // Use the httpproxy package instead of http.ProxyFromEnvironment because - // the latter reads proxy-related environment variables only once at + // Use the httpproxy package functions instead of `http.ProxyFromEnvironment` + // because the latter reads proxy-related environment variables only once at // initialization. This behavior causes issues when running multiple tests // with different proxy configurations, as changes to environment variables - // during tests would be ignored. By using httpproxy.FromEnvironment(), we + // during tests would be ignored. By using `httpproxy.FromEnvironment()`, we // ensure proxy settings are read dynamically in each test execution. origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { @@ -143,6 +141,7 @@ func (s) TestGRPCDialWithProxy(t *testing.T) { } defer conn.Close() + // Send an empty RPC to the backend through the proxy. client := testgrpc.NewTestServiceClient(conn) if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { t.Errorf("EmptyCall failed: %v", err) @@ -171,12 +170,6 @@ func (s) TestGRPCDialWithDNSAndProxy(t *testing.T) { os.Setenv("HTTPS_PROXY", unresolvedProxyURI) defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() - // Use the httpproxy package instead of http.ProxyFromEnvironment because - // the latter reads proxy-related environment variables only once at - // initialization. This behavior causes issues when running multiple tests - // with different proxy configurations, as changes to environment variables - // during tests would be ignored. By using httpproxy.FromEnvironment(), we - // ensure proxy settings are read dynamically in each test execution. origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { return httpproxy.FromEnvironment().ProxyFunc()(req.URL) @@ -206,7 +199,7 @@ func (s) TestGRPCDialWithDNSAndProxy(t *testing.T) { } defer conn.Close() - // Send an RPC to the backend through the proxy. + // Send an empty RPC to the backend through the proxy. client := testgrpc.NewTestServiceClient(conn) if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { t.Errorf("EmptyCall failed: %v", err) @@ -235,12 +228,6 @@ func (s) TestGRPCNewClientWithProxy(t *testing.T) { os.Setenv("HTTPS_PROXY", unresolvedProxyURI) defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() - // Use the httpproxy package instead of http.ProxyFromEnvironment because - // the latter reads proxy-related environment variables only once at - // initialization. This behavior causes issues when running multiple tests - // with different proxy configurations, as changes to environment variables - // during tests would be ignored. By using httpproxy.FromEnvironment(), we - // ensure proxy settings are read dynamically in each test execution. origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { return httpproxy.FromEnvironment().ProxyFunc()(req.URL) @@ -251,13 +238,8 @@ func (s) TestGRPCNewClientWithProxy(t *testing.T) { // Set up and update a manual resolver for proxy resolution. proxyResolver := setupDNS(t) - proxyResolver.InitialState(resolver.State{ - Addresses: []resolver.Address{ - {Addr: pLis.Addr().String()}, - }, - }) + proxyResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: pLis.Addr().String()}}}) - // Dial to the proxy server. ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() conn, err := grpc.NewClient(unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) @@ -266,7 +248,7 @@ func (s) TestGRPCNewClientWithProxy(t *testing.T) { } defer conn.Close() - // Send an RPC to the backend through the proxy. + // Send an empty RPC to the backend through the proxy. client := testgrpc.NewTestServiceClient(conn) if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { t.Errorf("EmptyCall failed: %v", err) @@ -288,7 +270,6 @@ func (s) TestGRPCNewClientWithProxy(t *testing.T) { // establishes a connection to the backend server. func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { backendAddr := createAndStartBackendServer(t) - // Set up and start the proxy server. pLis, errCh, doneCh, _ := setupProxy(t, backendAddr, true, requestCheck(backendAddr)) // Overwrite the proxy environment and restore it after the test. @@ -296,12 +277,6 @@ func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { os.Setenv("HTTPS_PROXY", unresolvedProxyURI) defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() - // Use the httpproxy package instead of http.ProxyFromEnvironment because - // the latter reads proxy-related environment variables only once at - // initialization. This behavior causes issues when running multiple tests - // with different proxy configurations, as changes to environment variables - // during tests would be ignored. By using httpproxy.FromEnvironment(), we - // ensure proxy settings are read dynamically in each test execution. origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { return httpproxy.FromEnvironment().ProxyFunc()(req.URL) @@ -328,13 +303,13 @@ func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { } t.Cleanup(func() { conn.Close() }) - // Create a test service client and make an RPC call. + // Send an empty RPC to the backend through the proxy. client := testgrpc.NewTestServiceClient(conn) if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { t.Errorf("EmptyCall() failed: %v", err) } - // Check if the proxy encountered any errors. + // Verify if the proxy encountered any errors. select { case err := <-errCh: t.Fatalf("proxy server encountered an error: %v", err) @@ -350,7 +325,6 @@ func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { // HTTP CONNECT request, and establishes a connection to the backend server. func (s) TestGRPCNewClientWithProxyAndCustomResolverWithEndpoints(t *testing.T) { backendAddr := createAndStartBackendServer(t) - // Set up and start the proxy server. pLis, errCh, doneCh, _ := setupProxy(t, backendAddr, true, requestCheck(backendAddr)) // Overwrite the proxy environment and restore it after the test. @@ -358,12 +332,6 @@ func (s) TestGRPCNewClientWithProxyAndCustomResolverWithEndpoints(t *testing.T) os.Setenv("HTTPS_PROXY", unresolvedProxyURI) defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() - // Use the httpproxy package instead of http.ProxyFromEnvironment because - // the latter reads proxy-related environment variables only once at - // initialization. This behavior causes issues when running multiple tests - // with different proxy configurations, as changes to environment variables - // during tests would be ignored. By using httpproxy.FromEnvironment(), we - // ensure proxy settings are read dynamically in each test execution. origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { return httpproxy.FromEnvironment().ProxyFunc()(req.URL) @@ -376,6 +344,7 @@ func (s) TestGRPCNewClientWithProxyAndCustomResolverWithEndpoints(t *testing.T) targetResolver := manual.NewBuilderWithScheme("test") resolver.Register(targetResolver) targetResolver.InitialState(resolver.State{Endpoints: []resolver.Endpoint{{Addresses: []resolver.Address{{Addr: backendAddr}}}}}) + // Set up and update a manual resolver for proxy resolution. proxyResolver := setupDNS(t) proxyResolver.InitialState(resolver.State{Endpoints: []resolver.Endpoint{{Addresses: []resolver.Address{{Addr: pLis.Addr().String()}}}}}) @@ -389,7 +358,7 @@ func (s) TestGRPCNewClientWithProxyAndCustomResolverWithEndpoints(t *testing.T) } t.Cleanup(func() { conn.Close() }) - // Create a test service client and make an RPC call. + // Send an empty RPC to the backend through the proxy. client := testgrpc.NewTestServiceClient(conn) if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { t.Errorf("EmptyCall() failed: %v", err) @@ -419,12 +388,6 @@ func (s) TestGRPCNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { os.Setenv("HTTPS_PROXY", unresolvedProxyURI) defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() - // Use the httpproxy package instead of http.ProxyFromEnvironment because - // the latter reads proxy-related environment variables only once at - // initialization. This behavior causes issues when running multiple tests - // with different proxy configurations, as changes to environment variables - // during tests would be ignored. By using httpproxy.FromEnvironment(), we - // ensure proxy settings are read dynamically in each test execution. origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { return httpproxy.FromEnvironment().ProxyFunc()(req.URL) @@ -446,12 +409,10 @@ func (s) TestGRPCNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { resolver.Register(proxyResolver) proxyResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: pLis.Addr().String()}}}) - // Dial options with target resolution enabled. dopts := []grpc.DialOption{ grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithTargetResolutionEnabled(), + grpc.WithTargetResolutionEnabled(), // Target resolution on client enabled. } - // Dial to the proxy server. ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() conn, err := grpc.NewClient(unresolvedTargetURI, dopts...) @@ -460,7 +421,7 @@ func (s) TestGRPCNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { } t.Cleanup(func() { conn.Close() }) - // Create a test service client and make an RPC call. + // Send an empty RPC to the backend through the proxy. client := testgrpc.NewTestServiceClient(conn) if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { t.Errorf("EmptyCall() failed: %v", err) @@ -491,12 +452,6 @@ func (s) TestGRPCNewClientWithNoProxy(t *testing.T) { os.Setenv("HTTPS_PROXY", unresolvedProxyURI) defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() - // Use the httpproxy package instead of http.ProxyFromEnvironment because - // the latter reads proxy-related environment variables only once at - // initialization. This behavior causes issues when running multiple tests - // with different proxy configurations, as changes to environment variables - // during tests would be ignored. By using httpproxy.FromEnvironment(), we - // ensure proxy settings are read dynamically in each test execution. origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { return httpproxy.FromEnvironment().ProxyFunc()(req.URL) @@ -508,10 +463,9 @@ func (s) TestGRPCNewClientWithNoProxy(t *testing.T) { targetResolver := setupDNS(t) targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) - // Dial options with proxy explicitly disabled. dopts := []grpc.DialOption{ grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithNoProxy(), // Disable proxy. + grpc.WithNoProxy(), // Diable proxy. } ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() @@ -527,7 +481,7 @@ func (s) TestGRPCNewClientWithNoProxy(t *testing.T) { t.Errorf("EmptyCall() failed: %v", err) } - // Verify that the proxy was not dialed. + // Verify that the proxy server was not dialed. select { case <-proxyStartedCh: t.Fatal("unexpected dial to proxy server") @@ -552,12 +506,6 @@ func (s) TestGRPCNewClientWithContextDialer(t *testing.T) { os.Setenv("HTTPS_PROXY", unresolvedProxyURI) defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() - // Use the httpproxy package instead of http.ProxyFromEnvironment because - // the latter reads proxy-related environment variables only once at - // initialization. This behavior causes issues when running multiple tests - // with different proxy configurations, as changes to environment variables - // during tests would be ignored. By using httpproxy.FromEnvironment(), we - // ensure proxy settings are read dynamically in each test execution. origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { return httpproxy.FromEnvironment().ProxyFunc()(req.URL) @@ -566,8 +514,7 @@ func (s) TestGRPCNewClientWithContextDialer(t *testing.T) { delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment }() - // Create a custom dialer that directly dials the backend. We'll use this - // to bypass any proxy logic. + // Create a custom dialer that directly dials the backend.ß dialerCalled := make(chan struct{}) customDialer := func(_ context.Context, backendAddr string) (net.Conn, error) { close(dialerCalled) @@ -579,7 +526,7 @@ func (s) TestGRPCNewClientWithContextDialer(t *testing.T) { dopts := []grpc.DialOption{ grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithContextDialer(customDialer), + grpc.WithContextDialer(customDialer), // Use a custom dialer. } ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() @@ -646,12 +593,6 @@ func (s) TestBasicAuthInGrpcNewClientWithProxy(t *testing.T) { os.Setenv("HTTPS_PROXY", user+":"+password+"@"+unresolvedProxyURI) defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() - // Use the httpproxy package instead of http.ProxyFromEnvironment because - // the latter reads proxy-related environment variables only once at - // initialization. This behavior causes issues when running multiple tests - // with different proxy configurations, as changes to environment variables - // during tests would be ignored. By using httpproxy.FromEnvironment(), we - // ensure proxy settings are read dynamically in each test execution. origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { return httpproxy.FromEnvironment().ProxyFunc()(req.URL) @@ -676,7 +617,7 @@ func (s) TestBasicAuthInGrpcNewClientWithProxy(t *testing.T) { } defer conn.Close() - // Send an RPC to the backend through the proxy. + // Send an empty RPC to the backend through the proxy. client := testgrpc.NewTestServiceClient(conn) if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { t.Errorf("EmptyCall failed: %v", err) From 4483e955b0fce1042e6be4cf4372b10f9a5493d1 Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Mon, 23 Dec 2024 13:24:18 +0530 Subject: [PATCH 19/53] rebase --- internal/transport/http2_client.go | 2 +- internal/transport/proxy.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/transport/http2_client.go b/internal/transport/http2_client.go index ec75a8d92e10..d9bc672d4109 100644 --- a/internal/transport/http2_client.go +++ b/internal/transport/http2_client.go @@ -159,7 +159,7 @@ func dial(ctx context.Context, fn func(context.Context, string) (net.Conn, error // A non-empty ConnectAddr attribute indicates that a proxy is configured, // so initiate a proxy dial. - if _, present := proxyattributes.ExtractOptions(addr); present { + if _, present := proxyattributes.Get(addr); present { return proxyDial(ctx, addr, grpcUA) } networkType, ok := networktype.Get(addr) diff --git a/internal/transport/proxy.go b/internal/transport/proxy.go index 2e84d4b5e11e..a6361dbf53a5 100644 --- a/internal/transport/proxy.go +++ b/internal/transport/proxy.go @@ -61,7 +61,7 @@ func doHTTPConnectHandshake(ctx context.Context, conn net.Conn, addr resolver.Ad } }() - opts, _ := proxyattributes.ExtractOptions(addr) + opts, _ := proxyattributes.Get(addr) req := &http.Request{ Method: http.MethodConnect, URL: &url.URL{Host: opts.ConnectAddr}, From d4f6215adba1038c103c7d1ee94103368938ea29 Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Mon, 23 Dec 2024 13:37:35 +0530 Subject: [PATCH 20/53] comment --- internal/transport/http2_client.go | 2 -- internal/transport/proxy.go | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/internal/transport/http2_client.go b/internal/transport/http2_client.go index d9bc672d4109..0e89da0c7e29 100644 --- a/internal/transport/http2_client.go +++ b/internal/transport/http2_client.go @@ -157,8 +157,6 @@ type http2Client struct { func dial(ctx context.Context, fn func(context.Context, string) (net.Conn, error), addr resolver.Address, grpcUA string) (net.Conn, error) { address := addr.Addr - // A non-empty ConnectAddr attribute indicates that a proxy is configured, - // so initiate a proxy dial. if _, present := proxyattributes.Get(addr); present { return proxyDial(ctx, addr, grpcUA) } diff --git a/internal/transport/proxy.go b/internal/transport/proxy.go index a6361dbf53a5..2ab5bc5875ff 100644 --- a/internal/transport/proxy.go +++ b/internal/transport/proxy.go @@ -71,7 +71,7 @@ func doHTTPConnectHandshake(ctx context.Context, conn net.Conn, addr resolver.Ad if user := opts.User; user != (url.Userinfo{}) { u := user.Username() p, pSet := user.Password() - if !pSet { + if !pSet && logger.V(2) { logger.Warningf("password not set for basic authentication for proxy dialing") } req.Header.Add(proxyAuthHeaderKey, "Basic "+basicAuth(u, p)) From 333a68cf4fb719c385debbc505013b5bc8201130 Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Mon, 23 Dec 2024 23:11:11 +0530 Subject: [PATCH 21/53] proxy testutils refactor --- clientconn.go | 6 +- dialoptions.go | 7 +- internal/testutils/proxy.go | 111 ----------------------------- internal/transport/http2_client.go | 4 +- internal/transport/proxy.go | 7 +- test/proxy_test.go | 89 ++++++++++++++++++++++- 6 files changed, 97 insertions(+), 127 deletions(-) delete mode 100644 internal/testutils/proxy.go diff --git a/clientconn.go b/clientconn.go index 42950a9a1e92..ba574e4b5eb0 100644 --- a/clientconn.go +++ b/clientconn.go @@ -228,10 +228,8 @@ func DialContext(ctx context.Context, target string, opts ...DialOption) (conn * // // WithTargetResolutionEnabled in `grpc.Dial` ensures that it preserves // behavior: when default scheme passthrough is used, skip hostname - // resolution, when any other scheme like "dns" is used for resolution, - // perform resolution on the client as expected. - opts = append([]DialOption{withDefaultScheme("passthrough"), WithTargetResolutionEnabled()}, opts...) - + // resolution, when "dns" is used for resolution, + // perform resolution on the client. opts = append([]DialOption{withDefaultScheme("passthrough"), WithTargetResolutionEnabled()}, opts...) cc, err := NewClient(target, opts...) if err != nil { diff --git a/dialoptions.go b/dialoptions.go index 7d589acc1a88..856ff37e8d3b 100644 --- a/dialoptions.go +++ b/dialoptions.go @@ -94,8 +94,8 @@ type dialOptions struct { idleTimeout time.Duration defaultScheme string maxCallAttempts int - targetResolutionEnabled bool // Specifies if client should perform target resolution when proxy is enabled. - useProxy bool // Specifies if a proxy should be used. + targetResolutionEnabled bool // Specifies if target hostnames should be resolved when proxying is enabled. + useProxy bool // Specifies if a server should be connected via proxy. } // DialOption configures how we set up the connection. @@ -384,7 +384,8 @@ func WithNoProxy() DialOption { } // WithTargetResolutionEnabled returns a DialOption which enables target -// resolution on client. This is ignored if WithNoProxy is used. +// resolution on client even when "dns" scheme is used. This is ignored if +// WithNoProxy is used. // // # Experimental // diff --git a/internal/testutils/proxy.go b/internal/testutils/proxy.go deleted file mode 100644 index 8fa1238c0390..000000000000 --- a/internal/testutils/proxy.go +++ /dev/null @@ -1,111 +0,0 @@ -/* - * - * 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 testutils - -import ( - "bufio" - "bytes" - "fmt" - "io" - "net" - "net/http" - "time" -) - -const defaultTestTimeout = 10 * time.Second - -// ProxyServer represents a test proxy server. -type ProxyServer struct { - lis net.Listener - in net.Conn // Connection from the client to the proxy. - out net.Conn // Connection from the proxy to the backend. - requestCheck func(*http.Request) error // Function to check the request sent to proxy. -} - -// Stop closes the ProxyServer and its connections to client and server. -func (p *ProxyServer) Stop() { - p.lis.Close() - if p.in != nil { - p.in.Close() - } - if p.out != nil { - p.out.Close() - } -} - -// NewProxyServer create and starts a proxy server. -func NewProxyServer(lis net.Listener, reqCheck func(*http.Request) error, errCh chan error, doneCh chan struct{}, backendAddr string, resOnClient bool, proxyStarted func()) *ProxyServer { - p := &ProxyServer{ - lis: lis, - requestCheck: reqCheck, - } - - // Start the proxy server. - go func() { - in, err := p.lis.Accept() - if err != nil { - return - } - p.in = in - // This will be used in tests to check if the proxy server is started. - if proxyStarted != nil { - proxyStarted() - } - req, err := http.ReadRequest(bufio.NewReader(in)) - if err != nil { - errCh <- fmt.Errorf("failed to read CONNECT req: %v", err) - return - } - if err := p.requestCheck(req); err != nil { - resp := http.Response{StatusCode: http.StatusMethodNotAllowed} - resp.Write(p.in) - p.in.Close() - errCh <- fmt.Errorf("get wrong CONNECT req: %+v, error: %v", req, err) - return - } - var out net.Conn - // If resolution is done on client,connect to address received in - // CONNECT request or else connect to backend address directly. This is - // to mimick the name resolution on proxy server. - if resOnClient { - out, err = net.Dial("tcp", req.URL.Host) - } else { - out, err = net.Dial("tcp", backendAddr) - } - if err != nil { - errCh <- fmt.Errorf("failed to dial to server: %v", err) - return - } - out.SetDeadline(time.Now().Add(defaultTestTimeout)) - - // Response OK to client - resp := http.Response{StatusCode: http.StatusOK, Proto: "HTTP/1.0"} - var buf bytes.Buffer - resp.Write(&buf) - p.in.Write(buf.Bytes()) - p.out = out - - // Perform the proxy function, i.e pass the data from client to server - // and server to client. - go io.Copy(p.in, p.out) - go io.Copy(p.out, p.in) - close(doneCh) - }() - return p -} diff --git a/internal/transport/http2_client.go b/internal/transport/http2_client.go index 0e89da0c7e29..c67b3e97ba53 100644 --- a/internal/transport/http2_client.go +++ b/internal/transport/http2_client.go @@ -157,8 +157,8 @@ type http2Client struct { func dial(ctx context.Context, fn func(context.Context, string) (net.Conn, error), addr resolver.Address, grpcUA string) (net.Conn, error) { address := addr.Addr - if _, present := proxyattributes.Get(addr); present { - return proxyDial(ctx, addr, grpcUA) + if opts, present := proxyattributes.Get(addr); present { + return proxyDial(ctx, addr, grpcUA, opts) } networkType, ok := networktype.Get(addr) if fn != nil { diff --git a/internal/transport/proxy.go b/internal/transport/proxy.go index 2ab5bc5875ff..e1314d11844d 100644 --- a/internal/transport/proxy.go +++ b/internal/transport/proxy.go @@ -54,14 +54,13 @@ func basicAuth(username, password string) string { return base64.StdEncoding.EncodeToString([]byte(auth)) } -func doHTTPConnectHandshake(ctx context.Context, conn net.Conn, addr resolver.Address, grpcUA string) (_ net.Conn, err error) { +func doHTTPConnectHandshake(ctx context.Context, conn net.Conn, addr resolver.Address, grpcUA string, opts proxyattributes.Options) (_ net.Conn, err error) { defer func() { if err != nil { conn.Close() } }() - opts, _ := proxyattributes.Get(addr) req := &http.Request{ Method: http.MethodConnect, URL: &url.URL{Host: opts.ConnectAddr}, @@ -104,12 +103,12 @@ func doHTTPConnectHandshake(ctx context.Context, conn net.Conn, addr resolver.Ad } // proxyDial establishes a TCP connection to the specified address and performs an HTTP CONNECT handshake. -func proxyDial(ctx context.Context, addr resolver.Address, grpcUA string) (net.Conn, error) { +func proxyDial(ctx context.Context, addr resolver.Address, grpcUA string, opts proxyattributes.Options) (net.Conn, error) { conn, err := internal.NetDialerWithTCPKeepalive().DialContext(ctx, "tcp", addr.Addr) if err != nil { return nil, err } - return doHTTPConnectHandshake(ctx, conn, addr, grpcUA) + return doHTTPConnectHandshake(ctx, conn, addr, grpcUA, opts) } func sendHTTPRequest(ctx context.Context, req *http.Request, conn net.Conn) error { diff --git a/test/proxy_test.go b/test/proxy_test.go index 97ac3223bada..7932d90dc63a 100644 --- a/test/proxy_test.go +++ b/test/proxy_test.go @@ -19,21 +19,24 @@ package test import ( + "bufio" + "bytes" "context" "encoding/base64" "fmt" + "io" "net" "net/http" "net/url" "os" "testing" + "time" "golang.org/x/net/http/httpproxy" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/internal/resolver/delegatingresolver" "google.golang.org/grpc/internal/stubserver" - "google.golang.org/grpc/internal/testutils" testgrpc "google.golang.org/grpc/interop/grpc_testing" "google.golang.org/grpc/resolver" "google.golang.org/grpc/resolver/manual" @@ -70,6 +73,86 @@ func setupDNS(t *testing.T) *manual.Resolver { return mr } +// proxyServer represents a test proxy server. +type proxyServer struct { + lis net.Listener + in net.Conn // Connection from the client to the proxy. + out net.Conn // Connection from the proxy to the backend. + requestCheck func(*http.Request) error // Function to check the request sent to proxy. +} + +// Stop closes the ProxyServer and its connections to client and server. +func (p *proxyServer) stop() { + p.lis.Close() + if p.in != nil { + p.in.Close() + } + if p.out != nil { + p.out.Close() + } +} + +// Creates and starts a proxy server. +func newProxyServer(lis net.Listener, reqCheck func(*http.Request) error, errCh chan error, doneCh chan struct{}, backendAddr string, resOnClient bool, proxyStarted func()) *proxyServer { + p := &proxyServer{ + lis: lis, + requestCheck: reqCheck, + } + + // Start the proxy server. + go func() { + in, err := p.lis.Accept() + if err != nil { + return + } + p.in = in + // This will be used in tests to check if the proxy server is started. + if proxyStarted != nil { + proxyStarted() + } + req, err := http.ReadRequest(bufio.NewReader(in)) + if err != nil { + errCh <- fmt.Errorf("failed to read CONNECT req: %v", err) + return + } + if err := p.requestCheck(req); err != nil { + resp := http.Response{StatusCode: http.StatusMethodNotAllowed} + resp.Write(p.in) + p.in.Close() + errCh <- fmt.Errorf("get wrong CONNECT req: %+v, error: %v", req, err) + return + } + var out net.Conn + // If resolution is done on client,connect to address received in + // CONNECT request or else connect to backend address directly. This is + // to mimick the name resolution on proxy server. + if resOnClient { + out, err = net.Dial("tcp", req.URL.Host) + } else { + out, err = net.Dial("tcp", backendAddr) + } + if err != nil { + errCh <- fmt.Errorf("failed to dial to server: %v", err) + return + } + out.SetDeadline(time.Now().Add(defaultTestTimeout)) + + // Response OK to client + resp := http.Response{StatusCode: http.StatusOK, Proto: "HTTP/1.0"} + var buf bytes.Buffer + resp.Write(&buf) + p.in.Write(buf.Bytes()) + p.out = out + + // Perform the proxy function, i.e pass the data from client to server + // and server to client. + go io.Copy(p.in, p.out) + go io.Copy(p.out, p.in) + close(doneCh) + }() + return p +} + // setupProxy initializes and starts a proxy server, registers a cleanup to // stop it, and returns the proxy's listener and helper channels. func setupProxy(t *testing.T, backendAddr string, resolutionOnClient bool, reqCheck func(*http.Request) error) (net.Listener, chan error, chan struct{}, chan struct{}) { @@ -86,8 +169,8 @@ func setupProxy(t *testing.T, backendAddr string, resolutionOnClient bool, reqCh close(errCh) }) - proxyServer := testutils.NewProxyServer(pLis, reqCheck, errCh, doneCh, backendAddr, resolutionOnClient, func() { close(proxyStartedCh) }) - t.Cleanup(proxyServer.Stop) + proxyServer := newProxyServer(pLis, reqCheck, errCh, doneCh, backendAddr, resolutionOnClient, func() { close(proxyStartedCh) }) + t.Cleanup(proxyServer.stop) return pLis, errCh, doneCh, proxyStartedCh } From cacf058a2deced4c2353fcc64314a59ce3bc520b Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Mon, 23 Dec 2024 23:23:07 +0530 Subject: [PATCH 22/53] correct vet.sh --- internal/transport/proxy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/transport/proxy.go b/internal/transport/proxy.go index e1314d11844d..dbf07d6c375b 100644 --- a/internal/transport/proxy.go +++ b/internal/transport/proxy.go @@ -54,7 +54,7 @@ func basicAuth(username, password string) string { return base64.StdEncoding.EncodeToString([]byte(auth)) } -func doHTTPConnectHandshake(ctx context.Context, conn net.Conn, addr resolver.Address, grpcUA string, opts proxyattributes.Options) (_ net.Conn, err error) { +func doHTTPConnectHandshake(ctx context.Context, conn net.Conn, grpcUA string, opts proxyattributes.Options) (_ net.Conn, err error) { defer func() { if err != nil { conn.Close() From 7c5b1b3ae551bf57a8a0e6a5972a2eecb3fc0b96 Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Mon, 23 Dec 2024 23:26:07 +0530 Subject: [PATCH 23/53] correct vet --- internal/transport/proxy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/transport/proxy.go b/internal/transport/proxy.go index dbf07d6c375b..2c7fb5ec6e5c 100644 --- a/internal/transport/proxy.go +++ b/internal/transport/proxy.go @@ -108,7 +108,7 @@ func proxyDial(ctx context.Context, addr resolver.Address, grpcUA string, opts p if err != nil { return nil, err } - return doHTTPConnectHandshake(ctx, conn, addr, grpcUA, opts) + return doHTTPConnectHandshake(ctx, conn, grpcUA, opts) } func sendHTTPRequest(ctx context.Context, req *http.Request, conn net.Conn) error { From ff05ee3a294d174ad65daba2617408f1e716e933 Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Fri, 27 Dec 2024 12:51:14 +0530 Subject: [PATCH 24/53] something --- dialoptions.go | 4 +- internal/transport/proxy.go | 6 +- internal/transport/proxy_ext_test.go | 463 +++++++++++++++++ internal/transport/proxy_test.go | 105 ++++ internal/transport/proxyutils.go | 149 ++++++ test/proxy_test.go | 716 --------------------------- 6 files changed, 721 insertions(+), 722 deletions(-) create mode 100644 internal/transport/proxy_ext_test.go create mode 100644 internal/transport/proxy_test.go create mode 100644 internal/transport/proxyutils.go delete mode 100644 test/proxy_test.go diff --git a/dialoptions.go b/dialoptions.go index 856ff37e8d3b..2eae24fa573b 100644 --- a/dialoptions.go +++ b/dialoptions.go @@ -384,8 +384,8 @@ func WithNoProxy() DialOption { } // WithTargetResolutionEnabled returns a DialOption which enables target -// resolution on client even when "dns" scheme is used. This is ignored if -// WithNoProxy is used. +// resolution on client when a proxy is used along with the the "dns" scheme. +// This is ignored if WithNoProxy is used. // // # Experimental // diff --git a/internal/transport/proxy.go b/internal/transport/proxy.go index 2c7fb5ec6e5c..9d021adffa0a 100644 --- a/internal/transport/proxy.go +++ b/internal/transport/proxy.go @@ -69,10 +69,7 @@ func doHTTPConnectHandshake(ctx context.Context, conn net.Conn, grpcUA string, o if user := opts.User; user != (url.Userinfo{}) { u := user.Username() - p, pSet := user.Password() - if !pSet && logger.V(2) { - logger.Warningf("password not set for basic authentication for proxy dialing") - } + p, _ := user.Password() req.Header.Add(proxyAuthHeaderKey, "Basic "+basicAuth(u, p)) } if err := sendHTTPRequest(ctx, req, conn); err != nil { @@ -104,6 +101,7 @@ func doHTTPConnectHandshake(ctx context.Context, conn net.Conn, grpcUA string, o // proxyDial establishes a TCP connection to the specified address and performs an HTTP CONNECT handshake. func proxyDial(ctx context.Context, addr resolver.Address, grpcUA string, opts proxyattributes.Options) (net.Conn, error) { + fmt.Printf("\nemchandwani : address : %v\n", addr.Addr) conn, err := internal.NetDialerWithTCPKeepalive().DialContext(ctx, "tcp", addr.Addr) if err != nil { return nil, err diff --git a/internal/transport/proxy_ext_test.go b/internal/transport/proxy_ext_test.go new file mode 100644 index 000000000000..66f1bc941644 --- /dev/null +++ b/internal/transport/proxy_ext_test.go @@ -0,0 +1,463 @@ +/* + * + * 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 transport_test + +import ( + "context" + "encoding/base64" + "fmt" + "net" + "net/http" + "net/url" + "testing" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/internal/grpctest" + "google.golang.org/grpc/internal/resolver/delegatingresolver" + "google.golang.org/grpc/internal/stubserver" + "google.golang.org/grpc/internal/testutils" + "google.golang.org/grpc/internal/transport" + testgrpc "google.golang.org/grpc/interop/grpc_testing" + "google.golang.org/grpc/resolver" + "google.golang.org/grpc/resolver/manual" +) + +type s struct { + grpctest.Tester +} + +func Test(t *testing.T) { + grpctest.RunSubTests(t, s{}) +} + +const defaultTestTimeout = 10 * time.Second + +// Starts a StubServer and returns the server's address in both IPv6 ("[::1]:port") and unresolved ("localhost:port") formats. +func createAndStartBackendServer(t *testing.T) (string, string) { + t.Helper() + backend := &stubserver.StubServer{ + EmptyCallF: func(context.Context, *testgrpc.Empty) (*testgrpc.Empty, error) { return &testgrpc.Empty{}, nil }, + } + if err := backend.StartServer(); err != nil { + t.Fatalf("failed to start backend: %v", err) + } + t.Logf("Started TestService backend at: %q", backend.Address) + t.Cleanup(backend.Stop) + port := testutils.ParsePort(t, backend.Address) + return backend.Address, fmt.Sprintf("localhost:%d", port) +} + +// Tests the scenario where grpc.Dial is performed using a proxy with the +// default resolver in the target URI. The test verifies that the connection is +// established to the proxy server, sends the unresolved target URI in the HTTP +// CONNECT request, and is successfully connected to the backend server. +func (s) TestGRPCDialWithProxy(t *testing.T) { + _, unresolvedTargetURI := createAndStartBackendServer(t) + unresolvedProxyURI, _ := transport.SetupProxy(t, transport.RequestCheck(unresolvedTargetURI), false) + + hpfe := func(req *http.Request) (*url.URL, error) { + if req.URL.Host == unresolvedTargetURI { + return &url.URL{ + Scheme: "https", + Host: unresolvedProxyURI, + }, nil + } + return nil, nil + } + orighpfe := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = hpfe + defer func() { + delegatingresolver.HTTPSProxyFromEnvironment = orighpfe + }() + + ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) + defer cancel() + conn, err := grpc.Dial(unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + t.Fatalf("grpc.Dial(%s) failed: %v", unresolvedTargetURI, err) + } + defer conn.Close() + + // Send an empty RPC to the backend through the proxy. + client := testgrpc.NewTestServiceClient(conn) + if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { + t.Fatalf("EmptyCall failed: %v", err) + } +} + +// Tests the scenario where `grpc.Dial` is performed with a proxy and the "dns" +// scheme for the target. The test verifies that the proxy URI is correctly +// resolved and that the target URI resolution on the client preserves the +// original behavior of `grpc.Dial`. It also ensures that a connection is +// established to the proxy server, with the resolved target URI sent in the +// HTTP CONNECT request, successfully connecting to the backend server. +func TestGRPCDialWithDNSAndProxy(t *testing.T) { + backendAddr, unresolvedTargetURI := createAndStartBackendServer(t) + unresolvedProxyURI, _ := transport.SetupProxy(t, transport.RequestCheck(backendAddr), false) + + // Overwrite the proxy environment and restore it after the test. + hpfe := func(req *http.Request) (*url.URL, error) { + if req.URL.Host == unresolvedTargetURI { + return &url.URL{ + Scheme: "https", + Host: unresolvedProxyURI, + }, nil + } + return nil, nil + } + orighpfe := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = hpfe + defer func() { + delegatingresolver.HTTPSProxyFromEnvironment = orighpfe + }() + + targetResolver := setupDNS(t) + targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) + + ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) + defer cancel() + conn, err := grpc.Dial("dns:///"+unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + t.Fatalf("grpc.Dial(%s) failed: %v", "dns:///"+unresolvedTargetURI, err) + } + defer conn.Close() + + // Send an empty RPC to the backend through the proxy. + client := testgrpc.NewTestServiceClient(conn) + if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { + t.Fatalf("EmptyCall failed: %v", err) + } +} + +// Tests the scenario where grpc.NewClient is used with the default DNS +// resolver for the target URI and a proxy is configured. The test verifies +// that the client resolves proxy URI, connects to the proxy server, sends the +// unresolved target URI in the HTTP CONNECT request, and successfully +// establishes a connection to the backend server. +func (s) TestGRPCNewClientWithProxy(t *testing.T) { + _, unresolvedTargetURI := createAndStartBackendServer(t) + unresolvedProxyURI, _ := transport.SetupProxy(t, transport.RequestCheck(unresolvedTargetURI), false) + + // Overwrite the proxy environment and restore it after the test. + hpfe := func(req *http.Request) (*url.URL, error) { + if req.URL.Host == unresolvedTargetURI { + return &url.URL{ + Scheme: "https", + Host: unresolvedProxyURI, + }, nil + } + return nil, nil + } + orighpfe := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = hpfe + defer func() { + delegatingresolver.HTTPSProxyFromEnvironment = orighpfe + }() + + ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) + defer cancel() + conn, err := grpc.NewClient(unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + t.Fatalf("grpc.NewClient failed: %v", err) + } + defer conn.Close() + + // Send an empty RPC to the backend through the proxy. + client := testgrpc.NewTestServiceClient(conn) + if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { + t.Errorf("EmptyCall failed: %v", err) + } +} + +// Tests the scenario where grpc.NewClient is used with a custom target URI +// scheme and a proxy is configured. The test verifies that the client +// successfully connects to the proxy server, resolves the proxy URI correctly, +// includes the resolved target URI in the HTTP CONNECT request, and +// establishes a connection to the backend server. +func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { + unresolvedTargetURI, backendAddr := createAndStartBackendServer(t) + unresolvedProxyURI, _ := transport.SetupProxy(t, transport.RequestCheck(backendAddr), false) + + // Overwrite the proxy environment and restore it after the test. + hpfe := func(req *http.Request) (*url.URL, error) { + if req.URL.Host == unresolvedTargetURI { + return &url.URL{ + Scheme: "https", + Host: unresolvedProxyURI, + }, nil + } + return nil, nil + } + orighpfe := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = hpfe + defer func() { + delegatingresolver.HTTPSProxyFromEnvironment = orighpfe + }() + + // Create and update a custom resolver for target URI. + targetResolver := manual.NewBuilderWithScheme("test") + resolver.Register(targetResolver) + targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) + + /// Create a client with the custom resolver. + ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) + defer cancel() + conn, err := grpc.NewClient(targetResolver.Scheme()+":///"+unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + t.Fatalf("grpc.NewClient(%s) failed: %v", targetResolver.Scheme()+":///"+unresolvedTargetURI, err) + } + t.Cleanup(func() { conn.Close() }) + + // Send an empty RPC to the backend through the proxy. + client := testgrpc.NewTestServiceClient(conn) + if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { + t.Fatalf("EmptyCall() failed: %v", err) + } +} + +// Tests the scenario where grpc.NewClient is used with the default "dns" +// resolver and the dial option grpc.WithTargetResolutionEnabled() is set, +// enabling target resolution on the client. The test verifies that target +// resolution happens on the client by sending resolved target URI in HTTP +// CONNECT request, the proxy URI is resolved correctly, and the connection is +// successfully established with the backend server through the proxy. +func (s) TestGRPCNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { + backendAddr, unresolvedTargetURI := createAndStartBackendServer(t) + unresolvedProxyURI, _ := transport.SetupProxy(t, transport.RequestCheck(backendAddr), false) + + // Overwrite the proxy environment and restore it after the test. + hpfe := func(req *http.Request) (*url.URL, error) { + if req.URL.Host == unresolvedTargetURI { + return &url.URL{ + Scheme: "https", + Host: unresolvedProxyURI, + }, nil + } + return nil, nil + } + orighpfe := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = hpfe + defer func() { + delegatingresolver.HTTPSProxyFromEnvironment = orighpfe + }() + + dopts := []grpc.DialOption{ + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithTargetResolutionEnabled(), // Target resolution on client enabled. + } + ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) + defer cancel() + conn, err := grpc.NewClient(unresolvedTargetURI, dopts...) + if err != nil { + t.Fatalf("grpc.NewClient(%s) failed: %v", unresolvedTargetURI, err) + } + t.Cleanup(func() { conn.Close() }) + + // Send an empty RPC to the backend through the proxy. + client := testgrpc.NewTestServiceClient(conn) + if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { + t.Errorf("EmptyCall() failed: %v", err) + } +} + +// Tests the scenario where grpc.NewClient is used with grpc.WithNoProxy() set, +// explicitly disabling proxy usage. The test verifies that the client does not +// dial the proxy but directly connects to the backend server. It also checks +// that the proxy resolution function is not called and that the proxy server +// never receives a connection request. +func (s) TestGRPCNewClientWithNoProxy(t *testing.T) { + unresolvedTargetURI, _ := createAndStartBackendServer(t) + unresolvedProxyURI, proxyStartedCh := transport.SetupProxy(t, func(req *http.Request) error { + return fmt.Errorf("proxy server should not have received a Connect request: %v", req) + }, false) + + // Overwrite the proxy environment and restore it after the test. + hpfe := func(req *http.Request) (*url.URL, error) { + if req.URL.Host == unresolvedTargetURI { + return &url.URL{ + Scheme: "https", + Host: unresolvedProxyURI, + }, nil + } + return nil, nil + } + orighpfe := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = hpfe + defer func() { + delegatingresolver.HTTPSProxyFromEnvironment = orighpfe + }() + + dopts := []grpc.DialOption{ + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithNoProxy(), // Disable proxy. + } + ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) + defer cancel() + conn, err := grpc.NewClient(unresolvedTargetURI, dopts...) + if err != nil { + t.Fatalf("grpc.NewClient(%s) failed: %v", unresolvedTargetURI, err) + } + defer conn.Close() + + // Create a test service client and make an RPC call. + client := testgrpc.NewTestServiceClient(conn) + if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { + t.Errorf("EmptyCall() failed: %v", err) + } + + // Verify that the proxy server was not dialed. + select { + case <-proxyStartedCh: + t.Fatal("unexpected dial to proxy server") + default: + t.Log("proxy server was not dialed") + } +} + +// Tests the scenario where grpc.NewClient is used with grpc.WithContextDialer() +// set. The test verifies that the client bypasses proxy dialing and uses the +// custom dialer instead. It ensures that the proxy server is never dialed, the +// proxy resolution function is not triggered, and the custom dialer is invoked +// as expected. +func (s) TestGRPCNewClientWithContextDialer(t *testing.T) { + unresolvedTargetURI, _ := createAndStartBackendServer(t) + unresolvedProxyURI, proxyStartedCh := transport.SetupProxy(t, func(req *http.Request) error { + return fmt.Errorf("proxy server should not have received a Connect request: %v", req) + }, false) + + // Overwrite the proxy environment and restore it after the test. + hpfe := func(req *http.Request) (*url.URL, error) { + if req.URL.Host == unresolvedTargetURI { + return &url.URL{ + Scheme: "https", + Host: unresolvedProxyURI, + }, nil + } + return nil, nil + } + orighpfe := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = hpfe + defer func() { + delegatingresolver.HTTPSProxyFromEnvironment = orighpfe + }() + + // Create a custom dialer that directly dials the backend.ß + dialerCalled := make(chan struct{}) + customDialer := func(_ context.Context, backendAddr string) (net.Conn, error) { + close(dialerCalled) + return net.Dial("tcp", backendAddr) + } + + dopts := []grpc.DialOption{ + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithContextDialer(customDialer), // Use a custom dialer. + } + ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) + defer cancel() + conn, err := grpc.NewClient(unresolvedTargetURI, dopts...) + if err != nil { + t.Fatalf("grpc.NewClient(%s) failed: %v", unresolvedTargetURI, err) + } + defer conn.Close() + + client := testgrpc.NewTestServiceClient(conn) + if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { + t.Errorf("EmptyCall() failed: %v", err) + } + + select { + case <-dialerCalled: + t.Log("custom dialer was invoked") + case <-time.After(defaultTestTimeout): + t.Fatalf("test timed out waiting for custom dialer to be called") + } + + select { + case <-proxyStartedCh: + t.Fatal("unexpected dial to proxy server") + default: + t.Log("proxy server was not dialled") + } +} + +// Tests the scenario where grpc.NewClient is used with the default DNS resolver +// for targetURI and a proxy. The test verifies that the client connects to the +// proxy server, sends the unresolved target URI in the HTTP CONNECT request, +// and successfully connects to the backend. Additionally, it checks that the +// correct user information is included in the Proxy-Authorization header of +// the CONNECT request. The test also ensures that target resolution does not +// happen on the client. +func (s) TestBasicAuthInGrpcNewClientWithProxy(t *testing.T) { + _, unresolvedTargetURI := createAndStartBackendServer(t) + const ( + user = "notAUser" + password = "notAPassword" + ) + unresolvedProxyURI, _ := transport.SetupProxy(t, func(req *http.Request) error { + if req.Method != http.MethodConnect { + return fmt.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) + } + if req.URL.Host != unresolvedTargetURI { + return fmt.Errorf("unexpected URL.Host %q, want %q", req.URL.Host, unresolvedTargetURI) + } + wantProxyAuthStr := "Basic " + base64.StdEncoding.EncodeToString([]byte(user+":"+password)) + if got := req.Header.Get("Proxy-Authorization"); got != wantProxyAuthStr { + gotDecoded, err := base64.StdEncoding.DecodeString(got) + if err != nil { + return fmt.Errorf("failed to decode Proxy-Authorization header: %v", err) + } + wantDecoded, _ := base64.StdEncoding.DecodeString(wantProxyAuthStr) + return fmt.Errorf("unexpected auth %q (%q), want %q (%q)", got, gotDecoded, wantProxyAuthStr, wantDecoded) + } + return nil + }, false) + + // Overwrite the proxy environment and restore it after the test. + hpfe := func(req *http.Request) (*url.URL, error) { + if req.URL.Host == unresolvedTargetURI { + return &url.URL{ + Scheme: "https", + User: url.UserPassword(user, password), + Host: unresolvedProxyURI, + }, nil + } + return nil, nil + } + orighpfe := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = hpfe + defer func() { + delegatingresolver.HTTPSProxyFromEnvironment = orighpfe + }() + + ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) + defer cancel() + conn, err := grpc.NewClient(unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + t.Fatalf("grpc.NewClient(%s) failed: %v", unresolvedTargetURI, err) + } + defer conn.Close() + + // Send an empty RPC to the backend through the proxy. + client := testgrpc.NewTestServiceClient(conn) + if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { + t.Errorf("EmptyCall failed: %v", err) + } +} diff --git a/internal/transport/proxy_test.go b/internal/transport/proxy_test.go new file mode 100644 index 000000000000..c3839852c27f --- /dev/null +++ b/internal/transport/proxy_test.go @@ -0,0 +1,105 @@ +//go:build !race +// +build !race + +/* + * + * Copyright 2017 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 transport + +import ( + "context" + "net" + "net/http" + "net/url" + "testing" + "time" + + "google.golang.org/grpc/internal/proxyattributes" + "google.golang.org/grpc/internal/resolver/delegatingresolver" + "google.golang.org/grpc/resolver" +) + +func (s) TestHTTPConnectWithServerHello(t *testing.T) { + serverMessage := []byte("server-hello") + blis, err := net.Listen("tcp", "localhost:0") + if err != nil { + t.Fatalf("failed to listen: %v", err) + } + + pAddr, _ := SetupProxy(t, RequestCheck(blis.Addr().String()), true) + + msg := []byte{4, 3, 5, 2} + recvBuf := make([]byte, len(msg)) + done := make(chan error, 1) + go func() { + in, err := blis.Accept() + if err != nil { + done <- err + return + } + defer in.Close() + in.Write(serverMessage) + in.Read(recvBuf) + done <- nil + }() + + // Overwrite the function in the test and restore them in defer. + hpfe := func(req *http.Request) (*url.URL, error) { + return &url.URL{ + Scheme: "https", + Host: pAddr, + }, nil + } + orighpfe := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = hpfe + defer func() { + delegatingresolver.HTTPSProxyFromEnvironment = orighpfe + }() + + // Dial to proxy server. + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + c, err := proxyDial(ctx, resolver.Address{Addr: pAddr}, "test", proxyattributes.Options{ConnectAddr: blis.Addr().String()}) + if err != nil { + t.Fatalf("HTTP connect Dial failed: %v", err) + } + defer c.Close() + c.SetDeadline(time.Now().Add(defaultTestTimeout)) + + // Send msg on the connection. + c.Write(msg) + if err := <-done; err != nil { + t.Fatalf("Failed to accept: %v", err) + } + + // Check received msg. + if string(recvBuf) != string(msg) { + t.Fatalf("Received msg: %v, want %v", recvBuf, msg) + } + + if len(serverMessage) > 0 { + gotServerMessage := make([]byte, len(serverMessage)) + if _, err := c.Read(gotServerMessage); err != nil { + t.Errorf("Got error while reading message from server: %v", err) + return + } + if string(gotServerMessage) != string(serverMessage) { + t.Errorf("Message from server: %v, want %v", gotServerMessage, serverMessage) + } + } +} diff --git a/internal/transport/proxyutils.go b/internal/transport/proxyutils.go new file mode 100644 index 000000000000..193c60971a95 --- /dev/null +++ b/internal/transport/proxyutils.go @@ -0,0 +1,149 @@ +/* + * + * 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 transport + +import ( + "bufio" + "bytes" + "fmt" + "io" + "net" + "net/http" + "testing" + "time" + + "google.golang.org/grpc/internal/testutils" +) + +const defaultTestTimeout = 10 * time.Second + +// RequestCheck returns a function that checks the HTTP CONNECT request for the +// correct CONNECT method and address. +func RequestCheck(connectAddr string) func(*http.Request) error { + return func(req *http.Request) error { + if req.Method != http.MethodConnect { + return fmt.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) + } + if req.URL.Host != connectAddr { + return fmt.Errorf("unexpected URL.Host in CONNECT req %q, want %q", req.URL.Host, connectAddr) + } + return nil + } +} + +// ProxyServer represents a test proxy server. +type ProxyServer struct { + lis net.Listener + in net.Conn // Connection from the client to the proxy. + out net.Conn // Connection from the proxy to the backend. + requestCheck func(*http.Request) error // Function to check the request sent to proxy. +} + +// Stop closes the ProxyServer and its connections to client and server. +func (p *ProxyServer) stop() { + p.lis.Close() + if p.in != nil { + p.in.Close() + } + if p.out != nil { + p.out.Close() + } +} + +// Creates and starts a proxy server. +func newProxyServer(t *testing.T, lis net.Listener, reqCheck func(*http.Request) error, proxyStarted func(), waitForServerHello bool) *ProxyServer { + t.Helper() + p := &ProxyServer{ + lis: lis, + requestCheck: reqCheck, + } + + // Start the proxy server. + go func() { + in, err := p.lis.Accept() + if err != nil { + return + } + p.in = in + // This will be used in tests to check if the proxy server is started. + if proxyStarted != nil { + proxyStarted() + } + req, err := http.ReadRequest(bufio.NewReader(in)) + if err != nil { + t.Errorf("failed to read CONNECT req: %v", err) + return + } + if err := p.requestCheck(req); err != nil { + resp := http.Response{StatusCode: http.StatusMethodNotAllowed} + resp.Write(p.in) + p.in.Close() + t.Errorf("get wrong CONNECT req: %+v, error: %v", req, err) + return + } + + out, err := net.Dial("tcp", req.URL.Host) + if err != nil { + t.Errorf("failed to dial to server: %v", err) + return + } + out.SetDeadline(time.Now().Add(defaultTestTimeout)) + resp := http.Response{StatusCode: http.StatusOK, Proto: "HTTP/1.0"} + var buf bytes.Buffer + resp.Write(&buf) + + if waitForServerHello { + // Batch the first message from the server with the http connect + // response. This is done to test the cases in which the grpc client has + // the response to the connect request and proxied packets from the + // destination server when it reads the transport. + b := make([]byte, 50) + bytesRead, err := out.Read(b) + if err != nil { + t.Errorf("Got error while reading server hello: %v", err) + in.Close() + out.Close() + return + } + buf.Write(b[0:bytesRead]) + } + p.in.Write(buf.Bytes()) + p.out = out + go io.Copy(p.in, p.out) + go io.Copy(p.out, p.in) + }() + return p +} + +// SetupProxy initializes and starts a proxy server, registers a cleanup to +// stop it, and returns the proxy's listener and helper channels. +func SetupProxy(t *testing.T, reqCheck func(*http.Request) error, waitForServerHello bool) (string, chan struct{}) { + t.Helper() + pLis, err := net.Listen("tcp", "localhost:0") + if err != nil { + t.Fatalf("failed to listen: %v", err) + } + + proxyStartedCh := make(chan struct{}) + + proxyServer := newProxyServer(t, pLis, reqCheck, func() { close(proxyStartedCh) }, waitForServerHello) + t.Cleanup(proxyServer.stop) + + return fmt.Sprintf("localhost:%d", testutils.ParsePort(t, pLis.Addr().String())), proxyStartedCh +} diff --git a/test/proxy_test.go b/test/proxy_test.go deleted file mode 100644 index 7932d90dc63a..000000000000 --- a/test/proxy_test.go +++ /dev/null @@ -1,716 +0,0 @@ -/* - * - * 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 test - -import ( - "bufio" - "bytes" - "context" - "encoding/base64" - "fmt" - "io" - "net" - "net/http" - "net/url" - "os" - "testing" - "time" - - "golang.org/x/net/http/httpproxy" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" - "google.golang.org/grpc/internal/resolver/delegatingresolver" - "google.golang.org/grpc/internal/stubserver" - testgrpc "google.golang.org/grpc/interop/grpc_testing" - "google.golang.org/grpc/resolver" - "google.golang.org/grpc/resolver/manual" -) - -const ( - unresolvedTargetURI = "example.com" - unresolvedProxyURI = "proxyexample.com" -) - -func createAndStartBackendServer(t *testing.T) string { - t.Helper() - backend := &stubserver.StubServer{ - EmptyCallF: func(context.Context, *testgrpc.Empty) (*testgrpc.Empty, error) { return &testgrpc.Empty{}, nil }, - } - if err := backend.StartServer(); err != nil { - t.Fatalf("failed to start backend: %v", err) - } - t.Logf("Started TestService backend at: %q", backend.Address) - t.Cleanup(backend.Stop) - return backend.Address -} - -// setupDNS sets up a manual DNS resolver and registers it, returning the -// builder for test customization. -func setupDNS(t *testing.T) *manual.Resolver { - t.Helper() - mr := manual.NewBuilderWithScheme("dns") - origRes := resolver.Get("dns") - resolver.Register(mr) - t.Cleanup(func() { - resolver.Register(origRes) - }) - return mr -} - -// proxyServer represents a test proxy server. -type proxyServer struct { - lis net.Listener - in net.Conn // Connection from the client to the proxy. - out net.Conn // Connection from the proxy to the backend. - requestCheck func(*http.Request) error // Function to check the request sent to proxy. -} - -// Stop closes the ProxyServer and its connections to client and server. -func (p *proxyServer) stop() { - p.lis.Close() - if p.in != nil { - p.in.Close() - } - if p.out != nil { - p.out.Close() - } -} - -// Creates and starts a proxy server. -func newProxyServer(lis net.Listener, reqCheck func(*http.Request) error, errCh chan error, doneCh chan struct{}, backendAddr string, resOnClient bool, proxyStarted func()) *proxyServer { - p := &proxyServer{ - lis: lis, - requestCheck: reqCheck, - } - - // Start the proxy server. - go func() { - in, err := p.lis.Accept() - if err != nil { - return - } - p.in = in - // This will be used in tests to check if the proxy server is started. - if proxyStarted != nil { - proxyStarted() - } - req, err := http.ReadRequest(bufio.NewReader(in)) - if err != nil { - errCh <- fmt.Errorf("failed to read CONNECT req: %v", err) - return - } - if err := p.requestCheck(req); err != nil { - resp := http.Response{StatusCode: http.StatusMethodNotAllowed} - resp.Write(p.in) - p.in.Close() - errCh <- fmt.Errorf("get wrong CONNECT req: %+v, error: %v", req, err) - return - } - var out net.Conn - // If resolution is done on client,connect to address received in - // CONNECT request or else connect to backend address directly. This is - // to mimick the name resolution on proxy server. - if resOnClient { - out, err = net.Dial("tcp", req.URL.Host) - } else { - out, err = net.Dial("tcp", backendAddr) - } - if err != nil { - errCh <- fmt.Errorf("failed to dial to server: %v", err) - return - } - out.SetDeadline(time.Now().Add(defaultTestTimeout)) - - // Response OK to client - resp := http.Response{StatusCode: http.StatusOK, Proto: "HTTP/1.0"} - var buf bytes.Buffer - resp.Write(&buf) - p.in.Write(buf.Bytes()) - p.out = out - - // Perform the proxy function, i.e pass the data from client to server - // and server to client. - go io.Copy(p.in, p.out) - go io.Copy(p.out, p.in) - close(doneCh) - }() - return p -} - -// setupProxy initializes and starts a proxy server, registers a cleanup to -// stop it, and returns the proxy's listener and helper channels. -func setupProxy(t *testing.T, backendAddr string, resolutionOnClient bool, reqCheck func(*http.Request) error) (net.Listener, chan error, chan struct{}, chan struct{}) { - t.Helper() - pLis, err := net.Listen("tcp", "localhost:0") - if err != nil { - t.Fatalf("failed to listen: %v", err) - } - - errCh := make(chan error, 1) - doneCh := make(chan struct{}) - proxyStartedCh := make(chan struct{}) - t.Cleanup(func() { - close(errCh) - }) - - proxyServer := newProxyServer(pLis, reqCheck, errCh, doneCh, backendAddr, resolutionOnClient, func() { close(proxyStartedCh) }) - t.Cleanup(proxyServer.stop) - - return pLis, errCh, doneCh, proxyStartedCh -} - -// requestCheck returns a function that checks the HTTP CONNECT request for the -// correct CONNECT method and address. -func requestCheck(connectAddr string) func(*http.Request) error { - return func(req *http.Request) error { - if req.Method != http.MethodConnect { - return fmt.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) - } - if req.URL.Host != connectAddr { - return fmt.Errorf("unexpected URL.Host in CONNECT req %q, want %q", req.URL.Host, connectAddr) - } - return nil - } -} - -// Tests the scenario where grpc.Dial is performed using a proxy with the -// default resolver in the target URI. The test verifies that the connection is -// established to the proxy server, sends the unresolved target URI in the HTTP -// CONNECT request, and is successfully connected to the backend server. -func (s) TestGRPCDialWithProxy(t *testing.T) { - backendAddr := createAndStartBackendServer(t) - pLis, errCh, doneCh, _ := setupProxy(t, backendAddr, false, requestCheck(unresolvedTargetURI)) - - // Overwrite the proxy environment and restore it after the test. - proxyEnv := os.Getenv("HTTPS_PROXY") - os.Setenv("HTTPS_PROXY", pLis.Addr().String()) - defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() - - // Use the httpproxy package functions instead of `http.ProxyFromEnvironment` - // because the latter reads proxy-related environment variables only once at - // initialization. This behavior causes issues when running multiple tests - // with different proxy configurations, as changes to environment variables - // during tests would be ignored. By using `httpproxy.FromEnvironment()`, we - // ensure proxy settings are read dynamically in each test execution. - origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment - delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { - return httpproxy.FromEnvironment().ProxyFunc()(req.URL) - } - defer func() { - delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment - }() - - ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) - defer cancel() - conn, err := grpc.Dial(unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) - if err != nil { - t.Fatalf("grpc.Dial(%s) failed: %v", unresolvedTargetURI, err) - } - defer conn.Close() - - // Send an empty RPC to the backend through the proxy. - client := testgrpc.NewTestServiceClient(conn) - if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { - t.Errorf("EmptyCall failed: %v", err) - } - - select { - case err := <-errCh: - t.Fatalf("proxy server encountered an error: %v", err) - case <-doneCh: - t.Logf("proxy server succeeded") - } -} - -// Tests the scenario where `grpc.Dial` is performed with a proxy and the "dns" -// scheme for the target. The test verifies that the proxy URI is correctly -// resolved and that the target URI resolution on the client preserves the -// original behavior of `grpc.Dial`. It also ensures that a connection is -// established to the proxy server, with the resolved target URI sent in the -// HTTP CONNECT request, successfully connecting to the backend server. -func (s) TestGRPCDialWithDNSAndProxy(t *testing.T) { - backendAddr := createAndStartBackendServer(t) - pLis, errCh, doneCh, _ := setupProxy(t, backendAddr, false, requestCheck(backendAddr)) - - // Overwrite the proxy environment and restore it after the test. - proxyEnv := os.Getenv("HTTPS_PROXY") - os.Setenv("HTTPS_PROXY", unresolvedProxyURI) - defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() - - origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment - delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { - return httpproxy.FromEnvironment().ProxyFunc()(req.URL) - } - defer func() { - delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment - }() - - // Configure manual resolvers for both proxy and target backends - targetResolver := setupDNS(t) - targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) - - // Temporarily modify ProxyScheme for this test. - origScheme := delegatingresolver.ProxyScheme - delegatingresolver.ProxyScheme = "test" - t.Cleanup(func() { delegatingresolver.ProxyScheme = origScheme }) - - proxyResolver := manual.NewBuilderWithScheme("test") - resolver.Register(proxyResolver) - proxyResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: pLis.Addr().String()}}}) - - ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) - defer cancel() - conn, err := grpc.Dial(targetResolver.Scheme()+":///"+unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) - if err != nil { - t.Fatalf("grpc.Dial(%s) failed: %v", targetResolver.Scheme()+":///"+unresolvedTargetURI, err) - } - defer conn.Close() - - // Send an empty RPC to the backend through the proxy. - client := testgrpc.NewTestServiceClient(conn) - if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { - t.Errorf("EmptyCall failed: %v", err) - } - - // Verify that the proxy server encountered no errors. - select { - case err := <-errCh: - t.Fatalf("proxy server encountered an error: %v", err) - case <-doneCh: - t.Logf("proxy server succeeded") - } -} - -// Tests the scenario where grpc.NewClient is used with the default DNS -// resolver for the target URI and a proxy is configured. The test verifies -// that the client resolves proxy URI, connects to the proxy server, sends the -// unresolved target URI in the HTTP CONNECT request, and successfully -// establishes a connection to the backend server. -func (s) TestGRPCNewClientWithProxy(t *testing.T) { - backendAddr := createAndStartBackendServer(t) - pLis, errCh, doneCh, _ := setupProxy(t, backendAddr, false, requestCheck(unresolvedTargetURI)) - - // Overwrite the proxy environment and restore it after the test. - proxyEnv := os.Getenv("HTTPS_PROXY") - os.Setenv("HTTPS_PROXY", unresolvedProxyURI) - defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() - - origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment - delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { - return httpproxy.FromEnvironment().ProxyFunc()(req.URL) - } - defer func() { - delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment - }() - - // Set up and update a manual resolver for proxy resolution. - proxyResolver := setupDNS(t) - proxyResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: pLis.Addr().String()}}}) - - ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) - defer cancel() - conn, err := grpc.NewClient(unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) - if err != nil { - t.Fatalf("grpc.NewClient failed: %v", err) - } - defer conn.Close() - - // Send an empty RPC to the backend through the proxy. - client := testgrpc.NewTestServiceClient(conn) - if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { - t.Errorf("EmptyCall failed: %v", err) - } - - // Verify if the proxy server encountered any errors. - select { - case err := <-errCh: - t.Fatalf("proxy server encountered an error: %v", err) - case <-doneCh: - t.Logf("proxy server succeeded") - } -} - -// Tests the scenario where grpc.NewClient is used with a custom target URI -// scheme and a proxy is configured. The test verifies that the client -// successfully connects to the proxy server, resolves the proxy URI correctly, -// includes the resolved target URI in the HTTP CONNECT request, and -// establishes a connection to the backend server. -func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { - backendAddr := createAndStartBackendServer(t) - pLis, errCh, doneCh, _ := setupProxy(t, backendAddr, true, requestCheck(backendAddr)) - - // Overwrite the proxy environment and restore it after the test. - proxyEnv := os.Getenv("HTTPS_PROXY") - os.Setenv("HTTPS_PROXY", unresolvedProxyURI) - defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() - - origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment - delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { - return httpproxy.FromEnvironment().ProxyFunc()(req.URL) - } - defer func() { - delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment - }() - - // Create and update a custom resolver for target URI. - targetResolver := manual.NewBuilderWithScheme("test") - resolver.Register(targetResolver) - targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) - - // Set up and update a manual resolver for proxy resolution. - proxyResolver := setupDNS(t) - proxyResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: pLis.Addr().String()}}}) - - // Dial to the proxy server. - ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) - defer cancel() - conn, err := grpc.NewClient(targetResolver.Scheme()+":///"+unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) - if err != nil { - t.Fatalf("grpc.NewClient(%s) failed: %v", targetResolver.Scheme()+":///"+unresolvedTargetURI, err) - } - t.Cleanup(func() { conn.Close() }) - - // Send an empty RPC to the backend through the proxy. - client := testgrpc.NewTestServiceClient(conn) - if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { - t.Errorf("EmptyCall() failed: %v", err) - } - - // Verify if the proxy encountered any errors. - select { - case err := <-errCh: - t.Fatalf("proxy server encountered an error: %v", err) - case <-doneCh: - t.Logf("proxy server succeeded") - } -} - -// Tests the scenario where grpc.NewClient is used with a custom target URI -// scheme and a proxy is configured and both the resolvers return endpoints. -// The test verifies that the client successfully connects to the proxy server, -// resolves the proxy URI correctly, includes the resolved target URI in the -// HTTP CONNECT request, and establishes a connection to the backend server. -func (s) TestGRPCNewClientWithProxyAndCustomResolverWithEndpoints(t *testing.T) { - backendAddr := createAndStartBackendServer(t) - pLis, errCh, doneCh, _ := setupProxy(t, backendAddr, true, requestCheck(backendAddr)) - - // Overwrite the proxy environment and restore it after the test. - proxyEnv := os.Getenv("HTTPS_PROXY") - os.Setenv("HTTPS_PROXY", unresolvedProxyURI) - defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() - - origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment - delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { - return httpproxy.FromEnvironment().ProxyFunc()(req.URL) - } - defer func() { - delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment - }() - - // Create and update a custom resolver for target URI. - targetResolver := manual.NewBuilderWithScheme("test") - resolver.Register(targetResolver) - targetResolver.InitialState(resolver.State{Endpoints: []resolver.Endpoint{{Addresses: []resolver.Address{{Addr: backendAddr}}}}}) - - // Set up and update a manual resolver for proxy resolution. - proxyResolver := setupDNS(t) - proxyResolver.InitialState(resolver.State{Endpoints: []resolver.Endpoint{{Addresses: []resolver.Address{{Addr: pLis.Addr().String()}}}}}) - - // Dial to the proxy server. - ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) - defer cancel() - conn, err := grpc.NewClient(targetResolver.Scheme()+":///"+unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) - if err != nil { - t.Fatalf("grpc.NewClient(%s) failed: %v", targetResolver.Scheme()+":///"+unresolvedTargetURI, err) - } - t.Cleanup(func() { conn.Close() }) - - // Send an empty RPC to the backend through the proxy. - client := testgrpc.NewTestServiceClient(conn) - if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { - t.Errorf("EmptyCall() failed: %v", err) - } - - // Check if the proxy encountered any errors. - select { - case err := <-errCh: - t.Fatalf("proxy server encountered an error: %v", err) - case <-doneCh: - t.Logf("proxy server succeeded") - } -} - -// Tests the scenario where grpc.NewClient is used with the default "dns" -// resolver and the dial option grpc.WithTargetResolutionEnabled() is set, -// enabling target resolution on the client. The test verifies that target -// resolution happens on the client by sending resolved target URI in HTTP -// CONNECT request, the proxy URI is resolved correctly, and the connection is -// successfully established with the backend server through the proxy. -func (s) TestGRPCNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { - backendAddr := createAndStartBackendServer(t) - pLis, errCh, doneCh, _ := setupProxy(t, backendAddr, true, requestCheck(backendAddr)) - - // Overwrite the proxy environment and restore it after the test. - proxyEnv := os.Getenv("HTTPS_PROXY") - os.Setenv("HTTPS_PROXY", unresolvedProxyURI) - defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() - - origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment - delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { - return httpproxy.FromEnvironment().ProxyFunc()(req.URL) - } - defer func() { - delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment - }() - - // Configure manual resolvers for both proxy and target backends - targetResolver := setupDNS(t) - targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) - - // Temporarily modify ProxyScheme for this test. - origScheme := delegatingresolver.ProxyScheme - delegatingresolver.ProxyScheme = "test" - t.Cleanup(func() { delegatingresolver.ProxyScheme = origScheme }) - - proxyResolver := manual.NewBuilderWithScheme("test") - resolver.Register(proxyResolver) - proxyResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: pLis.Addr().String()}}}) - - dopts := []grpc.DialOption{ - grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithTargetResolutionEnabled(), // Target resolution on client enabled. - } - ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) - defer cancel() - conn, err := grpc.NewClient(unresolvedTargetURI, dopts...) - if err != nil { - t.Fatalf("grpc.NewClient(%s) failed: %v", unresolvedTargetURI, err) - } - t.Cleanup(func() { conn.Close() }) - - // Send an empty RPC to the backend through the proxy. - client := testgrpc.NewTestServiceClient(conn) - if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { - t.Errorf("EmptyCall() failed: %v", err) - } - - // Verify if the proxy encountered any errors. - select { - case err := <-errCh: - t.Fatalf("proxy server encountered an error: %v", err) - case <-doneCh: - t.Logf("proxy server succeeded") - } -} - -// Tests the scenario where grpc.NewClient is used with grpc.WithNoProxy() set, -// explicitly disabling proxy usage. The test verifies that the client does not -// dial the proxy but directly connects to the backend server. It also checks -// that the proxy resolution function is not called and that the proxy server -// never receives a connection request. -func (s) TestGRPCNewClientWithNoProxy(t *testing.T) { - backendAddr := createAndStartBackendServer(t) - _, _, _, proxyStartedCh := setupProxy(t, backendAddr, true, func(req *http.Request) error { - return fmt.Errorf("proxy server should not have received a Connect request: %v", req) - }) - - // Overwrite the proxy environment and restore it after the test. - proxyEnv := os.Getenv("HTTPS_PROXY") - os.Setenv("HTTPS_PROXY", unresolvedProxyURI) - defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() - - origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment - delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { - return httpproxy.FromEnvironment().ProxyFunc()(req.URL) - } - defer func() { - delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment - }() - - targetResolver := setupDNS(t) - targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) - - dopts := []grpc.DialOption{ - grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithNoProxy(), // Diable proxy. - } - ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) - defer cancel() - conn, err := grpc.NewClient(unresolvedTargetURI, dopts...) - if err != nil { - t.Fatalf("grpc.NewClient(%s) failed: %v", unresolvedTargetURI, err) - } - defer conn.Close() - - // Create a test service client and make an RPC call. - client := testgrpc.NewTestServiceClient(conn) - if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { - t.Errorf("EmptyCall() failed: %v", err) - } - - // Verify that the proxy server was not dialed. - select { - case <-proxyStartedCh: - t.Fatal("unexpected dial to proxy server") - default: - t.Log("proxy server was not dialed") - } -} - -// Tests the scenario where grpc.NewClient is used with grpc.WithContextDialer() -// set. The test verifies that the client bypasses proxy dialing and uses the -// custom dialer instead. It ensures that the proxy server is never dialed, the -// proxy resolution function is not triggered, and the custom dialer is invoked -// as expected. -func (s) TestGRPCNewClientWithContextDialer(t *testing.T) { - backendAddr := createAndStartBackendServer(t) - _, _, _, proxyStartedCh := setupProxy(t, backendAddr, true, func(req *http.Request) error { - return fmt.Errorf("proxy server should not have received a Connect request: %v", req) - }) - - // Overwrite the proxy environment and restore it after the test. - proxyEnv := os.Getenv("HTTPS_PROXY") - os.Setenv("HTTPS_PROXY", unresolvedProxyURI) - defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() - - origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment - delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { - return httpproxy.FromEnvironment().ProxyFunc()(req.URL) - } - defer func() { - delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment - }() - - // Create a custom dialer that directly dials the backend.ß - dialerCalled := make(chan struct{}) - customDialer := func(_ context.Context, backendAddr string) (net.Conn, error) { - close(dialerCalled) - return net.Dial("tcp", backendAddr) - } - - targetResolver := setupDNS(t) - targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) - - dopts := []grpc.DialOption{ - grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithContextDialer(customDialer), // Use a custom dialer. - } - ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) - defer cancel() - conn, err := grpc.NewClient(unresolvedTargetURI, dopts...) - if err != nil { - t.Fatalf("grpc.NewClient(%s) failed: %v", unresolvedTargetURI, err) - } - defer conn.Close() - - client := testgrpc.NewTestServiceClient(conn) - if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { - t.Errorf("EmptyCall() failed: %v", err) - } - - select { - case <-dialerCalled: - t.Log("custom dialer was invoked") - default: - t.Error("custom dialer was not invoked") - } - - select { - case <-proxyStartedCh: - t.Fatal("unexpected dial to proxy server") - default: - t.Log("proxy server was not dialled") - } -} - -// Tests the scenario where grpc.NewClient is used with the default DNS resolver -// for targetURI and a proxy. The test verifies that the client connects to the -// proxy server, sends the unresolved target URI in the HTTP CONNECT request, -// and successfully connects to the backend. Additionally, it checks that the -// correct user information is included in the Proxy-Authorization header of -// the CONNECT request. The test also ensures that target resolution does not -// happen on the client. -func (s) TestBasicAuthInGrpcNewClientWithProxy(t *testing.T) { - backendAddr := createAndStartBackendServer(t) - const ( - user = "notAUser" - password = "notAPassword" - ) - pLis, errCh, doneCh, _ := setupProxy(t, backendAddr, false, func(req *http.Request) error { - if req.Method != http.MethodConnect { - return fmt.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) - } - if req.URL.Host != unresolvedTargetURI { - return fmt.Errorf("unexpected URL.Host %q, want %q", req.URL.Host, unresolvedTargetURI) - } - wantProxyAuthStr := "Basic " + base64.StdEncoding.EncodeToString([]byte(user+":"+password)) - if got := req.Header.Get("Proxy-Authorization"); got != wantProxyAuthStr { - gotDecoded, err := base64.StdEncoding.DecodeString(got) - if err != nil { - return fmt.Errorf("failed to decode Proxy-Authorization header: %v", err) - } - wantDecoded, _ := base64.StdEncoding.DecodeString(wantProxyAuthStr) - return fmt.Errorf("unexpected auth %q (%q), want %q (%q)", got, gotDecoded, wantProxyAuthStr, wantDecoded) - } - return nil - }) - - // Overwrite the proxy environment and restore it after the test. - proxyEnv := os.Getenv("HTTPS_PROXY") - os.Setenv("HTTPS_PROXY", user+":"+password+"@"+unresolvedProxyURI) - defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }() - - origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment - delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { - return httpproxy.FromEnvironment().ProxyFunc()(req.URL) - } - defer func() { - delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment - }() - - // Set up and update a manual resolver for proxy resolution. - proxyResolver := setupDNS(t) - proxyResolver.InitialState(resolver.State{ - Addresses: []resolver.Address{ - {Addr: pLis.Addr().String()}, - }, - }) - - ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) - defer cancel() - conn, err := grpc.NewClient(unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) - if err != nil { - t.Fatalf("grpc.NewClient(%s) failed: %v", unresolvedTargetURI, err) - } - defer conn.Close() - - // Send an empty RPC to the backend through the proxy. - client := testgrpc.NewTestServiceClient(conn) - if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { - t.Errorf("EmptyCall failed: %v", err) - } - - // Verify if the proxy server encountered any errors. - select { - case err := <-errCh: - t.Fatalf("proxy server encountered an error: %v", err) - case <-doneCh: - t.Logf("proxy server succeeded") - } -} From 104cd18207bc78157df3d4a15c0def9e2482514f Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Fri, 27 Dec 2024 14:41:01 +0530 Subject: [PATCH 25/53] working tests manual resolver --- internal/transport/keepalive_test.go | 1 - internal/transport/proxy.go | 1 - internal/transport/proxy_ext_test.go | 326 +++++++++++------- internal/transport/proxy_test.go | 7 +- .../{proxyutils.go => proxy_utils.go} | 17 +- 5 files changed, 215 insertions(+), 137 deletions(-) rename internal/transport/{proxyutils.go => proxy_utils.go} (86%) diff --git a/internal/transport/keepalive_test.go b/internal/transport/keepalive_test.go index 037b0b1c1b77..6721ea3dbe42 100644 --- a/internal/transport/keepalive_test.go +++ b/internal/transport/keepalive_test.go @@ -43,7 +43,6 @@ import ( "google.golang.org/grpc/testdata" ) -const defaultTestTimeout = 10 * time.Second const defaultTestShortTimeout = 10 * time.Millisecond // TestMaxConnectionIdle tests that a server will send GoAway to an idle diff --git a/internal/transport/proxy.go b/internal/transport/proxy.go index 9d021adffa0a..4fb3da4ab4ac 100644 --- a/internal/transport/proxy.go +++ b/internal/transport/proxy.go @@ -101,7 +101,6 @@ func doHTTPConnectHandshake(ctx context.Context, conn net.Conn, grpcUA string, o // proxyDial establishes a TCP connection to the specified address and performs an HTTP CONNECT handshake. func proxyDial(ctx context.Context, addr resolver.Address, grpcUA string, opts proxyattributes.Options) (net.Conn, error) { - fmt.Printf("\nemchandwani : address : %v\n", addr.Addr) conn, err := internal.NetDialerWithTCPKeepalive().DialContext(ctx, "tcp", addr.Addr) if err != nil { return nil, err diff --git a/internal/transport/proxy_ext_test.go b/internal/transport/proxy_ext_test.go index 66f1bc941644..afaf23779094 100644 --- a/internal/transport/proxy_ext_test.go +++ b/internal/transport/proxy_ext_test.go @@ -28,18 +28,30 @@ import ( "testing" "time" + "golang.org/x/net/http/httpproxy" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/internal/grpctest" "google.golang.org/grpc/internal/resolver/delegatingresolver" "google.golang.org/grpc/internal/stubserver" - "google.golang.org/grpc/internal/testutils" "google.golang.org/grpc/internal/transport" testgrpc "google.golang.org/grpc/interop/grpc_testing" "google.golang.org/grpc/resolver" "google.golang.org/grpc/resolver/manual" ) +const ( + unresolvedTargetURI = "example.com" + unresolvedProxyURI = "proxyexample.com" + defaultTestTimeout = 10 * time.Second +) + +// dnsCache is a map used in the proxy server to mimic DNS name resolution. +// It maps unresolved hostnames to their resolved addresses. If an entry +// is found in the cache, the proxy server uses it; otherwise, it defaults +// to the original hostname for connection. +var dnsCache = make(map[string]string) + type s struct { grpctest.Tester } @@ -48,10 +60,7 @@ func Test(t *testing.T) { grpctest.RunSubTests(t, s{}) } -const defaultTestTimeout = 10 * time.Second - -// Starts a StubServer and returns the server's address in both IPv6 ("[::1]:port") and unresolved ("localhost:port") formats. -func createAndStartBackendServer(t *testing.T) (string, string) { +func createAndStartBackendServer(t *testing.T) string { t.Helper() backend := &stubserver.StubServer{ EmptyCallF: func(context.Context, *testgrpc.Empty) (*testgrpc.Empty, error) { return &testgrpc.Empty{}, nil }, @@ -61,8 +70,20 @@ func createAndStartBackendServer(t *testing.T) (string, string) { } t.Logf("Started TestService backend at: %q", backend.Address) t.Cleanup(backend.Stop) - port := testutils.ParsePort(t, backend.Address) - return backend.Address, fmt.Sprintf("localhost:%d", port) + return backend.Address +} + +// setupDNS sets up a manual DNS resolver and registers it, returning the +// builder for test customization. +func setupDNS(t *testing.T) *manual.Resolver { + t.Helper() + mr := manual.NewBuilderWithScheme("dns") + origRes := resolver.Get("dns") + resolver.Register(mr) + t.Cleanup(func() { + resolver.Register(origRes) + }) + return mr } // Tests the scenario where grpc.Dial is performed using a proxy with the @@ -70,22 +91,25 @@ func createAndStartBackendServer(t *testing.T) (string, string) { // established to the proxy server, sends the unresolved target URI in the HTTP // CONNECT request, and is successfully connected to the backend server. func (s) TestGRPCDialWithProxy(t *testing.T) { - _, unresolvedTargetURI := createAndStartBackendServer(t) - unresolvedProxyURI, _ := transport.SetupProxy(t, transport.RequestCheck(unresolvedTargetURI), false) - - hpfe := func(req *http.Request) (*url.URL, error) { - if req.URL.Host == unresolvedTargetURI { - return &url.URL{ - Scheme: "https", - Host: unresolvedProxyURI, - }, nil - } - return nil, nil + backendAddr := createAndStartBackendServer(t) + dnsCache := map[string]string{unresolvedTargetURI: backendAddr} + pLis, _ := transport.SetupProxy(t, dnsCache, transport.RequestCheck(unresolvedTargetURI), false) + + // Overwrite the proxy environment and restore it after the test. + t.Setenv("HTTPS_PROXY", pLis.Addr().String()) + + // Use the httpproxy package functions instead of `http.ProxyFromEnvironment` + // because the latter reads proxy-related environment variables only once at + // initialization. This behavior causes issues when running multiple tests + // with different proxy configurations, as changes to environment variables + // during tests would be ignored. By using `httpproxy.FromEnvironment()`, we + // ensure proxy settings are read dynamically in each test execution. + origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { + return httpproxy.FromEnvironment().ProxyFunc()(req.URL) } - orighpfe := delegatingresolver.HTTPSProxyFromEnvironment - delegatingresolver.HTTPSProxyFromEnvironment = hpfe defer func() { - delegatingresolver.HTTPSProxyFromEnvironment = orighpfe + delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment }() ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) @@ -99,7 +123,7 @@ func (s) TestGRPCDialWithProxy(t *testing.T) { // Send an empty RPC to the backend through the proxy. client := testgrpc.NewTestServiceClient(conn) if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { - t.Fatalf("EmptyCall failed: %v", err) + t.Errorf("EmptyCall failed: %v", err) } } @@ -109,41 +133,46 @@ func (s) TestGRPCDialWithProxy(t *testing.T) { // original behavior of `grpc.Dial`. It also ensures that a connection is // established to the proxy server, with the resolved target URI sent in the // HTTP CONNECT request, successfully connecting to the backend server. -func TestGRPCDialWithDNSAndProxy(t *testing.T) { - backendAddr, unresolvedTargetURI := createAndStartBackendServer(t) - unresolvedProxyURI, _ := transport.SetupProxy(t, transport.RequestCheck(backendAddr), false) +func (s) TestGRPCDialWithDNSAndProxy(t *testing.T) { + backendAddr := createAndStartBackendServer(t) + pLis, _ := transport.SetupProxy(t, dnsCache, transport.RequestCheck(backendAddr), false) // Overwrite the proxy environment and restore it after the test. - hpfe := func(req *http.Request) (*url.URL, error) { - if req.URL.Host == unresolvedTargetURI { - return &url.URL{ - Scheme: "https", - Host: unresolvedProxyURI, - }, nil - } - return nil, nil + t.Setenv("HTTPS_PROXY", unresolvedProxyURI) + + origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { + return httpproxy.FromEnvironment().ProxyFunc()(req.URL) } - orighpfe := delegatingresolver.HTTPSProxyFromEnvironment - delegatingresolver.HTTPSProxyFromEnvironment = hpfe defer func() { - delegatingresolver.HTTPSProxyFromEnvironment = orighpfe + delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment }() + // Configure manual resolvers for both proxy and target backends targetResolver := setupDNS(t) targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) + // Temporarily modify ProxyScheme for this test. + origScheme := delegatingresolver.ProxyScheme + delegatingresolver.ProxyScheme = "test" + t.Cleanup(func() { delegatingresolver.ProxyScheme = origScheme }) + + proxyResolver := manual.NewBuilderWithScheme("test") + resolver.Register(proxyResolver) + proxyResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: pLis.Addr().String()}}}) + ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() - conn, err := grpc.Dial("dns:///"+unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) + conn, err := grpc.Dial(targetResolver.Scheme()+":///"+unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { - t.Fatalf("grpc.Dial(%s) failed: %v", "dns:///"+unresolvedTargetURI, err) + t.Fatalf("grpc.Dial(%s) failed: %v", targetResolver.Scheme()+":///"+unresolvedTargetURI, err) } defer conn.Close() // Send an empty RPC to the backend through the proxy. client := testgrpc.NewTestServiceClient(conn) if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { - t.Fatalf("EmptyCall failed: %v", err) + t.Errorf("EmptyCall failed: %v", err) } } @@ -153,25 +182,25 @@ func TestGRPCDialWithDNSAndProxy(t *testing.T) { // unresolved target URI in the HTTP CONNECT request, and successfully // establishes a connection to the backend server. func (s) TestGRPCNewClientWithProxy(t *testing.T) { - _, unresolvedTargetURI := createAndStartBackendServer(t) - unresolvedProxyURI, _ := transport.SetupProxy(t, transport.RequestCheck(unresolvedTargetURI), false) + backendAddr := createAndStartBackendServer(t) + dnsCache := map[string]string{unresolvedTargetURI: backendAddr} + pLis, _ := transport.SetupProxy(t, dnsCache, transport.RequestCheck(unresolvedTargetURI), false) // Overwrite the proxy environment and restore it after the test. - hpfe := func(req *http.Request) (*url.URL, error) { - if req.URL.Host == unresolvedTargetURI { - return &url.URL{ - Scheme: "https", - Host: unresolvedProxyURI, - }, nil - } - return nil, nil + t.Setenv("HTTPS_PROXY", unresolvedProxyURI) + + origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { + return httpproxy.FromEnvironment().ProxyFunc()(req.URL) } - orighpfe := delegatingresolver.HTTPSProxyFromEnvironment - delegatingresolver.HTTPSProxyFromEnvironment = hpfe defer func() { - delegatingresolver.HTTPSProxyFromEnvironment = orighpfe + delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment }() + // Set up and update a manual resolver for proxy resolution. + proxyResolver := setupDNS(t) + proxyResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: pLis.Addr().String()}}}) + ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() conn, err := grpc.NewClient(unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) @@ -193,23 +222,18 @@ func (s) TestGRPCNewClientWithProxy(t *testing.T) { // includes the resolved target URI in the HTTP CONNECT request, and // establishes a connection to the backend server. func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { - unresolvedTargetURI, backendAddr := createAndStartBackendServer(t) - unresolvedProxyURI, _ := transport.SetupProxy(t, transport.RequestCheck(backendAddr), false) + backendAddr := createAndStartBackendServer(t) + pLis, _ := transport.SetupProxy(t, dnsCache, transport.RequestCheck(backendAddr), false) // Overwrite the proxy environment and restore it after the test. - hpfe := func(req *http.Request) (*url.URL, error) { - if req.URL.Host == unresolvedTargetURI { - return &url.URL{ - Scheme: "https", - Host: unresolvedProxyURI, - }, nil - } - return nil, nil + t.Setenv("HTTPS_PROXY", unresolvedProxyURI) + + origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { + return httpproxy.FromEnvironment().ProxyFunc()(req.URL) } - orighpfe := delegatingresolver.HTTPSProxyFromEnvironment - delegatingresolver.HTTPSProxyFromEnvironment = hpfe defer func() { - delegatingresolver.HTTPSProxyFromEnvironment = orighpfe + delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment }() // Create and update a custom resolver for target URI. @@ -217,7 +241,56 @@ func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { resolver.Register(targetResolver) targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) - /// Create a client with the custom resolver. + // Set up and update a manual resolver for proxy resolution. + proxyResolver := setupDNS(t) + proxyResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: pLis.Addr().String()}}}) + + // Dial to the proxy server. + ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) + defer cancel() + conn, err := grpc.NewClient(targetResolver.Scheme()+":///"+unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + t.Fatalf("grpc.NewClient(%s) failed: %v", targetResolver.Scheme()+":///"+unresolvedTargetURI, err) + } + t.Cleanup(func() { conn.Close() }) + + // Send an empty RPC to the backend through the proxy. + client := testgrpc.NewTestServiceClient(conn) + if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { + t.Errorf("EmptyCall() failed: %v", err) + } +} + +// Tests the scenario where grpc.NewClient is used with a custom target URI +// scheme and a proxy is configured and both the resolvers return endpoints. +// The test verifies that the client successfully connects to the proxy server, +// resolves the proxy URI correctly, includes the resolved target URI in the +// HTTP CONNECT request, and establishes a connection to the backend server. +func (s) TestGRPCNewClientWithProxyAndCustomResolverWithEndpoints(t *testing.T) { + backendAddr := createAndStartBackendServer(t) + pLis, _ := transport.SetupProxy(t, dnsCache, transport.RequestCheck(backendAddr), false) + + // Overwrite the proxy environment and restore it after the test. + t.Setenv("HTTPS_PROXY", unresolvedProxyURI) + + origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { + return httpproxy.FromEnvironment().ProxyFunc()(req.URL) + } + defer func() { + delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment + }() + + // Create and update a custom resolver for target URI. + targetResolver := manual.NewBuilderWithScheme("test") + resolver.Register(targetResolver) + targetResolver.InitialState(resolver.State{Endpoints: []resolver.Endpoint{{Addresses: []resolver.Address{{Addr: backendAddr}}}}}) + + // Set up and update a manual resolver for proxy resolution. + proxyResolver := setupDNS(t) + proxyResolver.InitialState(resolver.State{Endpoints: []resolver.Endpoint{{Addresses: []resolver.Address{{Addr: pLis.Addr().String()}}}}}) + + // Dial to the proxy server. ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() conn, err := grpc.NewClient(targetResolver.Scheme()+":///"+unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) @@ -229,7 +302,7 @@ func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { // Send an empty RPC to the backend through the proxy. client := testgrpc.NewTestServiceClient(conn) if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { - t.Fatalf("EmptyCall() failed: %v", err) + t.Errorf("EmptyCall() failed: %v", err) } } @@ -240,25 +313,33 @@ func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { // CONNECT request, the proxy URI is resolved correctly, and the connection is // successfully established with the backend server through the proxy. func (s) TestGRPCNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { - backendAddr, unresolvedTargetURI := createAndStartBackendServer(t) - unresolvedProxyURI, _ := transport.SetupProxy(t, transport.RequestCheck(backendAddr), false) + backendAddr := createAndStartBackendServer(t) + pLis, _ := transport.SetupProxy(t, dnsCache, transport.RequestCheck(backendAddr), false) // Overwrite the proxy environment and restore it after the test. - hpfe := func(req *http.Request) (*url.URL, error) { - if req.URL.Host == unresolvedTargetURI { - return &url.URL{ - Scheme: "https", - Host: unresolvedProxyURI, - }, nil - } - return nil, nil + t.Setenv("HTTPS_PROXY", unresolvedProxyURI) + + origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { + return httpproxy.FromEnvironment().ProxyFunc()(req.URL) } - orighpfe := delegatingresolver.HTTPSProxyFromEnvironment - delegatingresolver.HTTPSProxyFromEnvironment = hpfe defer func() { - delegatingresolver.HTTPSProxyFromEnvironment = orighpfe + delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment }() + // Configure manual resolvers for both proxy and target backends + targetResolver := setupDNS(t) + targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) + + // Temporarily modify ProxyScheme for this test. + origScheme := delegatingresolver.ProxyScheme + delegatingresolver.ProxyScheme = "test" + t.Cleanup(func() { delegatingresolver.ProxyScheme = origScheme }) + + proxyResolver := manual.NewBuilderWithScheme("test") + resolver.Register(proxyResolver) + proxyResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: pLis.Addr().String()}}}) + dopts := []grpc.DialOption{ grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithTargetResolutionEnabled(), // Target resolution on client enabled. @@ -284,30 +365,28 @@ func (s) TestGRPCNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { // that the proxy resolution function is not called and that the proxy server // never receives a connection request. func (s) TestGRPCNewClientWithNoProxy(t *testing.T) { - unresolvedTargetURI, _ := createAndStartBackendServer(t) - unresolvedProxyURI, proxyStartedCh := transport.SetupProxy(t, func(req *http.Request) error { + backendAddr := createAndStartBackendServer(t) + _, proxyStartedCh := transport.SetupProxy(t, dnsCache, func(req *http.Request) error { return fmt.Errorf("proxy server should not have received a Connect request: %v", req) }, false) // Overwrite the proxy environment and restore it after the test. - hpfe := func(req *http.Request) (*url.URL, error) { - if req.URL.Host == unresolvedTargetURI { - return &url.URL{ - Scheme: "https", - Host: unresolvedProxyURI, - }, nil - } - return nil, nil + t.Setenv("HTTPS_PROXY", unresolvedProxyURI) + + origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { + return httpproxy.FromEnvironment().ProxyFunc()(req.URL) } - orighpfe := delegatingresolver.HTTPSProxyFromEnvironment - delegatingresolver.HTTPSProxyFromEnvironment = hpfe defer func() { - delegatingresolver.HTTPSProxyFromEnvironment = orighpfe + delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment }() + targetResolver := setupDNS(t) + targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) + dopts := []grpc.DialOption{ grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithNoProxy(), // Disable proxy. + grpc.WithNoProxy(), // Diable proxy. } ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() @@ -338,25 +417,20 @@ func (s) TestGRPCNewClientWithNoProxy(t *testing.T) { // proxy resolution function is not triggered, and the custom dialer is invoked // as expected. func (s) TestGRPCNewClientWithContextDialer(t *testing.T) { - unresolvedTargetURI, _ := createAndStartBackendServer(t) - unresolvedProxyURI, proxyStartedCh := transport.SetupProxy(t, func(req *http.Request) error { + backendAddr := createAndStartBackendServer(t) + _, proxyStartedCh := transport.SetupProxy(t, dnsCache, func(req *http.Request) error { return fmt.Errorf("proxy server should not have received a Connect request: %v", req) }, false) // Overwrite the proxy environment and restore it after the test. - hpfe := func(req *http.Request) (*url.URL, error) { - if req.URL.Host == unresolvedTargetURI { - return &url.URL{ - Scheme: "https", - Host: unresolvedProxyURI, - }, nil - } - return nil, nil + t.Setenv("HTTPS_PROXY", unresolvedProxyURI) + + origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { + return httpproxy.FromEnvironment().ProxyFunc()(req.URL) } - orighpfe := delegatingresolver.HTTPSProxyFromEnvironment - delegatingresolver.HTTPSProxyFromEnvironment = hpfe defer func() { - delegatingresolver.HTTPSProxyFromEnvironment = orighpfe + delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment }() // Create a custom dialer that directly dials the backend.ß @@ -366,6 +440,9 @@ func (s) TestGRPCNewClientWithContextDialer(t *testing.T) { return net.Dial("tcp", backendAddr) } + targetResolver := setupDNS(t) + targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) + dopts := []grpc.DialOption{ grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithContextDialer(customDialer), // Use a custom dialer. @@ -386,8 +463,8 @@ func (s) TestGRPCNewClientWithContextDialer(t *testing.T) { select { case <-dialerCalled: t.Log("custom dialer was invoked") - case <-time.After(defaultTestTimeout): - t.Fatalf("test timed out waiting for custom dialer to be called") + default: + t.Error("custom dialer was not invoked") } select { @@ -406,12 +483,13 @@ func (s) TestGRPCNewClientWithContextDialer(t *testing.T) { // the CONNECT request. The test also ensures that target resolution does not // happen on the client. func (s) TestBasicAuthInGrpcNewClientWithProxy(t *testing.T) { - _, unresolvedTargetURI := createAndStartBackendServer(t) + backendAddr := createAndStartBackendServer(t) const ( user = "notAUser" password = "notAPassword" ) - unresolvedProxyURI, _ := transport.SetupProxy(t, func(req *http.Request) error { + dnsCache := map[string]string{unresolvedTargetURI: backendAddr} + pLis, _ := transport.SetupProxy(t, dnsCache, func(req *http.Request) error { if req.Method != http.MethodConnect { return fmt.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) } @@ -431,22 +509,24 @@ func (s) TestBasicAuthInGrpcNewClientWithProxy(t *testing.T) { }, false) // Overwrite the proxy environment and restore it after the test. - hpfe := func(req *http.Request) (*url.URL, error) { - if req.URL.Host == unresolvedTargetURI { - return &url.URL{ - Scheme: "https", - User: url.UserPassword(user, password), - Host: unresolvedProxyURI, - }, nil - } - return nil, nil + t.Setenv("HTTPS_PROXY", user+":"+password+"@"+unresolvedProxyURI) + + origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { + return httpproxy.FromEnvironment().ProxyFunc()(req.URL) } - orighpfe := delegatingresolver.HTTPSProxyFromEnvironment - delegatingresolver.HTTPSProxyFromEnvironment = hpfe defer func() { - delegatingresolver.HTTPSProxyFromEnvironment = orighpfe + delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment }() + // Set up and update a manual resolver for proxy resolution. + proxyResolver := setupDNS(t) + proxyResolver.InitialState(resolver.State{ + Addresses: []resolver.Address{ + {Addr: pLis.Addr().String()}, + }, + }) + ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() conn, err := grpc.NewClient(unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) diff --git a/internal/transport/proxy_test.go b/internal/transport/proxy_test.go index c3839852c27f..c47d6349c659 100644 --- a/internal/transport/proxy_test.go +++ b/internal/transport/proxy_test.go @@ -40,8 +40,7 @@ func (s) TestHTTPConnectWithServerHello(t *testing.T) { if err != nil { t.Fatalf("failed to listen: %v", err) } - - pAddr, _ := SetupProxy(t, RequestCheck(blis.Addr().String()), true) + pLis, _ := SetupProxy(t, map[string]string{}, RequestCheck(blis.Addr().String()), true) msg := []byte{4, 3, 5, 2} recvBuf := make([]byte, len(msg)) @@ -62,7 +61,7 @@ func (s) TestHTTPConnectWithServerHello(t *testing.T) { hpfe := func(req *http.Request) (*url.URL, error) { return &url.URL{ Scheme: "https", - Host: pAddr, + Host: pLis.Addr().String(), }, nil } orighpfe := delegatingresolver.HTTPSProxyFromEnvironment @@ -74,7 +73,7 @@ func (s) TestHTTPConnectWithServerHello(t *testing.T) { // Dial to proxy server. ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() - c, err := proxyDial(ctx, resolver.Address{Addr: pAddr}, "test", proxyattributes.Options{ConnectAddr: blis.Addr().String()}) + c, err := proxyDial(ctx, resolver.Address{Addr: pLis.Addr().String()}, "test", proxyattributes.Options{ConnectAddr: blis.Addr().String()}) if err != nil { t.Fatalf("HTTP connect Dial failed: %v", err) } diff --git a/internal/transport/proxyutils.go b/internal/transport/proxy_utils.go similarity index 86% rename from internal/transport/proxyutils.go rename to internal/transport/proxy_utils.go index 193c60971a95..e542c8084eba 100644 --- a/internal/transport/proxyutils.go +++ b/internal/transport/proxy_utils.go @@ -27,8 +27,6 @@ import ( "net/http" "testing" "time" - - "google.golang.org/grpc/internal/testutils" ) const defaultTestTimeout = 10 * time.Second @@ -67,7 +65,7 @@ func (p *ProxyServer) stop() { } // Creates and starts a proxy server. -func newProxyServer(t *testing.T, lis net.Listener, reqCheck func(*http.Request) error, proxyStarted func(), waitForServerHello bool) *ProxyServer { +func newProxyServer(t *testing.T, lis net.Listener, dnsCache map[string]string, reqCheck func(*http.Request) error, proxyStarted func(), waitForServerHello bool) *ProxyServer { t.Helper() p := &ProxyServer{ lis: lis, @@ -97,8 +95,11 @@ func newProxyServer(t *testing.T, lis net.Listener, reqCheck func(*http.Request) t.Errorf("get wrong CONNECT req: %+v, error: %v", req, err) return } - - out, err := net.Dial("tcp", req.URL.Host) + addr := dnsCache[req.URL.Host] + if addr == "" { + addr = req.URL.Host + } + out, err := net.Dial("tcp", addr) if err != nil { t.Errorf("failed to dial to server: %v", err) return @@ -133,7 +134,7 @@ func newProxyServer(t *testing.T, lis net.Listener, reqCheck func(*http.Request) // SetupProxy initializes and starts a proxy server, registers a cleanup to // stop it, and returns the proxy's listener and helper channels. -func SetupProxy(t *testing.T, reqCheck func(*http.Request) error, waitForServerHello bool) (string, chan struct{}) { +func SetupProxy(t *testing.T, dnsCache map[string]string, reqCheck func(*http.Request) error, waitForServerHello bool) (net.Listener, chan struct{}) { t.Helper() pLis, err := net.Listen("tcp", "localhost:0") if err != nil { @@ -142,8 +143,8 @@ func SetupProxy(t *testing.T, reqCheck func(*http.Request) error, waitForServerH proxyStartedCh := make(chan struct{}) - proxyServer := newProxyServer(t, pLis, reqCheck, func() { close(proxyStartedCh) }, waitForServerHello) + proxyServer := newProxyServer(t, pLis, dnsCache, reqCheck, func() { close(proxyStartedCh) }, waitForServerHello) t.Cleanup(proxyServer.stop) - return fmt.Sprintf("localhost:%d", testutils.ParsePort(t, pLis.Addr().String())), proxyStartedCh + return pLis, proxyStartedCh } From fb23cca583220537feca980a6a68c0d1193dde2e Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Fri, 27 Dec 2024 14:53:54 +0530 Subject: [PATCH 26/53] working tests manual resolver --- internal/transport/proxy_test.go | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/internal/transport/proxy_test.go b/internal/transport/proxy_test.go index c47d6349c659..aff5829e4f3f 100644 --- a/internal/transport/proxy_test.go +++ b/internal/transport/proxy_test.go @@ -29,6 +29,7 @@ import ( "testing" "time" + "golang.org/x/net/http/httpproxy" "google.golang.org/grpc/internal/proxyattributes" "google.golang.org/grpc/internal/resolver/delegatingresolver" "google.golang.org/grpc/resolver" @@ -58,16 +59,14 @@ func (s) TestHTTPConnectWithServerHello(t *testing.T) { }() // Overwrite the function in the test and restore them in defer. - hpfe := func(req *http.Request) (*url.URL, error) { - return &url.URL{ - Scheme: "https", - Host: pLis.Addr().String(), - }, nil + t.Setenv("HTTPS_PROXY", pLis.Addr().String()) + + origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { + return httpproxy.FromEnvironment().ProxyFunc()(req.URL) } - orighpfe := delegatingresolver.HTTPSProxyFromEnvironment - delegatingresolver.HTTPSProxyFromEnvironment = hpfe defer func() { - delegatingresolver.HTTPSProxyFromEnvironment = orighpfe + delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment }() // Dial to proxy server. From ef927ceaf018dc0daf01decffc847a87b6bd035d Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Fri, 27 Dec 2024 15:14:33 +0530 Subject: [PATCH 27/53] test --- clientconn.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/clientconn.go b/clientconn.go index ba574e4b5eb0..07e97fed5710 100644 --- a/clientconn.go +++ b/clientconn.go @@ -226,10 +226,10 @@ func DialContext(ctx context.Context, target string, opts ...DialOption) (conn * // At the end of this method, we kick the channel out of idle, rather than // waiting for the first rpc. // - // WithTargetResolutionEnabled in `grpc.Dial` ensures that it preserves - // behavior: when default scheme passthrough is used, skip hostname - // resolution, when "dns" is used for resolution, - // perform resolution on the client. + // WithTargetResolutionEnabled dial option in `grpc.Dial` ensures that it + // preserves behavior: when default scheme passthrough is used, skip + // hostname resolution, when "dns" is used for resolution, perform + // resolution on the client. opts = append([]DialOption{withDefaultScheme("passthrough"), WithTargetResolutionEnabled()}, opts...) cc, err := NewClient(target, opts...) if err != nil { From bc5efc39a9eb12a42e6620919486a75c1cae0794 Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Thu, 2 Jan 2025 16:51:17 +0530 Subject: [PATCH 28/53] trying test without manual resolver --- internal/transport/proxy_ext_test.go | 459 +++++++++++++-------------- internal/transport/proxy_test.go | 4 +- internal/transport/proxy_utils.go | 118 ++++--- 3 files changed, 297 insertions(+), 284 deletions(-) diff --git a/internal/transport/proxy_ext_test.go b/internal/transport/proxy_ext_test.go index afaf23779094..b7bafaaa8d5b 100644 --- a/internal/transport/proxy_ext_test.go +++ b/internal/transport/proxy_ext_test.go @@ -24,6 +24,7 @@ import ( "fmt" "net" "net/http" + "net/netip" "net/url" "testing" "time" @@ -34,6 +35,7 @@ import ( "google.golang.org/grpc/internal/grpctest" "google.golang.org/grpc/internal/resolver/delegatingresolver" "google.golang.org/grpc/internal/stubserver" + "google.golang.org/grpc/internal/testutils" "google.golang.org/grpc/internal/transport" testgrpc "google.golang.org/grpc/interop/grpc_testing" "google.golang.org/grpc/resolver" @@ -70,7 +72,8 @@ func createAndStartBackendServer(t *testing.T) string { } t.Logf("Started TestService backend at: %q", backend.Address) t.Cleanup(backend.Stop) - return backend.Address + // return backend.Address + return fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backend.Address)) } // setupDNS sets up a manual DNS resolver and registers it, returning the @@ -90,221 +93,210 @@ func setupDNS(t *testing.T) *manual.Resolver { // default resolver in the target URI. The test verifies that the connection is // established to the proxy server, sends the unresolved target URI in the HTTP // CONNECT request, and is successfully connected to the backend server. -func (s) TestGRPCDialWithProxy(t *testing.T) { - backendAddr := createAndStartBackendServer(t) - dnsCache := map[string]string{unresolvedTargetURI: backendAddr} - pLis, _ := transport.SetupProxy(t, dnsCache, transport.RequestCheck(unresolvedTargetURI), false) - - // Overwrite the proxy environment and restore it after the test. - t.Setenv("HTTPS_PROXY", pLis.Addr().String()) - - // Use the httpproxy package functions instead of `http.ProxyFromEnvironment` - // because the latter reads proxy-related environment variables only once at - // initialization. This behavior causes issues when running multiple tests - // with different proxy configurations, as changes to environment variables - // during tests would be ignored. By using `httpproxy.FromEnvironment()`, we - // ensure proxy settings are read dynamically in each test execution. - origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment - delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { - return httpproxy.FromEnvironment().ProxyFunc()(req.URL) - } - defer func() { - delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment - }() - - ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) - defer cancel() - conn, err := grpc.Dial(unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) - if err != nil { - t.Fatalf("grpc.Dial(%s) failed: %v", unresolvedTargetURI, err) - } - defer conn.Close() - - // Send an empty RPC to the backend through the proxy. - client := testgrpc.NewTestServiceClient(conn) - if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { - t.Errorf("EmptyCall failed: %v", err) - } -} - -// Tests the scenario where `grpc.Dial` is performed with a proxy and the "dns" -// scheme for the target. The test verifies that the proxy URI is correctly -// resolved and that the target URI resolution on the client preserves the -// original behavior of `grpc.Dial`. It also ensures that a connection is -// established to the proxy server, with the resolved target URI sent in the -// HTTP CONNECT request, successfully connecting to the backend server. -func (s) TestGRPCDialWithDNSAndProxy(t *testing.T) { - backendAddr := createAndStartBackendServer(t) - pLis, _ := transport.SetupProxy(t, dnsCache, transport.RequestCheck(backendAddr), false) - - // Overwrite the proxy environment and restore it after the test. - t.Setenv("HTTPS_PROXY", unresolvedProxyURI) - - origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment - delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { - return httpproxy.FromEnvironment().ProxyFunc()(req.URL) - } - defer func() { - delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment - }() - - // Configure manual resolvers for both proxy and target backends - targetResolver := setupDNS(t) - targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) - - // Temporarily modify ProxyScheme for this test. - origScheme := delegatingresolver.ProxyScheme - delegatingresolver.ProxyScheme = "test" - t.Cleanup(func() { delegatingresolver.ProxyScheme = origScheme }) - - proxyResolver := manual.NewBuilderWithScheme("test") - resolver.Register(proxyResolver) - proxyResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: pLis.Addr().String()}}}) - - ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) - defer cancel() - conn, err := grpc.Dial(targetResolver.Scheme()+":///"+unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) - if err != nil { - t.Fatalf("grpc.Dial(%s) failed: %v", targetResolver.Scheme()+":///"+unresolvedTargetURI, err) - } - defer conn.Close() - - // Send an empty RPC to the backend through the proxy. - client := testgrpc.NewTestServiceClient(conn) - if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { - t.Errorf("EmptyCall failed: %v", err) - } -} - -// Tests the scenario where grpc.NewClient is used with the default DNS -// resolver for the target URI and a proxy is configured. The test verifies -// that the client resolves proxy URI, connects to the proxy server, sends the -// unresolved target URI in the HTTP CONNECT request, and successfully -// establishes a connection to the backend server. -func (s) TestGRPCNewClientWithProxy(t *testing.T) { - backendAddr := createAndStartBackendServer(t) - dnsCache := map[string]string{unresolvedTargetURI: backendAddr} - pLis, _ := transport.SetupProxy(t, dnsCache, transport.RequestCheck(unresolvedTargetURI), false) - - // Overwrite the proxy environment and restore it after the test. - t.Setenv("HTTPS_PROXY", unresolvedProxyURI) - - origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment - delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { - return httpproxy.FromEnvironment().ProxyFunc()(req.URL) - } - defer func() { - delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment - }() - - // Set up and update a manual resolver for proxy resolution. - proxyResolver := setupDNS(t) - proxyResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: pLis.Addr().String()}}}) - - ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) - defer cancel() - conn, err := grpc.NewClient(unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) - if err != nil { - t.Fatalf("grpc.NewClient failed: %v", err) - } - defer conn.Close() - - // Send an empty RPC to the backend through the proxy. - client := testgrpc.NewTestServiceClient(conn) - if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { - t.Errorf("EmptyCall failed: %v", err) - } -} - -// Tests the scenario where grpc.NewClient is used with a custom target URI -// scheme and a proxy is configured. The test verifies that the client -// successfully connects to the proxy server, resolves the proxy URI correctly, -// includes the resolved target URI in the HTTP CONNECT request, and -// establishes a connection to the backend server. -func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { - backendAddr := createAndStartBackendServer(t) - pLis, _ := transport.SetupProxy(t, dnsCache, transport.RequestCheck(backendAddr), false) - - // Overwrite the proxy environment and restore it after the test. - t.Setenv("HTTPS_PROXY", unresolvedProxyURI) - - origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment - delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { - return httpproxy.FromEnvironment().ProxyFunc()(req.URL) - } - defer func() { - delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment - }() - - // Create and update a custom resolver for target URI. - targetResolver := manual.NewBuilderWithScheme("test") - resolver.Register(targetResolver) - targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) - - // Set up and update a manual resolver for proxy resolution. - proxyResolver := setupDNS(t) - proxyResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: pLis.Addr().String()}}}) - - // Dial to the proxy server. - ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) - defer cancel() - conn, err := grpc.NewClient(targetResolver.Scheme()+":///"+unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) - if err != nil { - t.Fatalf("grpc.NewClient(%s) failed: %v", targetResolver.Scheme()+":///"+unresolvedTargetURI, err) - } - t.Cleanup(func() { conn.Close() }) - - // Send an empty RPC to the backend through the proxy. - client := testgrpc.NewTestServiceClient(conn) - if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { - t.Errorf("EmptyCall() failed: %v", err) - } -} - -// Tests the scenario where grpc.NewClient is used with a custom target URI -// scheme and a proxy is configured and both the resolvers return endpoints. -// The test verifies that the client successfully connects to the proxy server, -// resolves the proxy URI correctly, includes the resolved target URI in the -// HTTP CONNECT request, and establishes a connection to the backend server. -func (s) TestGRPCNewClientWithProxyAndCustomResolverWithEndpoints(t *testing.T) { - backendAddr := createAndStartBackendServer(t) - pLis, _ := transport.SetupProxy(t, dnsCache, transport.RequestCheck(backendAddr), false) - - // Overwrite the proxy environment and restore it after the test. - t.Setenv("HTTPS_PROXY", unresolvedProxyURI) - - origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment - delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { - return httpproxy.FromEnvironment().ProxyFunc()(req.URL) - } - defer func() { - delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment - }() - - // Create and update a custom resolver for target URI. - targetResolver := manual.NewBuilderWithScheme("test") - resolver.Register(targetResolver) - targetResolver.InitialState(resolver.State{Endpoints: []resolver.Endpoint{{Addresses: []resolver.Address{{Addr: backendAddr}}}}}) - - // Set up and update a manual resolver for proxy resolution. - proxyResolver := setupDNS(t) - proxyResolver.InitialState(resolver.State{Endpoints: []resolver.Endpoint{{Addresses: []resolver.Address{{Addr: pLis.Addr().String()}}}}}) - - // Dial to the proxy server. - ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) - defer cancel() - conn, err := grpc.NewClient(targetResolver.Scheme()+":///"+unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) - if err != nil { - t.Fatalf("grpc.NewClient(%s) failed: %v", targetResolver.Scheme()+":///"+unresolvedTargetURI, err) - } - t.Cleanup(func() { conn.Close() }) - - // Send an empty RPC to the backend through the proxy. - client := testgrpc.NewTestServiceClient(conn) - if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { - t.Errorf("EmptyCall() failed: %v", err) - } -} +// func (s) TestGRPCDialWithProxy(t *testing.T) { +// backendAddr := createAndStartBackendServer(t) +// dnsCache := map[string]string{unresolvedTargetURI: backendAddr} +// pLis, _ := transport.SetupProxy(t, dnsCache, transport.RequestCheck(unresolvedTargetURI), false) + +// // Overwrite the proxy environment and restore it after the test. +// t.Setenv("HTTPS_PROXY", pLis.Addr().String()) + +// // Use the httpproxy package functions instead of `http.ProxyFromEnvironment` +// // because the latter reads proxy-related environment variables only once at +// // initialization. This behavior causes issues when running multiple tests +// // with different proxy configurations, as changes to environment variables +// // during tests would be ignored. By using `httpproxy.FromEnvironment()`, we +// // ensure proxy settings are read dynamically in each test execution. +// origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment +// delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { +// return httpproxy.FromEnvironment().ProxyFunc()(req.URL) +// } +// defer func() { +// delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment +// }() + +// ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) +// defer cancel() +// conn, err := grpc.Dial(unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) +// if err != nil { +// t.Fatalf("grpc.Dial(%s) failed: %v", unresolvedTargetURI, err) +// } +// defer conn.Close() + +// // Send an empty RPC to the backend through the proxy. +// client := testgrpc.NewTestServiceClient(conn) +// if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { +// t.Errorf("EmptyCall failed: %v", err) +// } +// } + +// // Tests the scenario where `grpc.Dial` is performed with a proxy and the "dns" +// // scheme for the target. The test verifies that the proxy URI is correctly +// // resolved and that the target URI resolution on the client preserves the +// // original behavior of `grpc.Dial`. It also ensures that a connection is +// // established to the proxy server, with the resolved target URI sent in the +// // HTTP CONNECT request, successfully connecting to the backend server. +// func TestGRPCDialWithDNSAndProxy(t *testing.T) { +// backendAddr := createAndStartBackendServer(t) +// pLis, _ := transport.SetupProxy(t, dnsCache, transport.RequestCheck(backendAddr), false) + +// // Overwrite the proxy environment and restore it after the test. +// t.Setenv("HTTPS_PROXY", unresolvedProxyURI) + +// origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment +// delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { +// return httpproxy.FromEnvironment().ProxyFunc()(req.URL) +// } +// defer func() { +// delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment +// }() + +// // Configure manual resolvers for both proxy and target backends +// targetResolver := setupDNS(t) +// targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) + +// // Temporarily modify ProxyScheme for this test. +// origScheme := delegatingresolver.ProxyScheme +// delegatingresolver.ProxyScheme = "test" +// t.Cleanup(func() { delegatingresolver.ProxyScheme = origScheme }) + +// proxyResolver := manual.NewBuilderWithScheme("test") +// resolver.Register(proxyResolver) +// proxyResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: pLis.Addr().String()}}}) + +// ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) +// defer cancel() +// conn, err := grpc.Dial(targetResolver.Scheme()+":///"+unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) +// if err != nil { +// t.Fatalf("grpc.Dial(%s) failed: %v", targetResolver.Scheme()+":///"+unresolvedTargetURI, err) +// } +// defer conn.Close() + +// // Send an empty RPC to the backend through the proxy. +// client := testgrpc.NewTestServiceClient(conn) +// if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { +// t.Errorf("EmptyCall failed: %v", err) +// } +// } + +// // Tests the scenario where grpc.NewClient is used with the default DNS +// // resolver for the target URI and a proxy is configured. The test verifies +// // that the client resolves proxy URI, connects to the proxy server, sends the +// // unresolved target URI in the HTTP CONNECT request, and successfully +// // establishes a connection to the backend server. +// func (s) TestGRPCNewClientWithProxy(t *testing.T) { +// backendAddr := createAndStartBackendServer(t) +// dnsCache := map[string]string{unresolvedTargetURI: backendAddr} +// pLis, _ := transport.SetupProxy(t, dnsCache, transport.RequestCheck(unresolvedTargetURI), false) + +// // Overwrite the proxy environment and restore it after the test. +// t.Setenv("HTTPS_PROXY", unresolvedProxyURI) + +// origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment +// delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { +// return httpproxy.FromEnvironment().ProxyFunc()(req.URL) +// } +// defer func() { +// delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment +// }() + +// // Set up and update a manual resolver for proxy resolution. + +// ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) +// defer cancel() +// conn, err := grpc.NewClient(unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) +// if err != nil { +// t.Fatalf("grpc.NewClient failed: %v", err) +// } +// defer conn.Close() + +// // Send an empty RPC to the backend through the proxy. +// client := testgrpc.NewTestServiceClient(conn) +// if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { +// t.Errorf("EmptyCall failed: %v", err) +// } +// } + +// // Tests the scenario where grpc.NewClient is used with a custom target URI +// // scheme and a proxy is configured. The test verifies that the client +// // successfully connects to the proxy server, resolves the proxy URI correctly, +// // includes the resolved target URI in the HTTP CONNECT request, and +// // establishes a connection to the backend server. +// func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { +// backendAddr := createAndStartBackendServer(t) +// pLis, _ := transport.SetupProxy(t, dnsCache, transport.RequestCheck(backendAddr), false) + +// // Overwrite the proxy environment and restore it after the test. +// t.Setenv("HTTPS_PROXY", unresolvedProxyURI) + +// origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment +// delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { +// return httpproxy.FromEnvironment().ProxyFunc()(req.URL) +// } +// defer func() { +// delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment +// }() + +// // Dial to the proxy server. +// ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) +// defer cancel() +// conn, err := grpc.NewClient(+":///"+unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) +// if err != nil { +// t.Fatalf("grpc.NewClient(%s) failed: %v", targetResolver.Scheme()+":///"+unresolvedTargetURI, err) +// } +// t.Cleanup(func() { conn.Close() }) + +// // Send an empty RPC to the backend through the proxy. +// client := testgrpc.NewTestServiceClient(conn) +// if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { +// t.Errorf("EmptyCall() failed: %v", err) +// } +// } + +// // Tests the scenario where grpc.NewClient is used with a custom target URI +// // scheme and a proxy is configured and both the resolvers return endpoints. +// // The test verifies that the client successfully connects to the proxy server, +// // resolves the proxy URI correctly, includes the resolved target URI in the +// // HTTP CONNECT request, and establishes a connection to the backend server. +// func (s) TestGRPCNewClientWithProxyAndCustomResolverWithEndpoints(t *testing.T) { +// backendAddr := createAndStartBackendServer(t) +// pLis, _ := transport.SetupProxy(t, dnsCache, transport.RequestCheck(backendAddr), false) + +// // Overwrite the proxy environment and restore it after the test. +// t.Setenv("HTTPS_PROXY", unresolvedProxyURI) + +// origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment +// delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { +// return httpproxy.FromEnvironment().ProxyFunc()(req.URL) +// } +// defer func() { +// delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment +// }() + +// // Create and update a custom resolver for target URI. +// targetResolver := manual.NewBuilderWithScheme("test") +// resolver.Register(targetResolver) +// targetResolver.InitialState(resolver.State{Endpoints: []resolver.Endpoint{{Addresses: []resolver.Address{{Addr: backendAddr}}}}}) + +// // Set up and update a manual resolver for proxy resolution. +// proxyResolver := setupDNS(t) +// proxyResolver.InitialState(resolver.State{Endpoints: []resolver.Endpoint{{Addresses: []resolver.Address{{Addr: pLis.Addr().String()}}}}}) + +// // Dial to the proxy server. +// ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) +// defer cancel() +// conn, err := grpc.NewClient(targetResolver.Scheme()+":///"+unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) +// if err != nil { +// t.Fatalf("grpc.NewClient(%s) failed: %v", targetResolver.Scheme()+":///"+unresolvedTargetURI, err) +// } +// t.Cleanup(func() { conn.Close() }) + +// // Send an empty RPC to the backend through the proxy. +// client := testgrpc.NewTestServiceClient(conn) +// if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { +// t.Errorf("EmptyCall() failed: %v", err) +// } +// } // Tests the scenario where grpc.NewClient is used with the default "dns" // resolver and the dial option grpc.WithTargetResolutionEnabled() is set, @@ -312,9 +304,16 @@ func (s) TestGRPCNewClientWithProxyAndCustomResolverWithEndpoints(t *testing.T) // resolution happens on the client by sending resolved target URI in HTTP // CONNECT request, the proxy URI is resolved correctly, and the connection is // successfully established with the backend server through the proxy. -func (s) TestGRPCNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { - backendAddr := createAndStartBackendServer(t) - pLis, _ := transport.SetupProxy(t, dnsCache, transport.RequestCheck(backendAddr), false) +func TestGRPCNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { + unresolvedTargetURI := createAndStartBackendServer(t) + unresolvedProxyURI, _ := transport.SetupProxy(t, dnsCache, func(req *http.Request) error { + host, _, err := net.SplitHostPort(req.URL.Host) + if err != nil { + return err + } + _, err = netip.ParseAddr(host) + return err + }, false) // Overwrite the proxy environment and restore it after the test. t.Setenv("HTTPS_PROXY", unresolvedProxyURI) @@ -327,18 +326,18 @@ func (s) TestGRPCNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment }() - // Configure manual resolvers for both proxy and target backends - targetResolver := setupDNS(t) - targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) + // // Configure manual resolvers for both proxy and target backends + // targetResolver := setupDNS(t) + // targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) - // Temporarily modify ProxyScheme for this test. - origScheme := delegatingresolver.ProxyScheme - delegatingresolver.ProxyScheme = "test" - t.Cleanup(func() { delegatingresolver.ProxyScheme = origScheme }) + // // Temporarily modify ProxyScheme for this test. + // origScheme := delegatingresolver.ProxyScheme + // delegatingresolver.ProxyScheme = "test" + // t.Cleanup(func() { delegatingresolver.ProxyScheme = origScheme }) - proxyResolver := manual.NewBuilderWithScheme("test") - resolver.Register(proxyResolver) - proxyResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: pLis.Addr().String()}}}) + // proxyResolver := manual.NewBuilderWithScheme("test") + // resolver.Register(proxyResolver) + // proxyResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: pLis.Addr().String()}}}) dopts := []grpc.DialOption{ grpc.WithTransportCredentials(insecure.NewCredentials()), @@ -523,7 +522,7 @@ func (s) TestBasicAuthInGrpcNewClientWithProxy(t *testing.T) { proxyResolver := setupDNS(t) proxyResolver.InitialState(resolver.State{ Addresses: []resolver.Address{ - {Addr: pLis.Addr().String()}, + {Addr: pLis}, }, }) diff --git a/internal/transport/proxy_test.go b/internal/transport/proxy_test.go index aff5829e4f3f..ce92e35499fb 100644 --- a/internal/transport/proxy_test.go +++ b/internal/transport/proxy_test.go @@ -59,7 +59,7 @@ func (s) TestHTTPConnectWithServerHello(t *testing.T) { }() // Overwrite the function in the test and restore them in defer. - t.Setenv("HTTPS_PROXY", pLis.Addr().String()) + t.Setenv("HTTPS_PROXY", pLis) origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { @@ -72,7 +72,7 @@ func (s) TestHTTPConnectWithServerHello(t *testing.T) { // Dial to proxy server. ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() - c, err := proxyDial(ctx, resolver.Address{Addr: pLis.Addr().String()}, "test", proxyattributes.Options{ConnectAddr: blis.Addr().String()}) + c, err := proxyDial(ctx, resolver.Address{Addr: pLis}, "test", proxyattributes.Options{ConnectAddr: blis.Addr().String()}) if err != nil { t.Fatalf("HTTP connect Dial failed: %v", err) } diff --git a/internal/transport/proxy_utils.go b/internal/transport/proxy_utils.go index e542c8084eba..46b31613a047 100644 --- a/internal/transport/proxy_utils.go +++ b/internal/transport/proxy_utils.go @@ -27,6 +27,8 @@ import ( "net/http" "testing" "time" + + "google.golang.org/grpc/internal/testutils" ) const defaultTestTimeout = 10 * time.Second @@ -63,6 +65,60 @@ func (p *ProxyServer) stop() { p.out.Close() } } +func (p *ProxyServer) handleRequest(t *testing.T, in net.Conn, proxyStarted func(), waitForServerHello bool) { + p.in = in + // This will be used in tests to check if the proxy server is started. + // if proxyStarted != nil { + // proxyStarted() + // } + req, err := http.ReadRequest(bufio.NewReader(in)) + if err != nil { + t.Errorf("failed to read CONNECT req: %v", err) + return + } + if err := p.requestCheck(req); err != nil { + resp := http.Response{StatusCode: http.StatusMethodNotAllowed} + resp.Write(p.in) + p.in.Close() + t.Errorf("get wrong CONNECT req: %+v, error: %v", req, err) + return + } + + // addr := dnsCache[req.URL.Host] + // if addr == "" { + // addr = req.URL.Host + // } + t.Logf("Dialing to %s", req.URL.Host) + out, err := net.Dial("tcp", req.URL.Host) + if err != nil { + t.Errorf("failed to dial to server: %v", err) + return + } + out.SetDeadline(time.Now().Add(defaultTestTimeout)) + resp := http.Response{StatusCode: http.StatusOK, Proto: "HTTP/1.0"} + var buf bytes.Buffer + resp.Write(&buf) + + if waitForServerHello { + // Batch the first message from the server with the http connect + // response. This is done to test the cases in which the grpc client has + // the response to the connect request and proxied packets from the + // destination server when it reads the transport. + b := make([]byte, 50) + bytesRead, err := out.Read(b) + if err != nil { + t.Errorf("Got error while reading server hello: %v", err) + in.Close() + out.Close() + return + } + buf.Write(b[0:bytesRead]) + } + p.in.Write(buf.Bytes()) + p.out = out + go io.Copy(p.in, p.out) + go io.Copy(p.out, p.in) +} // Creates and starts a proxy server. func newProxyServer(t *testing.T, lis net.Listener, dnsCache map[string]string, reqCheck func(*http.Request) error, proxyStarted func(), waitForServerHello bool) *ProxyServer { @@ -74,67 +130,25 @@ func newProxyServer(t *testing.T, lis net.Listener, dnsCache map[string]string, // Start the proxy server. go func() { - in, err := p.lis.Accept() - if err != nil { - return - } - p.in = in - // This will be used in tests to check if the proxy server is started. - if proxyStarted != nil { - proxyStarted() - } - req, err := http.ReadRequest(bufio.NewReader(in)) - if err != nil { - t.Errorf("failed to read CONNECT req: %v", err) - return - } - if err := p.requestCheck(req); err != nil { - resp := http.Response{StatusCode: http.StatusMethodNotAllowed} - resp.Write(p.in) - p.in.Close() - t.Errorf("get wrong CONNECT req: %+v, error: %v", req, err) - return - } - addr := dnsCache[req.URL.Host] - if addr == "" { - addr = req.URL.Host - } - out, err := net.Dial("tcp", addr) - if err != nil { - t.Errorf("failed to dial to server: %v", err) - return - } - out.SetDeadline(time.Now().Add(defaultTestTimeout)) - resp := http.Response{StatusCode: http.StatusOK, Proto: "HTTP/1.0"} - var buf bytes.Buffer - resp.Write(&buf) - - if waitForServerHello { - // Batch the first message from the server with the http connect - // response. This is done to test the cases in which the grpc client has - // the response to the connect request and proxied packets from the - // destination server when it reads the transport. - b := make([]byte, 50) - bytesRead, err := out.Read(b) + // var err error + // var in net.Conn + for { + in, err := p.lis.Accept() if err != nil { - t.Errorf("Got error while reading server hello: %v", err) - in.Close() - out.Close() + t.Logf("Shutting down proxy server: %v ", err) return } - buf.Write(b[0:bytesRead]) + // not called in a go routine because the proxy can currently handle only one connection. + p.handleRequest(t, in, proxyStarted, waitForServerHello) + } - p.in.Write(buf.Bytes()) - p.out = out - go io.Copy(p.in, p.out) - go io.Copy(p.out, p.in) }() return p } // SetupProxy initializes and starts a proxy server, registers a cleanup to // stop it, and returns the proxy's listener and helper channels. -func SetupProxy(t *testing.T, dnsCache map[string]string, reqCheck func(*http.Request) error, waitForServerHello bool) (net.Listener, chan struct{}) { +func SetupProxy(t *testing.T, dnsCache map[string]string, reqCheck func(*http.Request) error, waitForServerHello bool) (string, chan struct{}) { t.Helper() pLis, err := net.Listen("tcp", "localhost:0") if err != nil { @@ -146,5 +160,5 @@ func SetupProxy(t *testing.T, dnsCache map[string]string, reqCheck func(*http.Re proxyServer := newProxyServer(t, pLis, dnsCache, reqCheck, func() { close(proxyStartedCh) }, waitForServerHello) t.Cleanup(proxyServer.stop) - return pLis, proxyStartedCh + return fmt.Sprintf("localhost:%d", testutils.ParsePort(t, pLis.Addr().String())), proxyStartedCh } From 876f09e76118266fc018f64f485c867fb9ed7c4c Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Fri, 3 Jan 2025 16:15:10 +0530 Subject: [PATCH 29/53] e2e tests --- clientconn.go | 4 +- internal/transport/proxy_ext_test.go | 530 +++++++++++---------------- internal/transport/proxy_test.go | 19 +- internal/transport/proxy_utils.go | 80 ++-- 4 files changed, 274 insertions(+), 359 deletions(-) diff --git a/clientconn.go b/clientconn.go index 07e97fed5710..f122a026150c 100644 --- a/clientconn.go +++ b/clientconn.go @@ -123,7 +123,7 @@ func (dcs *defaultConfigSelector) SelectConfig(rpcInfo iresolver.RPCInfo) (*ires // // The target name syntax is defined in // https://github.com/grpc/grpc/blob/master/doc/naming.md. e.g. to use dns -// resolver, a "dns:///" prefix should be applied to the target. +// resolver, a prefix should be applied to the target. // // The DialOptions returned by WithBlock, WithTimeout, // WithReturnConnectionError, and FailOnNonTempDialError are ignored by this @@ -1327,6 +1327,8 @@ func (ac *addrConn) tryAllAddrs(ctx context.Context, addrs []resolver.Address, c if err == nil { return nil } + channelz.Infof(logger, ac.channelz, " Attempt failed %q to connect", addr.Addr) + if firstConnErr == nil { firstConnErr = err } diff --git a/internal/transport/proxy_ext_test.go b/internal/transport/proxy_ext_test.go index b7bafaaa8d5b..2319e6593eb6 100644 --- a/internal/transport/proxy_ext_test.go +++ b/internal/transport/proxy_ext_test.go @@ -24,12 +24,10 @@ import ( "fmt" "net" "net/http" - "net/netip" "net/url" "testing" "time" - "golang.org/x/net/http/httpproxy" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/internal/grpctest" @@ -43,17 +41,9 @@ import ( ) const ( - unresolvedTargetURI = "example.com" - unresolvedProxyURI = "proxyexample.com" - defaultTestTimeout = 10 * time.Second + defaultTestTimeout = 10 * time.Second ) -// dnsCache is a map used in the proxy server to mimic DNS name resolution. -// It maps unresolved hostnames to their resolved addresses. If an entry -// is found in the cache, the proxy server uses it; otherwise, it defaults -// to the original hostname for connection. -var dnsCache = make(map[string]string) - type s struct { grpctest.Tester } @@ -72,283 +62,155 @@ func createAndStartBackendServer(t *testing.T) string { } t.Logf("Started TestService backend at: %q", backend.Address) t.Cleanup(backend.Stop) - // return backend.Address return fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backend.Address)) } -// setupDNS sets up a manual DNS resolver and registers it, returning the -// builder for test customization. -func setupDNS(t *testing.T) *manual.Resolver { - t.Helper() - mr := manual.NewBuilderWithScheme("dns") - origRes := resolver.Get("dns") - resolver.Register(mr) - t.Cleanup(func() { - resolver.Register(origRes) - }) - return mr -} - // Tests the scenario where grpc.Dial is performed using a proxy with the // default resolver in the target URI. The test verifies that the connection is // established to the proxy server, sends the unresolved target URI in the HTTP // CONNECT request, and is successfully connected to the backend server. -// func (s) TestGRPCDialWithProxy(t *testing.T) { -// backendAddr := createAndStartBackendServer(t) -// dnsCache := map[string]string{unresolvedTargetURI: backendAddr} -// pLis, _ := transport.SetupProxy(t, dnsCache, transport.RequestCheck(unresolvedTargetURI), false) - -// // Overwrite the proxy environment and restore it after the test. -// t.Setenv("HTTPS_PROXY", pLis.Addr().String()) - -// // Use the httpproxy package functions instead of `http.ProxyFromEnvironment` -// // because the latter reads proxy-related environment variables only once at -// // initialization. This behavior causes issues when running multiple tests -// // with different proxy configurations, as changes to environment variables -// // during tests would be ignored. By using `httpproxy.FromEnvironment()`, we -// // ensure proxy settings are read dynamically in each test execution. -// origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment -// delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { -// return httpproxy.FromEnvironment().ProxyFunc()(req.URL) -// } -// defer func() { -// delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment -// }() - -// ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) -// defer cancel() -// conn, err := grpc.Dial(unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) -// if err != nil { -// t.Fatalf("grpc.Dial(%s) failed: %v", unresolvedTargetURI, err) -// } -// defer conn.Close() - -// // Send an empty RPC to the backend through the proxy. -// client := testgrpc.NewTestServiceClient(conn) -// if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { -// t.Errorf("EmptyCall failed: %v", err) -// } -// } - -// // Tests the scenario where `grpc.Dial` is performed with a proxy and the "dns" -// // scheme for the target. The test verifies that the proxy URI is correctly -// // resolved and that the target URI resolution on the client preserves the -// // original behavior of `grpc.Dial`. It also ensures that a connection is -// // established to the proxy server, with the resolved target URI sent in the -// // HTTP CONNECT request, successfully connecting to the backend server. -// func TestGRPCDialWithDNSAndProxy(t *testing.T) { -// backendAddr := createAndStartBackendServer(t) -// pLis, _ := transport.SetupProxy(t, dnsCache, transport.RequestCheck(backendAddr), false) - -// // Overwrite the proxy environment and restore it after the test. -// t.Setenv("HTTPS_PROXY", unresolvedProxyURI) - -// origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment -// delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { -// return httpproxy.FromEnvironment().ProxyFunc()(req.URL) -// } -// defer func() { -// delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment -// }() - -// // Configure manual resolvers for both proxy and target backends -// targetResolver := setupDNS(t) -// targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) - -// // Temporarily modify ProxyScheme for this test. -// origScheme := delegatingresolver.ProxyScheme -// delegatingresolver.ProxyScheme = "test" -// t.Cleanup(func() { delegatingresolver.ProxyScheme = origScheme }) - -// proxyResolver := manual.NewBuilderWithScheme("test") -// resolver.Register(proxyResolver) -// proxyResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: pLis.Addr().String()}}}) - -// ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) -// defer cancel() -// conn, err := grpc.Dial(targetResolver.Scheme()+":///"+unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) -// if err != nil { -// t.Fatalf("grpc.Dial(%s) failed: %v", targetResolver.Scheme()+":///"+unresolvedTargetURI, err) -// } -// defer conn.Close() - -// // Send an empty RPC to the backend through the proxy. -// client := testgrpc.NewTestServiceClient(conn) -// if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { -// t.Errorf("EmptyCall failed: %v", err) -// } -// } - -// // Tests the scenario where grpc.NewClient is used with the default DNS -// // resolver for the target URI and a proxy is configured. The test verifies -// // that the client resolves proxy URI, connects to the proxy server, sends the -// // unresolved target URI in the HTTP CONNECT request, and successfully -// // establishes a connection to the backend server. -// func (s) TestGRPCNewClientWithProxy(t *testing.T) { -// backendAddr := createAndStartBackendServer(t) -// dnsCache := map[string]string{unresolvedTargetURI: backendAddr} -// pLis, _ := transport.SetupProxy(t, dnsCache, transport.RequestCheck(unresolvedTargetURI), false) - -// // Overwrite the proxy environment and restore it after the test. -// t.Setenv("HTTPS_PROXY", unresolvedProxyURI) - -// origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment -// delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { -// return httpproxy.FromEnvironment().ProxyFunc()(req.URL) -// } -// defer func() { -// delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment -// }() - -// // Set up and update a manual resolver for proxy resolution. - -// ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) -// defer cancel() -// conn, err := grpc.NewClient(unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) -// if err != nil { -// t.Fatalf("grpc.NewClient failed: %v", err) -// } -// defer conn.Close() - -// // Send an empty RPC to the backend through the proxy. -// client := testgrpc.NewTestServiceClient(conn) -// if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { -// t.Errorf("EmptyCall failed: %v", err) -// } -// } - -// // Tests the scenario where grpc.NewClient is used with a custom target URI -// // scheme and a proxy is configured. The test verifies that the client -// // successfully connects to the proxy server, resolves the proxy URI correctly, -// // includes the resolved target URI in the HTTP CONNECT request, and -// // establishes a connection to the backend server. -// func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { -// backendAddr := createAndStartBackendServer(t) -// pLis, _ := transport.SetupProxy(t, dnsCache, transport.RequestCheck(backendAddr), false) - -// // Overwrite the proxy environment and restore it after the test. -// t.Setenv("HTTPS_PROXY", unresolvedProxyURI) - -// origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment -// delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { -// return httpproxy.FromEnvironment().ProxyFunc()(req.URL) -// } -// defer func() { -// delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment -// }() - -// // Dial to the proxy server. -// ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) -// defer cancel() -// conn, err := grpc.NewClient(+":///"+unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) -// if err != nil { -// t.Fatalf("grpc.NewClient(%s) failed: %v", targetResolver.Scheme()+":///"+unresolvedTargetURI, err) -// } -// t.Cleanup(func() { conn.Close() }) - -// // Send an empty RPC to the backend through the proxy. -// client := testgrpc.NewTestServiceClient(conn) -// if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { -// t.Errorf("EmptyCall() failed: %v", err) -// } -// } - -// // Tests the scenario where grpc.NewClient is used with a custom target URI -// // scheme and a proxy is configured and both the resolvers return endpoints. -// // The test verifies that the client successfully connects to the proxy server, -// // resolves the proxy URI correctly, includes the resolved target URI in the -// // HTTP CONNECT request, and establishes a connection to the backend server. -// func (s) TestGRPCNewClientWithProxyAndCustomResolverWithEndpoints(t *testing.T) { -// backendAddr := createAndStartBackendServer(t) -// pLis, _ := transport.SetupProxy(t, dnsCache, transport.RequestCheck(backendAddr), false) - -// // Overwrite the proxy environment and restore it after the test. -// t.Setenv("HTTPS_PROXY", unresolvedProxyURI) - -// origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment -// delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { -// return httpproxy.FromEnvironment().ProxyFunc()(req.URL) -// } -// defer func() { -// delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment -// }() - -// // Create and update a custom resolver for target URI. -// targetResolver := manual.NewBuilderWithScheme("test") -// resolver.Register(targetResolver) -// targetResolver.InitialState(resolver.State{Endpoints: []resolver.Endpoint{{Addresses: []resolver.Address{{Addr: backendAddr}}}}}) - -// // Set up and update a manual resolver for proxy resolution. -// proxyResolver := setupDNS(t) -// proxyResolver.InitialState(resolver.State{Endpoints: []resolver.Endpoint{{Addresses: []resolver.Address{{Addr: pLis.Addr().String()}}}}}) - -// // Dial to the proxy server. -// ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) -// defer cancel() -// conn, err := grpc.NewClient(targetResolver.Scheme()+":///"+unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) -// if err != nil { -// t.Fatalf("grpc.NewClient(%s) failed: %v", targetResolver.Scheme()+":///"+unresolvedTargetURI, err) -// } -// t.Cleanup(func() { conn.Close() }) - -// // Send an empty RPC to the backend through the proxy. -// client := testgrpc.NewTestServiceClient(conn) -// if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { -// t.Errorf("EmptyCall() failed: %v", err) -// } -// } - -// Tests the scenario where grpc.NewClient is used with the default "dns" -// resolver and the dial option grpc.WithTargetResolutionEnabled() is set, -// enabling target resolution on the client. The test verifies that target -// resolution happens on the client by sending resolved target URI in HTTP -// CONNECT request, the proxy URI is resolved correctly, and the connection is -// successfully established with the backend server through the proxy. -func TestGRPCNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { +func (s) TestGRPCDialWithProxy(t *testing.T) { unresolvedTargetURI := createAndStartBackendServer(t) - unresolvedProxyURI, _ := transport.SetupProxy(t, dnsCache, func(req *http.Request) error { - host, _, err := net.SplitHostPort(req.URL.Host) - if err != nil { - return err - } - _, err = netip.ParseAddr(host) - return err - }, false) + unresolvedProxyURI, _ := transport.SetupProxy(t, transport.RequestCheck(false), false) + + // Overwrite the function in the test and restore them in defer. + hpfe := func(req *http.Request) (*url.URL, error) { + return &url.URL{ + Scheme: "https", + Host: unresolvedProxyURI, + }, nil + } + orighpfe := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = hpfe + defer func() { + delegatingresolver.HTTPSProxyFromEnvironment = orighpfe + }() - // Overwrite the proxy environment and restore it after the test. - t.Setenv("HTTPS_PROXY", unresolvedProxyURI) + ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) + defer cancel() + conn, err := grpc.Dial(unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + t.Fatalf("grpc.Dial(%s) failed: %v", unresolvedTargetURI, err) + } + defer conn.Close() + + // Send an empty RPC to the backend through the proxy. + client := testgrpc.NewTestServiceClient(conn) + if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { + t.Errorf("EmptyCall failed: %v", err) + } +} - origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment - delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { - return httpproxy.FromEnvironment().ProxyFunc()(req.URL) +// Tests the scenario where `grpc.Dial` is performed with a proxy and the "dns" +// scheme for the target. The test verifies that the proxy URI is correctly +// resolved and that the target URI resolution on the client preserves the +// original behavior of `grpc.Dial`. It also ensures that a connection is +// established to the proxy server, with the resolved target URI sent in the +// HTTP CONNECT request, successfully connecting to the backend server. +func (s) TestGRPCDialWithDNSAndProxy(t *testing.T) { + unresolvedTargetURI := createAndStartBackendServer(t) + unresolvedProxyURI, _ := transport.SetupProxy(t, transport.RequestCheck(true), false) + + // Overwrite the function in the test and restore them in defer. + hpfe := func(req *http.Request) (*url.URL, error) { + return &url.URL{ + Scheme: "https", + Host: unresolvedProxyURI, + }, nil } + orighpfe := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = hpfe defer func() { - delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = orighpfe }() - // // Configure manual resolvers for both proxy and target backends - // targetResolver := setupDNS(t) - // targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) - - // // Temporarily modify ProxyScheme for this test. - // origScheme := delegatingresolver.ProxyScheme - // delegatingresolver.ProxyScheme = "test" - // t.Cleanup(func() { delegatingresolver.ProxyScheme = origScheme }) + ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) + defer cancel() + conn, err := grpc.Dial("dns:///"+unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + t.Fatalf("grpc.Dial(%s) failed: %v", "dns:///"+unresolvedTargetURI, err) + } + defer conn.Close() - // proxyResolver := manual.NewBuilderWithScheme("test") - // resolver.Register(proxyResolver) - // proxyResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: pLis.Addr().String()}}}) + // Send an empty RPC to the backend through the proxy. + client := testgrpc.NewTestServiceClient(conn) + if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { + t.Errorf("EmptyCall failed: %v", err) + } +} - dopts := []grpc.DialOption{ - grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithTargetResolutionEnabled(), // Target resolution on client enabled. +// Tests the scenario where `grpc.NewClient` is used with the default DNS +// resolver for the target URI and a proxy is configured. The test verifies +// that the client resolves proxy URI, connects to the proxy server, sends the +// unresolved target URI in the HTTP CONNECT request, and successfully +// establishes a connection to the backend server. +func (s) TestGRPCNewClientWithProxy(t *testing.T) { + unresolvedTargetURI := createAndStartBackendServer(t) + unresolvedProxyURI, _ := transport.SetupProxy(t, transport.RequestCheck(false), false) + + // Overwrite the function in the test and restore them in defer. + hpfe := func(req *http.Request) (*url.URL, error) { + return &url.URL{ + Scheme: "https", + Host: unresolvedProxyURI, + }, nil } + orighpfe := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = hpfe + defer func() { + delegatingresolver.HTTPSProxyFromEnvironment = orighpfe + }() + ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() - conn, err := grpc.NewClient(unresolvedTargetURI, dopts...) + conn, err := grpc.NewClient(unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { t.Fatalf("grpc.NewClient(%s) failed: %v", unresolvedTargetURI, err) } + defer conn.Close() + + // Send an empty RPC to the backend through the proxy. + client := testgrpc.NewTestServiceClient(conn) + if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { + t.Errorf("EmptyCall failed: %v", err) + } +} + +// Tests the scenario where grpc.NewClient is used with a custom target URI +// scheme and a proxy is configured. The test verifies that the client +// successfully connects to the proxy server, resolves the proxy URI correctly, +// includes the resolved target URI in the HTTP CONNECT request, and +// establishes a connection to the backend server. +func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { + unresolvedTargetURI := createAndStartBackendServer(t) + unresolvedProxyURI, _ := transport.SetupProxy(t, transport.RequestCheck(true), false) + + // Overwrite the function in the test and restore them in defer. + hpfe := func(req *http.Request) (*url.URL, error) { + return &url.URL{ + Scheme: "https", + Host: unresolvedProxyURI, + }, nil + } + orighpfe := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = hpfe + defer func() { + delegatingresolver.HTTPSProxyFromEnvironment = orighpfe + }() + + // Create and update a custom resolver for target URI. + resolvedTargetURI := fmt.Sprintf("[::0]:%d", testutils.ParsePort(t, unresolvedTargetURI)) + targetResolver := manual.NewBuilderWithScheme("test") + resolver.Register(targetResolver) + targetResolver.InitialState(resolver.State{Endpoints: []resolver.Endpoint{{Addresses: []resolver.Address{{Addr: resolvedTargetURI}}}}}) + + // Dial to the proxy server. + ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) + defer cancel() + conn, err := grpc.NewClient(targetResolver.Scheme()+":///"+unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + t.Fatalf("grpc.NewClient(%s) failed: %v", targetResolver.Scheme()+":///"+unresolvedTargetURI, err) + } t.Cleanup(func() { conn.Close() }) // Send an empty RPC to the backend through the proxy. @@ -358,34 +220,72 @@ func TestGRPCNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { } } +// Tests the scenario where grpc.NewClient is used with the default "dns" +// resolver and the dial option grpc.WithTargetResolutionEnabled() is set, +// enabling target resolution on the client. The test verifies that target +// resolution happens on the client by sending resolved target URI in HTTP +// CONNECT request, the proxy URI is resolved correctly, and the connection is +// successfully established with the backend server through the proxy. +func (s) TestGRPCNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { + unresolvedProxyURI, _ := transport.SetupProxy(t, transport.RequestCheck(true), false) + unresolvedTargetURI := createAndStartBackendServer(t) + + // Overwrite the function in the test and restore them in defer. + hpfe := func(req *http.Request) (*url.URL, error) { + return &url.URL{ + Scheme: "https", + Host: unresolvedProxyURI, + }, nil + } + orighpfe := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = hpfe + defer func() { + delegatingresolver.HTTPSProxyFromEnvironment = orighpfe + }() + + ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) + defer cancel() + conn, err := grpc.NewClient(unresolvedTargetURI, grpc.WithTargetResolutionEnabled(), grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + t.Fatalf("grpc.NewClient(%s) failed: %v", unresolvedTargetURI, err) + } + defer conn.Close() + t.Logf("emchadnwani : sending RPC") + // Send an empty RPC to the backend through the proxy. + client := testgrpc.NewTestServiceClient(conn) + if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { + t.Errorf("EmptyCall failed: %v", err) + } + t.Logf("emcahdwnai : RPC done") +} + // Tests the scenario where grpc.NewClient is used with grpc.WithNoProxy() set, // explicitly disabling proxy usage. The test verifies that the client does not // dial the proxy but directly connects to the backend server. It also checks // that the proxy resolution function is not called and that the proxy server // never receives a connection request. func (s) TestGRPCNewClientWithNoProxy(t *testing.T) { - backendAddr := createAndStartBackendServer(t) - _, proxyStartedCh := transport.SetupProxy(t, dnsCache, func(req *http.Request) error { + unresolvedTargetURI := createAndStartBackendServer(t) + unresolvedProxyURI, proxyStartedCh := transport.SetupProxy(t, func(req *http.Request) error { return fmt.Errorf("proxy server should not have received a Connect request: %v", req) }, false) - // Overwrite the proxy environment and restore it after the test. - t.Setenv("HTTPS_PROXY", unresolvedProxyURI) - - origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment - delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { - return httpproxy.FromEnvironment().ProxyFunc()(req.URL) + // Overwrite the function in the test and restore them in defer. + hpfe := func(req *http.Request) (*url.URL, error) { + return &url.URL{ + Scheme: "https", + Host: unresolvedProxyURI, + }, nil } + orighpfe := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = hpfe defer func() { - delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = orighpfe }() - targetResolver := setupDNS(t) - targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) - dopts := []grpc.DialOption{ grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithNoProxy(), // Diable proxy. + grpc.WithNoProxy(), // Disable proxy. } ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() @@ -415,33 +315,30 @@ func (s) TestGRPCNewClientWithNoProxy(t *testing.T) { // custom dialer instead. It ensures that the proxy server is never dialed, the // proxy resolution function is not triggered, and the custom dialer is invoked // as expected. -func (s) TestGRPCNewClientWithContextDialer(t *testing.T) { - backendAddr := createAndStartBackendServer(t) - _, proxyStartedCh := transport.SetupProxy(t, dnsCache, func(req *http.Request) error { +func TestGRPCNewClientWithContextDialer(t *testing.T) { + unresolvedTargetURI := createAndStartBackendServer(t) + unresolvedProxyURI, proxyStartedCh := transport.SetupProxy(t, func(req *http.Request) error { return fmt.Errorf("proxy server should not have received a Connect request: %v", req) }, false) - // Overwrite the proxy environment and restore it after the test. - t.Setenv("HTTPS_PROXY", unresolvedProxyURI) - - origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment - delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { - return httpproxy.FromEnvironment().ProxyFunc()(req.URL) + // Overwrite the function in the test and restore them in defer. + hpfe := func(req *http.Request) (*url.URL, error) { + return &url.URL{ + Scheme: "https", + Host: unresolvedProxyURI, + }, nil } + orighpfe := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = hpfe defer func() { - delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = orighpfe }() - // Create a custom dialer that directly dials the backend.ß - dialerCalled := make(chan struct{}) - customDialer := func(_ context.Context, backendAddr string) (net.Conn, error) { - close(dialerCalled) - return net.Dial("tcp", backendAddr) + // Create a custom dialer that directly dials the backend. + customDialer := func(_ context.Context, unresolvedTargetURI string) (net.Conn, error) { + return net.Dial("tcp", unresolvedTargetURI) } - targetResolver := setupDNS(t) - targetResolver.InitialState(resolver.State{Addresses: []resolver.Address{{Addr: backendAddr}}}) - dopts := []grpc.DialOption{ grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithContextDialer(customDialer), // Use a custom dialer. @@ -458,14 +355,7 @@ func (s) TestGRPCNewClientWithContextDialer(t *testing.T) { if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { t.Errorf("EmptyCall() failed: %v", err) } - - select { - case <-dialerCalled: - t.Log("custom dialer was invoked") - default: - t.Error("custom dialer was not invoked") - } - + select { case <-proxyStartedCh: t.Fatal("unexpected dial to proxy server") @@ -482,13 +372,12 @@ func (s) TestGRPCNewClientWithContextDialer(t *testing.T) { // the CONNECT request. The test also ensures that target resolution does not // happen on the client. func (s) TestBasicAuthInGrpcNewClientWithProxy(t *testing.T) { - backendAddr := createAndStartBackendServer(t) + unresolvedTargetURI := createAndStartBackendServer(t) const ( user = "notAUser" password = "notAPassword" ) - dnsCache := map[string]string{unresolvedTargetURI: backendAddr} - pLis, _ := transport.SetupProxy(t, dnsCache, func(req *http.Request) error { + unresolvedProxyURI, _ := transport.SetupProxy(t, func(req *http.Request) error { if req.Method != http.MethodConnect { return fmt.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) } @@ -507,25 +396,20 @@ func (s) TestBasicAuthInGrpcNewClientWithProxy(t *testing.T) { return nil }, false) - // Overwrite the proxy environment and restore it after the test. - t.Setenv("HTTPS_PROXY", user+":"+password+"@"+unresolvedProxyURI) - - origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment - delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { - return httpproxy.FromEnvironment().ProxyFunc()(req.URL) + // Overwrite the function in the test and restore them in defer. + hpfe := func(req *http.Request) (*url.URL, error) { + return &url.URL{ + Scheme: "https", + User: url.UserPassword(user, password), + Host: unresolvedProxyURI, + }, nil } + orighpfe := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = hpfe defer func() { - delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = orighpfe }() - // Set up and update a manual resolver for proxy resolution. - proxyResolver := setupDNS(t) - proxyResolver.InitialState(resolver.State{ - Addresses: []resolver.Address{ - {Addr: pLis}, - }, - }) - ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() conn, err := grpc.NewClient(unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) diff --git a/internal/transport/proxy_test.go b/internal/transport/proxy_test.go index ce92e35499fb..61383eedea56 100644 --- a/internal/transport/proxy_test.go +++ b/internal/transport/proxy_test.go @@ -29,7 +29,6 @@ import ( "testing" "time" - "golang.org/x/net/http/httpproxy" "google.golang.org/grpc/internal/proxyattributes" "google.golang.org/grpc/internal/resolver/delegatingresolver" "google.golang.org/grpc/resolver" @@ -41,7 +40,7 @@ func (s) TestHTTPConnectWithServerHello(t *testing.T) { if err != nil { t.Fatalf("failed to listen: %v", err) } - pLis, _ := SetupProxy(t, map[string]string{}, RequestCheck(blis.Addr().String()), true) + proxyAddr, _ := SetupProxy(t, RequestCheck(true), true) msg := []byte{4, 3, 5, 2} recvBuf := make([]byte, len(msg)) @@ -59,20 +58,22 @@ func (s) TestHTTPConnectWithServerHello(t *testing.T) { }() // Overwrite the function in the test and restore them in defer. - t.Setenv("HTTPS_PROXY", pLis) - - origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment - delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { - return httpproxy.FromEnvironment().ProxyFunc()(req.URL) + hpfe := func(req *http.Request) (*url.URL, error) { + return &url.URL{ + Scheme: "https", + Host: proxyAddr, + }, nil } + orighpfe := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = hpfe defer func() { - delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = orighpfe }() // Dial to proxy server. ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() - c, err := proxyDial(ctx, resolver.Address{Addr: pLis}, "test", proxyattributes.Options{ConnectAddr: blis.Addr().String()}) + c, err := proxyDial(ctx, resolver.Address{Addr: proxyAddr}, "test", proxyattributes.Options{ConnectAddr: blis.Addr().String()}) if err != nil { t.Fatalf("HTTP connect Dial failed: %v", err) } diff --git a/internal/transport/proxy_utils.go b/internal/transport/proxy_utils.go index 46b31613a047..88ea044cd969 100644 --- a/internal/transport/proxy_utils.go +++ b/internal/transport/proxy_utils.go @@ -25,6 +25,8 @@ import ( "io" "net" "net/http" + "net/netip" + "sync" "testing" "time" @@ -33,15 +35,25 @@ import ( const defaultTestTimeout = 10 * time.Second -// RequestCheck returns a function that checks the HTTP CONNECT request for the -// correct CONNECT method and address. -func RequestCheck(connectAddr string) func(*http.Request) error { +// RequestCheck returns a function that validates the CONNECT method and +// determines whether the address is resolved or unresolved. It only checks if +// the address is an IP address or not, relying on RPC failures to handle +// incorrect resolutions. +func RequestCheck(isResolved bool) func(*http.Request) error { return func(req *http.Request) error { if req.Method != http.MethodConnect { return fmt.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) } - if req.URL.Host != connectAddr { - return fmt.Errorf("unexpected URL.Host in CONNECT req %q, want %q", req.URL.Host, connectAddr) + host, _, err := net.SplitHostPort(req.URL.Host) + if err != nil { + return err + } + _, err = netip.ParseAddr(host) + if isResolved && err != nil { + return fmt.Errorf("unexpected URL.Host in CONNECT req %q, want an IP address", req.URL.Host) + } + if !isResolved && err == nil { + return fmt.Errorf("unexpected URL.Host in CONNECT req %q, do not want an IP address", req.URL.Host) } return nil } @@ -57,6 +69,7 @@ type ProxyServer struct { // Stop closes the ProxyServer and its connections to client and server. func (p *ProxyServer) stop() { + fmt.Println("emchadnwani :stopping proxy") p.lis.Close() if p.in != nil { p.in.Close() @@ -65,12 +78,14 @@ func (p *ProxyServer) stop() { p.out.Close() } } + func (p *ProxyServer) handleRequest(t *testing.T, in net.Conn, proxyStarted func(), waitForServerHello bool) { p.in = in + // This will be used in tests to check if the proxy server is started. - // if proxyStarted != nil { - // proxyStarted() - // } + if proxyStarted != nil { + proxyStarted() + } req, err := http.ReadRequest(bufio.NewReader(in)) if err != nil { t.Errorf("failed to read CONNECT req: %v", err) @@ -80,18 +95,16 @@ func (p *ProxyServer) handleRequest(t *testing.T, in net.Conn, proxyStarted func resp := http.Response{StatusCode: http.StatusMethodNotAllowed} resp.Write(p.in) p.in.Close() - t.Errorf("get wrong CONNECT req: %+v, error: %v", req, err) + t.Errorf("failed to read CONNECT req: %v", err) return } - // addr := dnsCache[req.URL.Host] - // if addr == "" { - // addr = req.URL.Host - // } t.Logf("Dialing to %s", req.URL.Host) out, err := net.Dial("tcp", req.URL.Host) if err != nil { - t.Errorf("failed to dial to server: %v", err) + p.in.Close() + p.in = nil + t.Logf("failed to dial to server: %v", err) return } out.SetDeadline(time.Now().Add(defaultTestTimeout)) @@ -116,12 +129,22 @@ func (p *ProxyServer) handleRequest(t *testing.T, in net.Conn, proxyStarted func } p.in.Write(buf.Bytes()) p.out = out - go io.Copy(p.in, p.out) - go io.Copy(p.out, p.in) + wg := sync.WaitGroup{} + wg.Add(2) + go func() { + io.Copy(p.in, p.out) + wg.Done() + }() + go func() { + io.Copy(p.out, p.in) + wg.Done() + }() + wg.Wait() + t.Logf("emchadnwani: proxy go routine exits") } // Creates and starts a proxy server. -func newProxyServer(t *testing.T, lis net.Listener, dnsCache map[string]string, reqCheck func(*http.Request) error, proxyStarted func(), waitForServerHello bool) *ProxyServer { +func newProxyServer(t *testing.T, lis net.Listener, reqCheck func(*http.Request) error, proxyStarted func(), waitForServerHello bool) *ProxyServer { t.Helper() p := &ProxyServer{ lis: lis, @@ -130,15 +153,15 @@ func newProxyServer(t *testing.T, lis net.Listener, dnsCache map[string]string, // Start the proxy server. go func() { - // var err error - // var in net.Conn for { in, err := p.lis.Accept() + fmt.Printf("emchandwani1 : %v\n", err) if err != nil { - t.Logf("Shutting down proxy server: %v ", err) + // t.Logf("Shutting down proxy server: %v ", err) return } - // not called in a go routine because the proxy can currently handle only one connection. + // p.handleRequest is not invoked in a goroutine because the test + // proxy currently supports handling only one connection at a time. p.handleRequest(t, in, proxyStarted, waitForServerHello) } @@ -148,7 +171,7 @@ func newProxyServer(t *testing.T, lis net.Listener, dnsCache map[string]string, // SetupProxy initializes and starts a proxy server, registers a cleanup to // stop it, and returns the proxy's listener and helper channels. -func SetupProxy(t *testing.T, dnsCache map[string]string, reqCheck func(*http.Request) error, waitForServerHello bool) (string, chan struct{}) { +func SetupProxy(t *testing.T, reqCheck func(*http.Request) error, waitForServerHello bool) (string, chan struct{}) { t.Helper() pLis, err := net.Listen("tcp", "localhost:0") if err != nil { @@ -156,9 +179,14 @@ func SetupProxy(t *testing.T, dnsCache map[string]string, reqCheck func(*http.Re } proxyStartedCh := make(chan struct{}) - - proxyServer := newProxyServer(t, pLis, dnsCache, reqCheck, func() { close(proxyStartedCh) }, waitForServerHello) + // var once sync.Once + fmt.Printf("emchadnwani proxy staring at : %v", pLis.Addr().String()) + proxyServer := newProxyServer(t, pLis, reqCheck, func() { + // once.Do(func() { + // close(proxyStartedCh) + // }) + }, waitForServerHello) t.Cleanup(proxyServer.stop) - - return fmt.Sprintf("localhost:%d", testutils.ParsePort(t, pLis.Addr().String())), proxyStartedCh + pAddr := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, pLis.Addr().String())) + return pAddr, proxyStartedCh } From 3e20010ffcd0ec1313882cbea0368fbaa93fcee8 Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Fri, 3 Jan 2025 16:36:49 +0530 Subject: [PATCH 30/53] correct vet --- internal/transport/proxy_ext_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/transport/proxy_ext_test.go b/internal/transport/proxy_ext_test.go index 2319e6593eb6..c2d43278479e 100644 --- a/internal/transport/proxy_ext_test.go +++ b/internal/transport/proxy_ext_test.go @@ -355,7 +355,7 @@ func TestGRPCNewClientWithContextDialer(t *testing.T) { if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { t.Errorf("EmptyCall() failed: %v", err) } - + select { case <-proxyStartedCh: t.Fatal("unexpected dial to proxy server") From 6827a6230bd9128e769c267de6fd1478c893a0ec Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Fri, 3 Jan 2025 17:13:31 +0530 Subject: [PATCH 31/53] correct e2e tests --- clientconn.go | 3 +-- .../delegatingresolver/delegatingresolver.go | 4 +-- internal/transport/proxy_ext_test.go | 25 ++++++++----------- internal/transport/proxy_utils.go | 18 +++++-------- 4 files changed, 19 insertions(+), 31 deletions(-) diff --git a/clientconn.go b/clientconn.go index f122a026150c..0538a517f89b 100644 --- a/clientconn.go +++ b/clientconn.go @@ -123,7 +123,7 @@ func (dcs *defaultConfigSelector) SelectConfig(rpcInfo iresolver.RPCInfo) (*ires // // The target name syntax is defined in // https://github.com/grpc/grpc/blob/master/doc/naming.md. e.g. to use dns -// resolver, a prefix should be applied to the target. +// resolver, a "dns:///" prefix should be applied to the target. // // The DialOptions returned by WithBlock, WithTimeout, // WithReturnConnectionError, and FailOnNonTempDialError are ignored by this @@ -1327,7 +1327,6 @@ func (ac *addrConn) tryAllAddrs(ctx context.Context, addrs []resolver.Address, c if err == nil { return nil } - channelz.Infof(logger, ac.channelz, " Attempt failed %q to connect", addr.Addr) if firstConnErr == nil { firstConnErr = err diff --git a/internal/resolver/delegatingresolver/delegatingresolver.go b/internal/resolver/delegatingresolver/delegatingresolver.go index 7a62420fe9fe..6050e3d055bb 100644 --- a/internal/resolver/delegatingresolver/delegatingresolver.go +++ b/internal/resolver/delegatingresolver/delegatingresolver.go @@ -36,8 +36,6 @@ var ( logger = grpclog.Component("delegating-resolver") // HTTPSProxyFromEnvironment will be overwritten in the tests HTTPSProxyFromEnvironment = http.ProxyFromEnvironment - // ProxyScheme will be overwritten in tests - ProxyScheme = "dns" ) // delegatingResolver manages both target URI and proxy address resolution by @@ -149,7 +147,7 @@ func New(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOpti // "dns" scheme. It adjusts the proxyURL to conform to the "dns:///" format and // builds a resolver with a wrappingClientConn to capture resolved addresses. func (r *delegatingResolver) proxyURIResolver(opts resolver.BuildOptions) (resolver.Resolver, error) { - proxyBuilder := resolver.Get(ProxyScheme) + proxyBuilder := resolver.Get("dns") if proxyBuilder == nil { panic("delegating_resolver: resolver for proxy not found for scheme dns") } diff --git a/internal/transport/proxy_ext_test.go b/internal/transport/proxy_ext_test.go index c2d43278479e..8dd467007c76 100644 --- a/internal/transport/proxy_ext_test.go +++ b/internal/transport/proxy_ext_test.go @@ -40,9 +40,7 @@ import ( "google.golang.org/grpc/resolver/manual" ) -const ( - defaultTestTimeout = 10 * time.Second -) +const defaultTestTimeout = 10 * time.Second type s struct { grpctest.Tester @@ -97,7 +95,7 @@ func (s) TestGRPCDialWithProxy(t *testing.T) { // Send an empty RPC to the backend through the proxy. client := testgrpc.NewTestServiceClient(conn) if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { - t.Errorf("EmptyCall failed: %v", err) + t.Fatalf("EmptyCall failed: %v", err) } } @@ -135,7 +133,7 @@ func (s) TestGRPCDialWithDNSAndProxy(t *testing.T) { // Send an empty RPC to the backend through the proxy. client := testgrpc.NewTestServiceClient(conn) if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { - t.Errorf("EmptyCall failed: %v", err) + t.Fatalf("EmptyCall failed: %v", err) } } @@ -172,7 +170,7 @@ func (s) TestGRPCNewClientWithProxy(t *testing.T) { // Send an empty RPC to the backend through the proxy. client := testgrpc.NewTestServiceClient(conn) if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { - t.Errorf("EmptyCall failed: %v", err) + t.Fatalf("EmptyCall failed: %v", err) } } @@ -216,7 +214,7 @@ func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { // Send an empty RPC to the backend through the proxy. client := testgrpc.NewTestServiceClient(conn) if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { - t.Errorf("EmptyCall() failed: %v", err) + t.Fatalf("EmptyCall() failed: %v", err) } } @@ -250,13 +248,12 @@ func (s) TestGRPCNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { t.Fatalf("grpc.NewClient(%s) failed: %v", unresolvedTargetURI, err) } defer conn.Close() - t.Logf("emchadnwani : sending RPC") + // Send an empty RPC to the backend through the proxy. client := testgrpc.NewTestServiceClient(conn) if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { - t.Errorf("EmptyCall failed: %v", err) + t.Fatalf("EmptyCall failed: %v", err) } - t.Logf("emcahdwnai : RPC done") } // Tests the scenario where grpc.NewClient is used with grpc.WithNoProxy() set, @@ -298,7 +295,7 @@ func (s) TestGRPCNewClientWithNoProxy(t *testing.T) { // Create a test service client and make an RPC call. client := testgrpc.NewTestServiceClient(conn) if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { - t.Errorf("EmptyCall() failed: %v", err) + t.Fatalf("EmptyCall() failed: %v", err) } // Verify that the proxy server was not dialed. @@ -315,7 +312,7 @@ func (s) TestGRPCNewClientWithNoProxy(t *testing.T) { // custom dialer instead. It ensures that the proxy server is never dialed, the // proxy resolution function is not triggered, and the custom dialer is invoked // as expected. -func TestGRPCNewClientWithContextDialer(t *testing.T) { +func (s) TestGRPCNewClientWithContextDialer(t *testing.T) { unresolvedTargetURI := createAndStartBackendServer(t) unresolvedProxyURI, proxyStartedCh := transport.SetupProxy(t, func(req *http.Request) error { return fmt.Errorf("proxy server should not have received a Connect request: %v", req) @@ -353,7 +350,7 @@ func TestGRPCNewClientWithContextDialer(t *testing.T) { client := testgrpc.NewTestServiceClient(conn) if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { - t.Errorf("EmptyCall() failed: %v", err) + t.Fatalf("EmptyCall() failed: %v", err) } select { @@ -421,6 +418,6 @@ func (s) TestBasicAuthInGrpcNewClientWithProxy(t *testing.T) { // Send an empty RPC to the backend through the proxy. client := testgrpc.NewTestServiceClient(conn) if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { - t.Errorf("EmptyCall failed: %v", err) + t.Fatalf("EmptyCall failed: %v", err) } } diff --git a/internal/transport/proxy_utils.go b/internal/transport/proxy_utils.go index 88ea044cd969..c03ffe75ce21 100644 --- a/internal/transport/proxy_utils.go +++ b/internal/transport/proxy_utils.go @@ -69,7 +69,6 @@ type ProxyServer struct { // Stop closes the ProxyServer and its connections to client and server. func (p *ProxyServer) stop() { - fmt.Println("emchadnwani :stopping proxy") p.lis.Close() if p.in != nil { p.in.Close() @@ -80,8 +79,6 @@ func (p *ProxyServer) stop() { } func (p *ProxyServer) handleRequest(t *testing.T, in net.Conn, proxyStarted func(), waitForServerHello bool) { - p.in = in - // This will be used in tests to check if the proxy server is started. if proxyStarted != nil { proxyStarted() @@ -102,8 +99,7 @@ func (p *ProxyServer) handleRequest(t *testing.T, in net.Conn, proxyStarted func t.Logf("Dialing to %s", req.URL.Host) out, err := net.Dial("tcp", req.URL.Host) if err != nil { - p.in.Close() - p.in = nil + in.Close() t.Logf("failed to dial to server: %v", err) return } @@ -127,6 +123,7 @@ func (p *ProxyServer) handleRequest(t *testing.T, in net.Conn, proxyStarted func } buf.Write(b[0:bytesRead]) } + p.in = in p.in.Write(buf.Bytes()) p.out = out wg := sync.WaitGroup{} @@ -140,7 +137,6 @@ func (p *ProxyServer) handleRequest(t *testing.T, in net.Conn, proxyStarted func wg.Done() }() wg.Wait() - t.Logf("emchadnwani: proxy go routine exits") } // Creates and starts a proxy server. @@ -155,9 +151,7 @@ func newProxyServer(t *testing.T, lis net.Listener, reqCheck func(*http.Request) go func() { for { in, err := p.lis.Accept() - fmt.Printf("emchandwani1 : %v\n", err) if err != nil { - // t.Logf("Shutting down proxy server: %v ", err) return } // p.handleRequest is not invoked in a goroutine because the test @@ -179,12 +173,12 @@ func SetupProxy(t *testing.T, reqCheck func(*http.Request) error, waitForServerH } proxyStartedCh := make(chan struct{}) - // var once sync.Once + var once sync.Once fmt.Printf("emchadnwani proxy staring at : %v", pLis.Addr().String()) proxyServer := newProxyServer(t, pLis, reqCheck, func() { - // once.Do(func() { - // close(proxyStartedCh) - // }) + once.Do(func() { + close(proxyStartedCh) + }) }, waitForServerHello) t.Cleanup(proxyServer.stop) pAddr := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, pLis.Addr().String())) From 9acd8de9c90496f55e817c9bb5edd65c5b820563 Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Fri, 3 Jan 2025 21:35:03 +0530 Subject: [PATCH 32/53] correct vet --- internal/transport/proxy_ext_test.go | 98 ++++++++++++++++++---------- internal/transport/proxy_test.go | 2 +- 2 files changed, 66 insertions(+), 34 deletions(-) diff --git a/internal/transport/proxy_ext_test.go b/internal/transport/proxy_ext_test.go index 8dd467007c76..390d0dc66fa5 100644 --- a/internal/transport/proxy_ext_test.go +++ b/internal/transport/proxy_ext_test.go @@ -73,10 +73,14 @@ func (s) TestGRPCDialWithProxy(t *testing.T) { // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { - return &url.URL{ - Scheme: "https", - Host: unresolvedProxyURI, - }, nil + if req.URL.Host == unresolvedProxyURI { + return &url.URL{ + Scheme: "https", + Host: unresolvedProxyURI, + }, nil + } + t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, unresolvedProxyURI) + return nil, nil } orighpfe := delegatingresolver.HTTPSProxyFromEnvironment delegatingresolver.HTTPSProxyFromEnvironment = hpfe @@ -111,10 +115,14 @@ func (s) TestGRPCDialWithDNSAndProxy(t *testing.T) { // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { - return &url.URL{ - Scheme: "https", - Host: unresolvedProxyURI, - }, nil + if req.URL.Host == unresolvedProxyURI { + return &url.URL{ + Scheme: "https", + Host: unresolvedProxyURI, + }, nil + } + t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, unresolvedProxyURI) + return nil, nil } orighpfe := delegatingresolver.HTTPSProxyFromEnvironment delegatingresolver.HTTPSProxyFromEnvironment = hpfe @@ -148,10 +156,14 @@ func (s) TestGRPCNewClientWithProxy(t *testing.T) { // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { - return &url.URL{ - Scheme: "https", - Host: unresolvedProxyURI, - }, nil + if req.URL.Host == unresolvedProxyURI { + return &url.URL{ + Scheme: "https", + Host: unresolvedProxyURI, + }, nil + } + t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, unresolvedProxyURI) + return nil, nil } orighpfe := delegatingresolver.HTTPSProxyFromEnvironment delegatingresolver.HTTPSProxyFromEnvironment = hpfe @@ -185,10 +197,14 @@ func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { - return &url.URL{ - Scheme: "https", - Host: unresolvedProxyURI, - }, nil + if req.URL.Host == unresolvedProxyURI { + return &url.URL{ + Scheme: "https", + Host: unresolvedProxyURI, + }, nil + } + t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, unresolvedProxyURI) + return nil, nil } orighpfe := delegatingresolver.HTTPSProxyFromEnvironment delegatingresolver.HTTPSProxyFromEnvironment = hpfe @@ -230,10 +246,14 @@ func (s) TestGRPCNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { - return &url.URL{ - Scheme: "https", - Host: unresolvedProxyURI, - }, nil + if req.URL.Host == unresolvedProxyURI { + return &url.URL{ + Scheme: "https", + Host: unresolvedProxyURI, + }, nil + } + t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, unresolvedProxyURI) + return nil, nil } orighpfe := delegatingresolver.HTTPSProxyFromEnvironment delegatingresolver.HTTPSProxyFromEnvironment = hpfe @@ -269,10 +289,14 @@ func (s) TestGRPCNewClientWithNoProxy(t *testing.T) { // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { - return &url.URL{ - Scheme: "https", - Host: unresolvedProxyURI, - }, nil + if req.URL.Host == unresolvedProxyURI { + return &url.URL{ + Scheme: "https", + Host: unresolvedProxyURI, + }, nil + } + t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, unresolvedProxyURI) + return nil, nil } orighpfe := delegatingresolver.HTTPSProxyFromEnvironment delegatingresolver.HTTPSProxyFromEnvironment = hpfe @@ -320,10 +344,14 @@ func (s) TestGRPCNewClientWithContextDialer(t *testing.T) { // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { - return &url.URL{ - Scheme: "https", - Host: unresolvedProxyURI, - }, nil + if req.URL.Host == unresolvedProxyURI { + return &url.URL{ + Scheme: "https", + Host: unresolvedProxyURI, + }, nil + } + t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, unresolvedProxyURI) + return nil, nil } orighpfe := delegatingresolver.HTTPSProxyFromEnvironment delegatingresolver.HTTPSProxyFromEnvironment = hpfe @@ -395,11 +423,15 @@ func (s) TestBasicAuthInGrpcNewClientWithProxy(t *testing.T) { // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { - return &url.URL{ - Scheme: "https", - User: url.UserPassword(user, password), - Host: unresolvedProxyURI, - }, nil + if req.URL.Host == unresolvedProxyURI { + return &url.URL{ + Scheme: "https", + User: url.UserPassword(user, password), + Host: unresolvedProxyURI, + }, nil + } + t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, unresolvedProxyURI) + return nil, nil } orighpfe := delegatingresolver.HTTPSProxyFromEnvironment delegatingresolver.HTTPSProxyFromEnvironment = hpfe diff --git a/internal/transport/proxy_test.go b/internal/transport/proxy_test.go index 61383eedea56..95401f526b70 100644 --- a/internal/transport/proxy_test.go +++ b/internal/transport/proxy_test.go @@ -58,7 +58,7 @@ func (s) TestHTTPConnectWithServerHello(t *testing.T) { }() // Overwrite the function in the test and restore them in defer. - hpfe := func(req *http.Request) (*url.URL, error) { + hpfe := func(_ *http.Request) (*url.URL, error) { return &url.URL{ Scheme: "https", Host: proxyAddr, From 8f5055e3374387a71e185e10fabfa9c7ca348128 Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Fri, 3 Jan 2025 21:39:01 +0530 Subject: [PATCH 33/53] vet --- internal/transport/proxy_ext_test.go | 32 ++++++++++++++-------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/internal/transport/proxy_ext_test.go b/internal/transport/proxy_ext_test.go index 390d0dc66fa5..8c273e4a9150 100644 --- a/internal/transport/proxy_ext_test.go +++ b/internal/transport/proxy_ext_test.go @@ -73,13 +73,13 @@ func (s) TestGRPCDialWithProxy(t *testing.T) { // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { - if req.URL.Host == unresolvedProxyURI { + if req.URL.Host == unresolvedTargetURI { return &url.URL{ Scheme: "https", Host: unresolvedProxyURI, }, nil } - t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, unresolvedProxyURI) + t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, unresolvedTargetURI) return nil, nil } orighpfe := delegatingresolver.HTTPSProxyFromEnvironment @@ -115,13 +115,13 @@ func (s) TestGRPCDialWithDNSAndProxy(t *testing.T) { // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { - if req.URL.Host == unresolvedProxyURI { + if req.URL.Host == unresolvedTargetURI { return &url.URL{ Scheme: "https", Host: unresolvedProxyURI, }, nil } - t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, unresolvedProxyURI) + t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, unresolvedTargetURI) return nil, nil } orighpfe := delegatingresolver.HTTPSProxyFromEnvironment @@ -156,13 +156,13 @@ func (s) TestGRPCNewClientWithProxy(t *testing.T) { // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { - if req.URL.Host == unresolvedProxyURI { + if req.URL.Host == unresolvedTargetURI { return &url.URL{ Scheme: "https", Host: unresolvedProxyURI, }, nil } - t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, unresolvedProxyURI) + t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, unresolvedTargetURI) return nil, nil } orighpfe := delegatingresolver.HTTPSProxyFromEnvironment @@ -197,13 +197,13 @@ func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { - if req.URL.Host == unresolvedProxyURI { + if req.URL.Host == unresolvedTargetURI { return &url.URL{ Scheme: "https", Host: unresolvedProxyURI, }, nil } - t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, unresolvedProxyURI) + t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, unresolvedTargetURI) return nil, nil } orighpfe := delegatingresolver.HTTPSProxyFromEnvironment @@ -246,13 +246,13 @@ func (s) TestGRPCNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { - if req.URL.Host == unresolvedProxyURI { + if req.URL.Host == unresolvedTargetURI { return &url.URL{ Scheme: "https", Host: unresolvedProxyURI, }, nil } - t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, unresolvedProxyURI) + t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, unresolvedTargetURI) return nil, nil } orighpfe := delegatingresolver.HTTPSProxyFromEnvironment @@ -289,13 +289,13 @@ func (s) TestGRPCNewClientWithNoProxy(t *testing.T) { // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { - if req.URL.Host == unresolvedProxyURI { + if req.URL.Host == unresolvedTargetURI { return &url.URL{ Scheme: "https", Host: unresolvedProxyURI, }, nil } - t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, unresolvedProxyURI) + t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, unresolvedTargetURI) return nil, nil } orighpfe := delegatingresolver.HTTPSProxyFromEnvironment @@ -344,13 +344,13 @@ func (s) TestGRPCNewClientWithContextDialer(t *testing.T) { // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { - if req.URL.Host == unresolvedProxyURI { + if req.URL.Host == unresolvedTargetURI { return &url.URL{ Scheme: "https", Host: unresolvedProxyURI, }, nil } - t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, unresolvedProxyURI) + t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, unresolvedTargetURI) return nil, nil } orighpfe := delegatingresolver.HTTPSProxyFromEnvironment @@ -423,14 +423,14 @@ func (s) TestBasicAuthInGrpcNewClientWithProxy(t *testing.T) { // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { - if req.URL.Host == unresolvedProxyURI { + if req.URL.Host == unresolvedTargetURI { return &url.URL{ Scheme: "https", User: url.UserPassword(user, password), Host: unresolvedProxyURI, }, nil } - t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, unresolvedProxyURI) + t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, unresolvedTargetURI) return nil, nil } orighpfe := delegatingresolver.HTTPSProxyFromEnvironment From bbd7f02d62a7d55a2e94ee866756596d2773fd78 Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Fri, 3 Jan 2025 21:46:48 +0530 Subject: [PATCH 34/53] vet --- internal/transport/proxy_ext_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/transport/proxy_ext_test.go b/internal/transport/proxy_ext_test.go index 8c273e4a9150..a291f4ca4400 100644 --- a/internal/transport/proxy_ext_test.go +++ b/internal/transport/proxy_ext_test.go @@ -66,7 +66,7 @@ func createAndStartBackendServer(t *testing.T) string { // Tests the scenario where grpc.Dial is performed using a proxy with the // default resolver in the target URI. The test verifies that the connection is // established to the proxy server, sends the unresolved target URI in the HTTP -// CONNECT request, and is successfully connected to the backend server. +// CONNECT request and is successfully connected to the backend server. func (s) TestGRPCDialWithProxy(t *testing.T) { unresolvedTargetURI := createAndStartBackendServer(t) unresolvedProxyURI, _ := transport.SetupProxy(t, transport.RequestCheck(false), false) From 128da2cfdc1712889a4dbf5f34562b3c831273f4 Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Tue, 7 Jan 2025 20:03:00 +0530 Subject: [PATCH 35/53] addresses comments --- internal/transport/proxy_ext_test.go | 145 ++++++++++++------ internal/transport/proxy_test.go | 36 +++-- .../testutils.go} | 107 +++---------- 3 files changed, 145 insertions(+), 143 deletions(-) rename internal/transport/{proxy_utils.go => testutils/testutils.go} (50%) diff --git a/internal/transport/proxy_ext_test.go b/internal/transport/proxy_ext_test.go index a291f4ca4400..5fdd2c06519e 100644 --- a/internal/transport/proxy_ext_test.go +++ b/internal/transport/proxy_ext_test.go @@ -24,6 +24,7 @@ import ( "fmt" "net" "net/http" + "net/netip" "net/url" "testing" "time" @@ -34,7 +35,7 @@ import ( "google.golang.org/grpc/internal/resolver/delegatingresolver" "google.golang.org/grpc/internal/stubserver" "google.golang.org/grpc/internal/testutils" - "google.golang.org/grpc/internal/transport" + tu "google.golang.org/grpc/internal/transport/testutils" testgrpc "google.golang.org/grpc/interop/grpc_testing" "google.golang.org/grpc/resolver" "google.golang.org/grpc/resolver/manual" @@ -63,20 +64,37 @@ func createAndStartBackendServer(t *testing.T) string { return fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backend.Address)) } +func isIPAddr(addr string) bool { + _, err := netip.ParseAddr(addr) + return err == nil +} + // Tests the scenario where grpc.Dial is performed using a proxy with the // default resolver in the target URI. The test verifies that the connection is // established to the proxy server, sends the unresolved target URI in the HTTP // CONNECT request and is successfully connected to the backend server. func (s) TestGRPCDialWithProxy(t *testing.T) { unresolvedTargetURI := createAndStartBackendServer(t) - unresolvedProxyURI, _ := transport.SetupProxy(t, transport.RequestCheck(false), false) + reqCheck := func(req *http.Request) { + if req.Method != http.MethodConnect { + t.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) + } + host, _, err := net.SplitHostPort(req.URL.Host) + if err != nil { + t.Error(err) + } + if got, want := isIPAddr(host), false; got != want { + t.Errorf("isIPAddr(%q) = %t, want = false", req.URL.Host, got) + } + } + pServer := tu.SetupProxy(t, reqCheck, false) // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { if req.URL.Host == unresolvedTargetURI { return &url.URL{ Scheme: "https", - Host: unresolvedProxyURI, + Host: pServer.Addr, }, nil } t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, unresolvedTargetURI) @@ -111,14 +129,26 @@ func (s) TestGRPCDialWithProxy(t *testing.T) { // HTTP CONNECT request, successfully connecting to the backend server. func (s) TestGRPCDialWithDNSAndProxy(t *testing.T) { unresolvedTargetURI := createAndStartBackendServer(t) - unresolvedProxyURI, _ := transport.SetupProxy(t, transport.RequestCheck(true), false) + reqCheck := func(req *http.Request) { + if req.Method != http.MethodConnect { + t.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) + } + host, _, err := net.SplitHostPort(req.URL.Host) + if err != nil { + t.Error(err) + } + if got, want := isIPAddr(host), true; got != want { + t.Errorf("isIPAddr(%q) = %t, want = true", req.URL.Host, got) + } + } + pServer := tu.SetupProxy(t, reqCheck, false) // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { if req.URL.Host == unresolvedTargetURI { return &url.URL{ Scheme: "https", - Host: unresolvedProxyURI, + Host: pServer.Addr, }, nil } t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, unresolvedTargetURI) @@ -152,14 +182,26 @@ func (s) TestGRPCDialWithDNSAndProxy(t *testing.T) { // establishes a connection to the backend server. func (s) TestGRPCNewClientWithProxy(t *testing.T) { unresolvedTargetURI := createAndStartBackendServer(t) - unresolvedProxyURI, _ := transport.SetupProxy(t, transport.RequestCheck(false), false) + reqCheck := func(req *http.Request) { + if req.Method != http.MethodConnect { + t.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) + } + host, _, err := net.SplitHostPort(req.URL.Host) + if err != nil { + t.Error(err) + } + if got, want := isIPAddr(host), false; got != want { + t.Errorf("isIPAddr(%q) = %t, want = false", req.URL.Host, got) + } + } + pServer := tu.SetupProxy(t, reqCheck, false) // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { if req.URL.Host == unresolvedTargetURI { return &url.URL{ Scheme: "https", - Host: unresolvedProxyURI, + Host: pServer.Addr, }, nil } t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, unresolvedTargetURI) @@ -193,14 +235,26 @@ func (s) TestGRPCNewClientWithProxy(t *testing.T) { // establishes a connection to the backend server. func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { unresolvedTargetURI := createAndStartBackendServer(t) - unresolvedProxyURI, _ := transport.SetupProxy(t, transport.RequestCheck(true), false) + reqCheck := func(req *http.Request) { + if req.Method != http.MethodConnect { + t.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) + } + host, _, err := net.SplitHostPort(req.URL.Host) + if err != nil { + t.Error(err) + } + if got, want := isIPAddr(host), true; got != want { + t.Errorf("isIPAddr(%q) = %t, want = true", req.URL.Host, got) + } + } + pServer := tu.SetupProxy(t, reqCheck, false) // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { if req.URL.Host == unresolvedTargetURI { return &url.URL{ Scheme: "https", - Host: unresolvedProxyURI, + Host: pServer.Addr, }, nil } t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, unresolvedTargetURI) @@ -225,7 +279,7 @@ func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { if err != nil { t.Fatalf("grpc.NewClient(%s) failed: %v", targetResolver.Scheme()+":///"+unresolvedTargetURI, err) } - t.Cleanup(func() { conn.Close() }) + defer conn.Close() // Send an empty RPC to the backend through the proxy. client := testgrpc.NewTestServiceClient(conn) @@ -241,7 +295,19 @@ func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { // CONNECT request, the proxy URI is resolved correctly, and the connection is // successfully established with the backend server through the proxy. func (s) TestGRPCNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { - unresolvedProxyURI, _ := transport.SetupProxy(t, transport.RequestCheck(true), false) + reqCheck := func(req *http.Request) { + if req.Method != http.MethodConnect { + t.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) + } + host, _, err := net.SplitHostPort(req.URL.Host) + if err != nil { + t.Error(err) + } + if got, want := isIPAddr(host), true; got != want { + t.Errorf("isIPAddr(%q) = %t, want = true", req.URL.Host, got) + } + } + pServer := tu.SetupProxy(t, reqCheck, false) unresolvedTargetURI := createAndStartBackendServer(t) // Overwrite the function in the test and restore them in defer. @@ -249,7 +315,7 @@ func (s) TestGRPCNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { if req.URL.Host == unresolvedTargetURI { return &url.URL{ Scheme: "https", - Host: unresolvedProxyURI, + Host: pServer.Addr, }, nil } t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, unresolvedTargetURI) @@ -283,16 +349,17 @@ func (s) TestGRPCNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { // never receives a connection request. func (s) TestGRPCNewClientWithNoProxy(t *testing.T) { unresolvedTargetURI := createAndStartBackendServer(t) - unresolvedProxyURI, proxyStartedCh := transport.SetupProxy(t, func(req *http.Request) error { - return fmt.Errorf("proxy server should not have received a Connect request: %v", req) - }, false) + reqCheck := func(_ *http.Request) { + t.Error("proxy server should not have received a Connect request") + } + pServer := tu.SetupProxy(t, reqCheck, false) // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { if req.URL.Host == unresolvedTargetURI { return &url.URL{ Scheme: "https", - Host: unresolvedProxyURI, + Host: pServer.Addr, }, nil } t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, unresolvedTargetURI) @@ -321,14 +388,6 @@ func (s) TestGRPCNewClientWithNoProxy(t *testing.T) { if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { t.Fatalf("EmptyCall() failed: %v", err) } - - // Verify that the proxy server was not dialed. - select { - case <-proxyStartedCh: - t.Fatal("unexpected dial to proxy server") - default: - t.Log("proxy server was not dialed") - } } // Tests the scenario where grpc.NewClient is used with grpc.WithContextDialer() @@ -338,16 +397,17 @@ func (s) TestGRPCNewClientWithNoProxy(t *testing.T) { // as expected. func (s) TestGRPCNewClientWithContextDialer(t *testing.T) { unresolvedTargetURI := createAndStartBackendServer(t) - unresolvedProxyURI, proxyStartedCh := transport.SetupProxy(t, func(req *http.Request) error { - return fmt.Errorf("proxy server should not have received a Connect request: %v", req) - }, false) + reqCheck := func(_ *http.Request) { + t.Error("proxy server should not have received a Connect request") + } + pServer := tu.SetupProxy(t, reqCheck, false) // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { if req.URL.Host == unresolvedTargetURI { return &url.URL{ Scheme: "https", - Host: unresolvedProxyURI, + Host: pServer.Addr, }, nil } t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, unresolvedTargetURI) @@ -380,13 +440,6 @@ func (s) TestGRPCNewClientWithContextDialer(t *testing.T) { if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { t.Fatalf("EmptyCall() failed: %v", err) } - - select { - case <-proxyStartedCh: - t.Fatal("unexpected dial to proxy server") - default: - t.Log("proxy server was not dialled") - } } // Tests the scenario where grpc.NewClient is used with the default DNS resolver @@ -402,24 +455,28 @@ func (s) TestBasicAuthInGrpcNewClientWithProxy(t *testing.T) { user = "notAUser" password = "notAPassword" ) - unresolvedProxyURI, _ := transport.SetupProxy(t, func(req *http.Request) error { + reqCheck := func(req *http.Request) { if req.Method != http.MethodConnect { - return fmt.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) + t.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) } - if req.URL.Host != unresolvedTargetURI { - return fmt.Errorf("unexpected URL.Host %q, want %q", req.URL.Host, unresolvedTargetURI) + host, _, err := net.SplitHostPort(req.URL.Host) + if err != nil { + t.Error(err) + } + if got, want := isIPAddr(host), false; got != want { + t.Errorf("isIPAddr(%q) = %t, want = false", req.URL.Host, got) } wantProxyAuthStr := "Basic " + base64.StdEncoding.EncodeToString([]byte(user+":"+password)) if got := req.Header.Get("Proxy-Authorization"); got != wantProxyAuthStr { gotDecoded, err := base64.StdEncoding.DecodeString(got) if err != nil { - return fmt.Errorf("failed to decode Proxy-Authorization header: %v", err) + t.Errorf("failed to decode Proxy-Authorization header: %v", err) } wantDecoded, _ := base64.StdEncoding.DecodeString(wantProxyAuthStr) - return fmt.Errorf("unexpected auth %q (%q), want %q (%q)", got, gotDecoded, wantProxyAuthStr, wantDecoded) + t.Errorf("unexpected auth %q (%q), want %q (%q)", got, gotDecoded, wantProxyAuthStr, wantDecoded) } - return nil - }, false) + } + pServer := tu.SetupProxy(t, reqCheck, false) // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { @@ -427,7 +484,7 @@ func (s) TestBasicAuthInGrpcNewClientWithProxy(t *testing.T) { return &url.URL{ Scheme: "https", User: url.UserPassword(user, password), - Host: unresolvedProxyURI, + Host: pServer.Addr, }, nil } t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, unresolvedTargetURI) diff --git a/internal/transport/proxy_test.go b/internal/transport/proxy_test.go index 95401f526b70..2a54d1f84cdb 100644 --- a/internal/transport/proxy_test.go +++ b/internal/transport/proxy_test.go @@ -25,22 +25,37 @@ import ( "context" "net" "net/http" - "net/url" + "net/netip" "testing" "time" "google.golang.org/grpc/internal/proxyattributes" - "google.golang.org/grpc/internal/resolver/delegatingresolver" + "google.golang.org/grpc/internal/transport/testutils" "google.golang.org/grpc/resolver" ) +const defaultTestTimeout = 10 * time.Second + func (s) TestHTTPConnectWithServerHello(t *testing.T) { serverMessage := []byte("server-hello") blis, err := net.Listen("tcp", "localhost:0") if err != nil { t.Fatalf("failed to listen: %v", err) } - proxyAddr, _ := SetupProxy(t, RequestCheck(true), true) + reqCheck := func(req *http.Request) { + if req.Method != http.MethodConnect { + t.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) + } + host, _, err := net.SplitHostPort(req.URL.Host) + if err != nil { + t.Error(err) + } + _, err = netip.ParseAddr(host) + if err != nil { + t.Error(err) + } + } + pServer := testutils.SetupProxy(t, reqCheck, true) msg := []byte{4, 3, 5, 2} recvBuf := make([]byte, len(msg)) @@ -57,23 +72,10 @@ func (s) TestHTTPConnectWithServerHello(t *testing.T) { done <- nil }() - // Overwrite the function in the test and restore them in defer. - hpfe := func(_ *http.Request) (*url.URL, error) { - return &url.URL{ - Scheme: "https", - Host: proxyAddr, - }, nil - } - orighpfe := delegatingresolver.HTTPSProxyFromEnvironment - delegatingresolver.HTTPSProxyFromEnvironment = hpfe - defer func() { - delegatingresolver.HTTPSProxyFromEnvironment = orighpfe - }() - // Dial to proxy server. ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() - c, err := proxyDial(ctx, resolver.Address{Addr: proxyAddr}, "test", proxyattributes.Options{ConnectAddr: blis.Addr().String()}) + c, err := proxyDial(ctx, resolver.Address{Addr: pServer.Addr}, "test", proxyattributes.Options{ConnectAddr: blis.Addr().String()}) if err != nil { t.Fatalf("HTTP connect Dial failed: %v", err) } diff --git a/internal/transport/proxy_utils.go b/internal/transport/testutils/testutils.go similarity index 50% rename from internal/transport/proxy_utils.go rename to internal/transport/testutils/testutils.go index c03ffe75ce21..e0eac2ffeed1 100644 --- a/internal/transport/proxy_utils.go +++ b/internal/transport/testutils/testutils.go @@ -16,7 +16,7 @@ * */ -package transport +package testutils import ( "bufio" @@ -25,8 +25,6 @@ import ( "io" "net" "net/http" - "net/netip" - "sync" "testing" "time" @@ -35,36 +33,13 @@ import ( const defaultTestTimeout = 10 * time.Second -// RequestCheck returns a function that validates the CONNECT method and -// determines whether the address is resolved or unresolved. It only checks if -// the address is an IP address or not, relying on RPC failures to handle -// incorrect resolutions. -func RequestCheck(isResolved bool) func(*http.Request) error { - return func(req *http.Request) error { - if req.Method != http.MethodConnect { - return fmt.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) - } - host, _, err := net.SplitHostPort(req.URL.Host) - if err != nil { - return err - } - _, err = netip.ParseAddr(host) - if isResolved && err != nil { - return fmt.Errorf("unexpected URL.Host in CONNECT req %q, want an IP address", req.URL.Host) - } - if !isResolved && err == nil { - return fmt.Errorf("unexpected URL.Host in CONNECT req %q, do not want an IP address", req.URL.Host) - } - return nil - } -} - // ProxyServer represents a test proxy server. type ProxyServer struct { - lis net.Listener - in net.Conn // Connection from the client to the proxy. - out net.Conn // Connection from the proxy to the backend. - requestCheck func(*http.Request) error // Function to check the request sent to proxy. + lis net.Listener + in net.Conn // Connection from the client to the proxy. + out net.Conn // Connection from the proxy to the backend. + onRequest func(*http.Request) // Function to check the request sent to proxy. + Addr string // Address of the proxy } // Stop closes the ProxyServer and its connections to client and server. @@ -78,23 +53,14 @@ func (p *ProxyServer) stop() { } } -func (p *ProxyServer) handleRequest(t *testing.T, in net.Conn, proxyStarted func(), waitForServerHello bool) { - // This will be used in tests to check if the proxy server is started. - if proxyStarted != nil { - proxyStarted() - } +func (p *ProxyServer) handleRequest(t *testing.T, in net.Conn, waitForServerHello bool) { req, err := http.ReadRequest(bufio.NewReader(in)) if err != nil { t.Errorf("failed to read CONNECT req: %v", err) return } - if err := p.requestCheck(req); err != nil { - resp := http.Response{StatusCode: http.StatusMethodNotAllowed} - resp.Write(p.in) - p.in.Close() - t.Errorf("failed to read CONNECT req: %v", err) - return - } + + p.onRequest(req) t.Logf("Dialing to %s", req.URL.Host) out, err := net.Dial("tcp", req.URL.Host) @@ -126,25 +92,23 @@ func (p *ProxyServer) handleRequest(t *testing.T, in net.Conn, proxyStarted func p.in = in p.in.Write(buf.Bytes()) p.out = out - wg := sync.WaitGroup{} - wg.Add(2) - go func() { - io.Copy(p.in, p.out) - wg.Done() - }() - go func() { - io.Copy(p.out, p.in) - wg.Done() - }() - wg.Wait() + + go io.Copy(p.in, p.out) + go io.Copy(p.out, p.in) } -// Creates and starts a proxy server. -func newProxyServer(t *testing.T, lis net.Listener, reqCheck func(*http.Request) error, proxyStarted func(), waitForServerHello bool) *ProxyServer { +// SetupProxy initializes and starts a proxy server, registers a cleanup to +// stop it, and returns the proxy's listener and helper channels. +func SetupProxy(t *testing.T, reqCheck func(*http.Request), waitForServerHello bool) *ProxyServer { t.Helper() + pLis, err := net.Listen("tcp", "localhost:0") + if err != nil { + t.Fatalf("failed to listen: %v", err) + } + p := &ProxyServer{ - lis: lis, - requestCheck: reqCheck, + lis: pLis, + onRequest: reqCheck, } // Start the proxy server. @@ -156,31 +120,10 @@ func newProxyServer(t *testing.T, lis net.Listener, reqCheck func(*http.Request) } // p.handleRequest is not invoked in a goroutine because the test // proxy currently supports handling only one connection at a time. - p.handleRequest(t, in, proxyStarted, waitForServerHello) - + p.handleRequest(t, in, waitForServerHello) } }() + t.Cleanup(p.stop) + p.Addr = fmt.Sprintf("localhost:%d", testutils.ParsePort(t, pLis.Addr().String())) return p } - -// SetupProxy initializes and starts a proxy server, registers a cleanup to -// stop it, and returns the proxy's listener and helper channels. -func SetupProxy(t *testing.T, reqCheck func(*http.Request) error, waitForServerHello bool) (string, chan struct{}) { - t.Helper() - pLis, err := net.Listen("tcp", "localhost:0") - if err != nil { - t.Fatalf("failed to listen: %v", err) - } - - proxyStartedCh := make(chan struct{}) - var once sync.Once - fmt.Printf("emchadnwani proxy staring at : %v", pLis.Addr().String()) - proxyServer := newProxyServer(t, pLis, reqCheck, func() { - once.Do(func() { - close(proxyStartedCh) - }) - }, waitForServerHello) - t.Cleanup(proxyServer.stop) - pAddr := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, pLis.Addr().String())) - return pAddr, proxyStartedCh -} From 7cb9ec884d773c529664787c9956dcce24381128 Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Tue, 7 Jan 2025 20:31:50 +0530 Subject: [PATCH 36/53] add package comment --- internal/transport/testutils/testutils.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/transport/testutils/testutils.go b/internal/transport/testutils/testutils.go index e0eac2ffeed1..f35d88b3ad3c 100644 --- a/internal/transport/testutils/testutils.go +++ b/internal/transport/testutils/testutils.go @@ -16,6 +16,7 @@ * */ +// Package testutils contains helpers for transport tests. package testutils import ( From 0d055b393ab067bb64ccb7f626c129e031d95152 Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Tue, 7 Jan 2025 20:45:51 +0530 Subject: [PATCH 37/53] correct timeout --- internal/transport/keepalive_test.go | 5 ++++- internal/transport/proxy_test.go | 2 -- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/transport/keepalive_test.go b/internal/transport/keepalive_test.go index 6721ea3dbe42..76cf12dca888 100644 --- a/internal/transport/keepalive_test.go +++ b/internal/transport/keepalive_test.go @@ -43,7 +43,10 @@ import ( "google.golang.org/grpc/testdata" ) -const defaultTestShortTimeout = 10 * time.Millisecond +const ( + defaultTestShortTimeout = 10 * time.Millisecond + defaultTestTimeout = 10 * time.Second +) // TestMaxConnectionIdle tests that a server will send GoAway to an idle // client. An idle client is one who doesn't make any RPC calls for a duration diff --git a/internal/transport/proxy_test.go b/internal/transport/proxy_test.go index 2a54d1f84cdb..4b76cbab0a45 100644 --- a/internal/transport/proxy_test.go +++ b/internal/transport/proxy_test.go @@ -34,8 +34,6 @@ import ( "google.golang.org/grpc/resolver" ) -const defaultTestTimeout = 10 * time.Second - func (s) TestHTTPConnectWithServerHello(t *testing.T) { serverMessage := []byte("server-hello") blis, err := net.Listen("tcp", "localhost:0") From 07b93442a21a32fc7e4c5dd0731547a942ff1149 Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Wed, 8 Jan 2025 11:18:24 +0530 Subject: [PATCH 38/53] format --- internal/transport/keepalive_test.go | 6 ++-- internal/transport/proxy_ext_test.go | 44 +++++++++------------------- 2 files changed, 16 insertions(+), 34 deletions(-) diff --git a/internal/transport/keepalive_test.go b/internal/transport/keepalive_test.go index 76cf12dca888..037b0b1c1b77 100644 --- a/internal/transport/keepalive_test.go +++ b/internal/transport/keepalive_test.go @@ -43,10 +43,8 @@ import ( "google.golang.org/grpc/testdata" ) -const ( - defaultTestShortTimeout = 10 * time.Millisecond - defaultTestTimeout = 10 * time.Second -) +const defaultTestTimeout = 10 * time.Second +const defaultTestShortTimeout = 10 * time.Millisecond // TestMaxConnectionIdle tests that a server will send GoAway to an idle // client. An idle client is one who doesn't make any RPC calls for a duration diff --git a/internal/transport/proxy_ext_test.go b/internal/transport/proxy_ext_test.go index 5fdd2c06519e..3964978b667c 100644 --- a/internal/transport/proxy_ext_test.go +++ b/internal/transport/proxy_ext_test.go @@ -84,7 +84,7 @@ func (s) TestGRPCDialWithProxy(t *testing.T) { t.Error(err) } if got, want := isIPAddr(host), false; got != want { - t.Errorf("isIPAddr(%q) = %t, want = false", req.URL.Host, got) + t.Errorf("isIPAddr(%q) = %t, want = %t", host, got, want) } } pServer := tu.SetupProxy(t, reqCheck, false) @@ -102,9 +102,7 @@ func (s) TestGRPCDialWithProxy(t *testing.T) { } orighpfe := delegatingresolver.HTTPSProxyFromEnvironment delegatingresolver.HTTPSProxyFromEnvironment = hpfe - defer func() { - delegatingresolver.HTTPSProxyFromEnvironment = orighpfe - }() + defer func() { delegatingresolver.HTTPSProxyFromEnvironment = orighpfe }() ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() @@ -138,7 +136,7 @@ func (s) TestGRPCDialWithDNSAndProxy(t *testing.T) { t.Error(err) } if got, want := isIPAddr(host), true; got != want { - t.Errorf("isIPAddr(%q) = %t, want = true", req.URL.Host, got) + t.Errorf("isIPAddr(%q) = %t, want = %t", host, got, want) } } pServer := tu.SetupProxy(t, reqCheck, false) @@ -156,9 +154,7 @@ func (s) TestGRPCDialWithDNSAndProxy(t *testing.T) { } orighpfe := delegatingresolver.HTTPSProxyFromEnvironment delegatingresolver.HTTPSProxyFromEnvironment = hpfe - defer func() { - delegatingresolver.HTTPSProxyFromEnvironment = orighpfe - }() + defer func() { delegatingresolver.HTTPSProxyFromEnvironment = orighpfe }() ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() @@ -191,7 +187,7 @@ func (s) TestGRPCNewClientWithProxy(t *testing.T) { t.Error(err) } if got, want := isIPAddr(host), false; got != want { - t.Errorf("isIPAddr(%q) = %t, want = false", req.URL.Host, got) + t.Errorf("isIPAddr(%q) = %t, want = %t", host, got, want) } } pServer := tu.SetupProxy(t, reqCheck, false) @@ -209,9 +205,7 @@ func (s) TestGRPCNewClientWithProxy(t *testing.T) { } orighpfe := delegatingresolver.HTTPSProxyFromEnvironment delegatingresolver.HTTPSProxyFromEnvironment = hpfe - defer func() { - delegatingresolver.HTTPSProxyFromEnvironment = orighpfe - }() + defer func() { delegatingresolver.HTTPSProxyFromEnvironment = orighpfe }() ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() @@ -244,7 +238,7 @@ func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { t.Error(err) } if got, want := isIPAddr(host), true; got != want { - t.Errorf("isIPAddr(%q) = %t, want = true", req.URL.Host, got) + t.Errorf("isIPAddr(%q) = %t, want = %t", host, got, want) } } pServer := tu.SetupProxy(t, reqCheck, false) @@ -262,9 +256,7 @@ func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { } orighpfe := delegatingresolver.HTTPSProxyFromEnvironment delegatingresolver.HTTPSProxyFromEnvironment = hpfe - defer func() { - delegatingresolver.HTTPSProxyFromEnvironment = orighpfe - }() + defer func() { delegatingresolver.HTTPSProxyFromEnvironment = orighpfe }() // Create and update a custom resolver for target URI. resolvedTargetURI := fmt.Sprintf("[::0]:%d", testutils.ParsePort(t, unresolvedTargetURI)) @@ -304,7 +296,7 @@ func (s) TestGRPCNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { t.Error(err) } if got, want := isIPAddr(host), true; got != want { - t.Errorf("isIPAddr(%q) = %t, want = true", req.URL.Host, got) + t.Errorf("isIPAddr(%q) = %t, want = %t", host, got, want) } } pServer := tu.SetupProxy(t, reqCheck, false) @@ -323,9 +315,7 @@ func (s) TestGRPCNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { } orighpfe := delegatingresolver.HTTPSProxyFromEnvironment delegatingresolver.HTTPSProxyFromEnvironment = hpfe - defer func() { - delegatingresolver.HTTPSProxyFromEnvironment = orighpfe - }() + defer func() { delegatingresolver.HTTPSProxyFromEnvironment = orighpfe }() ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() @@ -367,9 +357,7 @@ func (s) TestGRPCNewClientWithNoProxy(t *testing.T) { } orighpfe := delegatingresolver.HTTPSProxyFromEnvironment delegatingresolver.HTTPSProxyFromEnvironment = hpfe - defer func() { - delegatingresolver.HTTPSProxyFromEnvironment = orighpfe - }() + defer func() { delegatingresolver.HTTPSProxyFromEnvironment = orighpfe }() dopts := []grpc.DialOption{ grpc.WithTransportCredentials(insecure.NewCredentials()), @@ -415,9 +403,7 @@ func (s) TestGRPCNewClientWithContextDialer(t *testing.T) { } orighpfe := delegatingresolver.HTTPSProxyFromEnvironment delegatingresolver.HTTPSProxyFromEnvironment = hpfe - defer func() { - delegatingresolver.HTTPSProxyFromEnvironment = orighpfe - }() + defer func() { delegatingresolver.HTTPSProxyFromEnvironment = orighpfe }() // Create a custom dialer that directly dials the backend. customDialer := func(_ context.Context, unresolvedTargetURI string) (net.Conn, error) { @@ -464,7 +450,7 @@ func (s) TestBasicAuthInGrpcNewClientWithProxy(t *testing.T) { t.Error(err) } if got, want := isIPAddr(host), false; got != want { - t.Errorf("isIPAddr(%q) = %t, want = false", req.URL.Host, got) + t.Errorf("isIPAddr(%q) = %t, want = %t", host, got, want) } wantProxyAuthStr := "Basic " + base64.StdEncoding.EncodeToString([]byte(user+":"+password)) if got := req.Header.Get("Proxy-Authorization"); got != wantProxyAuthStr { @@ -492,9 +478,7 @@ func (s) TestBasicAuthInGrpcNewClientWithProxy(t *testing.T) { } orighpfe := delegatingresolver.HTTPSProxyFromEnvironment delegatingresolver.HTTPSProxyFromEnvironment = hpfe - defer func() { - delegatingresolver.HTTPSProxyFromEnvironment = orighpfe - }() + defer func() { delegatingresolver.HTTPSProxyFromEnvironment = orighpfe }() ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() From f75662df23d548d8f0f607b5211128ea702688d5 Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Thu, 9 Jan 2025 22:00:18 +0530 Subject: [PATCH 39/53] address comments --- internal/transport/proxy_ext_test.go | 147 ++++++++++++---------- internal/transport/proxy_test.go | 7 +- internal/transport/testutils/testutils.go | 10 +- 3 files changed, 88 insertions(+), 76 deletions(-) diff --git a/internal/transport/proxy_ext_test.go b/internal/transport/proxy_ext_test.go index 3964978b667c..f101a4f6997e 100644 --- a/internal/transport/proxy_ext_test.go +++ b/internal/transport/proxy_ext_test.go @@ -35,7 +35,7 @@ import ( "google.golang.org/grpc/internal/resolver/delegatingresolver" "google.golang.org/grpc/internal/stubserver" "google.golang.org/grpc/internal/testutils" - tu "google.golang.org/grpc/internal/transport/testutils" + transporttestutils "google.golang.org/grpc/internal/transport/testutils" testgrpc "google.golang.org/grpc/interop/grpc_testing" "google.golang.org/grpc/resolver" "google.golang.org/grpc/resolver/manual" @@ -51,7 +51,7 @@ func Test(t *testing.T) { grpctest.RunSubTests(t, s{}) } -func createAndStartBackendServer(t *testing.T) string { +func startBackendServer(t *testing.T) string { t.Helper() backend := &stubserver.StubServer{ EmptyCallF: func(context.Context, *testgrpc.Empty) (*testgrpc.Empty, error) { return &testgrpc.Empty{}, nil }, @@ -61,7 +61,7 @@ func createAndStartBackendServer(t *testing.T) string { } t.Logf("Started TestService backend at: %q", backend.Address) t.Cleanup(backend.Stop) - return fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backend.Address)) + return backend.Address } func isIPAddr(addr string) bool { @@ -74,20 +74,16 @@ func isIPAddr(addr string) bool { // established to the proxy server, sends the unresolved target URI in the HTTP // CONNECT request and is successfully connected to the backend server. func (s) TestGRPCDialWithProxy(t *testing.T) { - unresolvedTargetURI := createAndStartBackendServer(t) + backendAddr := startBackendServer(t) + unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backendAddr)) + proxyStarted := false reqCheck := func(req *http.Request) { - if req.Method != http.MethodConnect { - t.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) - } - host, _, err := net.SplitHostPort(req.URL.Host) - if err != nil { - t.Error(err) - } - if got, want := isIPAddr(host), false; got != want { - t.Errorf("isIPAddr(%q) = %t, want = %t", host, got, want) + proxyStarted = true + if got, want := req.URL.Host, unresolvedTargetURI; got != want { + t.Errorf(" Unexpected request host: %s , want = %s ", got, want) } } - pServer := tu.SetupProxy(t, reqCheck, false) + pServer := transporttestutils.HTTPProxy(t, reqCheck, false) // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { @@ -117,6 +113,10 @@ func (s) TestGRPCDialWithProxy(t *testing.T) { if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { t.Fatalf("EmptyCall failed: %v", err) } + + if !proxyStarted { + t.Fatalf("Proxy not started") + } } // Tests the scenario where `grpc.Dial` is performed with a proxy and the "dns" @@ -126,11 +126,12 @@ func (s) TestGRPCDialWithProxy(t *testing.T) { // established to the proxy server, with the resolved target URI sent in the // HTTP CONNECT request, successfully connecting to the backend server. func (s) TestGRPCDialWithDNSAndProxy(t *testing.T) { - unresolvedTargetURI := createAndStartBackendServer(t) + backendAddr := startBackendServer(t) + unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backendAddr)) + proxyStarted := false reqCheck := func(req *http.Request) { - if req.Method != http.MethodConnect { - t.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) - } + proxyStarted = true + host, _, err := net.SplitHostPort(req.URL.Host) if err != nil { t.Error(err) @@ -139,7 +140,7 @@ func (s) TestGRPCDialWithDNSAndProxy(t *testing.T) { t.Errorf("isIPAddr(%q) = %t, want = %t", host, got, want) } } - pServer := tu.SetupProxy(t, reqCheck, false) + pServer := transporttestutils.HTTPProxy(t, reqCheck, false) // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { @@ -169,6 +170,10 @@ func (s) TestGRPCDialWithDNSAndProxy(t *testing.T) { if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { t.Fatalf("EmptyCall failed: %v", err) } + + if !proxyStarted { + t.Fatalf("Proxy not started") + } } // Tests the scenario where `grpc.NewClient` is used with the default DNS @@ -176,21 +181,17 @@ func (s) TestGRPCDialWithDNSAndProxy(t *testing.T) { // that the client resolves proxy URI, connects to the proxy server, sends the // unresolved target URI in the HTTP CONNECT request, and successfully // establishes a connection to the backend server. -func (s) TestGRPCNewClientWithProxy(t *testing.T) { - unresolvedTargetURI := createAndStartBackendServer(t) +func (s) TestNewClientWithProxy(t *testing.T) { + backendAddr := startBackendServer(t) + unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backendAddr)) + proxyStarted := false reqCheck := func(req *http.Request) { - if req.Method != http.MethodConnect { - t.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) - } - host, _, err := net.SplitHostPort(req.URL.Host) - if err != nil { - t.Error(err) - } - if got, want := isIPAddr(host), false; got != want { - t.Errorf("isIPAddr(%q) = %t, want = %t", host, got, want) + proxyStarted = true + if got, want := req.URL.Host, unresolvedTargetURI; got != want { + t.Errorf(" Unexpected request host: %s , want = %s ", got, want) } } - pServer := tu.SetupProxy(t, reqCheck, false) + pServer := transporttestutils.HTTPProxy(t, reqCheck, false) // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { @@ -220,6 +221,9 @@ func (s) TestGRPCNewClientWithProxy(t *testing.T) { if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { t.Fatalf("EmptyCall failed: %v", err) } + if !proxyStarted { + t.Fatalf("Proxy not started") + } } // Tests the scenario where grpc.NewClient is used with a custom target URI @@ -227,12 +231,12 @@ func (s) TestGRPCNewClientWithProxy(t *testing.T) { // successfully connects to the proxy server, resolves the proxy URI correctly, // includes the resolved target URI in the HTTP CONNECT request, and // establishes a connection to the backend server. -func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { - unresolvedTargetURI := createAndStartBackendServer(t) +func (s) TestNewClientWithProxyAndCustomResolver(t *testing.T) { + backendAddr := startBackendServer(t) + unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backendAddr)) + proxyStarted := false reqCheck := func(req *http.Request) { - if req.Method != http.MethodConnect { - t.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) - } + proxyStarted = true host, _, err := net.SplitHostPort(req.URL.Host) if err != nil { t.Error(err) @@ -241,7 +245,7 @@ func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { t.Errorf("isIPAddr(%q) = %t, want = %t", host, got, want) } } - pServer := tu.SetupProxy(t, reqCheck, false) + pServer := transporttestutils.HTTPProxy(t, reqCheck, false) // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { @@ -259,10 +263,9 @@ func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { defer func() { delegatingresolver.HTTPSProxyFromEnvironment = orighpfe }() // Create and update a custom resolver for target URI. - resolvedTargetURI := fmt.Sprintf("[::0]:%d", testutils.ParsePort(t, unresolvedTargetURI)) targetResolver := manual.NewBuilderWithScheme("test") resolver.Register(targetResolver) - targetResolver.InitialState(resolver.State{Endpoints: []resolver.Endpoint{{Addresses: []resolver.Address{{Addr: resolvedTargetURI}}}}}) + targetResolver.InitialState(resolver.State{Endpoints: []resolver.Endpoint{{Addresses: []resolver.Address{{Addr: backendAddr}}}}}) // Dial to the proxy server. ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) @@ -278,6 +281,10 @@ func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { t.Fatalf("EmptyCall() failed: %v", err) } + + if !proxyStarted { + t.Fatalf("Proxy not started") + } } // Tests the scenario where grpc.NewClient is used with the default "dns" @@ -286,11 +293,12 @@ func (s) TestGRPCNewClientWithProxyAndCustomResolver(t *testing.T) { // resolution happens on the client by sending resolved target URI in HTTP // CONNECT request, the proxy URI is resolved correctly, and the connection is // successfully established with the backend server through the proxy. -func (s) TestGRPCNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { +func (s) TestNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { + backendAddr := startBackendServer(t) + unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backendAddr)) + proxyStarted := false reqCheck := func(req *http.Request) { - if req.Method != http.MethodConnect { - t.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) - } + proxyStarted = true host, _, err := net.SplitHostPort(req.URL.Host) if err != nil { t.Error(err) @@ -299,8 +307,7 @@ func (s) TestGRPCNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { t.Errorf("isIPAddr(%q) = %t, want = %t", host, got, want) } } - pServer := tu.SetupProxy(t, reqCheck, false) - unresolvedTargetURI := createAndStartBackendServer(t) + pServer := transporttestutils.HTTPProxy(t, reqCheck, false) // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { @@ -330,6 +337,10 @@ func (s) TestGRPCNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { t.Fatalf("EmptyCall failed: %v", err) } + + if !proxyStarted { + t.Fatalf("Proxy not started") + } } // Tests the scenario where grpc.NewClient is used with grpc.WithNoProxy() set, @@ -337,12 +348,11 @@ func (s) TestGRPCNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { // dial the proxy but directly connects to the backend server. It also checks // that the proxy resolution function is not called and that the proxy server // never receives a connection request. -func (s) TestGRPCNewClientWithNoProxy(t *testing.T) { - unresolvedTargetURI := createAndStartBackendServer(t) - reqCheck := func(_ *http.Request) { - t.Error("proxy server should not have received a Connect request") - } - pServer := tu.SetupProxy(t, reqCheck, false) +func (s) TestNewClientWithNoProxy(t *testing.T) { + backendAddr := startBackendServer(t) + unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backendAddr)) + reqCheck := func(_ *http.Request) { t.Error("proxy server should not have received a Connect request") } + pServer := transporttestutils.HTTPProxy(t, reqCheck, false) // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { @@ -383,12 +393,11 @@ func (s) TestGRPCNewClientWithNoProxy(t *testing.T) { // custom dialer instead. It ensures that the proxy server is never dialed, the // proxy resolution function is not triggered, and the custom dialer is invoked // as expected. -func (s) TestGRPCNewClientWithContextDialer(t *testing.T) { - unresolvedTargetURI := createAndStartBackendServer(t) - reqCheck := func(_ *http.Request) { - t.Error("proxy server should not have received a Connect request") - } - pServer := tu.SetupProxy(t, reqCheck, false) +func (s) TestNewClientWithContextDialer(t *testing.T) { + backendAddr := startBackendServer(t) + unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backendAddr)) + reqCheck := func(_ *http.Request) { t.Error("proxy server should not have received a Connect request") } + pServer := transporttestutils.HTTPProxy(t, reqCheck, false) // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { @@ -435,22 +444,18 @@ func (s) TestGRPCNewClientWithContextDialer(t *testing.T) { // correct user information is included in the Proxy-Authorization header of // the CONNECT request. The test also ensures that target resolution does not // happen on the client. -func (s) TestBasicAuthInGrpcNewClientWithProxy(t *testing.T) { - unresolvedTargetURI := createAndStartBackendServer(t) +func (s) TestBasicAuthInNewClientWithProxy(t *testing.T) { + backendAddr := startBackendServer(t) + unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backendAddr)) const ( user = "notAUser" password = "notAPassword" ) + proxyStarted := false reqCheck := func(req *http.Request) { - if req.Method != http.MethodConnect { - t.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) - } - host, _, err := net.SplitHostPort(req.URL.Host) - if err != nil { - t.Error(err) - } - if got, want := isIPAddr(host), false; got != want { - t.Errorf("isIPAddr(%q) = %t, want = %t", host, got, want) + proxyStarted = true + if got, want := req.URL.Host, unresolvedTargetURI; got != want { + t.Errorf(" Unexpected request host: %s , want = %s ", got, want) } wantProxyAuthStr := "Basic " + base64.StdEncoding.EncodeToString([]byte(user+":"+password)) if got := req.Header.Get("Proxy-Authorization"); got != wantProxyAuthStr { @@ -462,7 +467,7 @@ func (s) TestBasicAuthInGrpcNewClientWithProxy(t *testing.T) { t.Errorf("unexpected auth %q (%q), want %q (%q)", got, gotDecoded, wantProxyAuthStr, wantDecoded) } } - pServer := tu.SetupProxy(t, reqCheck, false) + pServer := transporttestutils.HTTPProxy(t, reqCheck, false) // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { @@ -493,4 +498,8 @@ func (s) TestBasicAuthInGrpcNewClientWithProxy(t *testing.T) { if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { t.Fatalf("EmptyCall failed: %v", err) } + + if !proxyStarted { + t.Fatalf("Proxy not started") + } } diff --git a/internal/transport/proxy_test.go b/internal/transport/proxy_test.go index 4b76cbab0a45..a1b6db099d3c 100644 --- a/internal/transport/proxy_test.go +++ b/internal/transport/proxy_test.go @@ -30,13 +30,14 @@ import ( "time" "google.golang.org/grpc/internal/proxyattributes" - "google.golang.org/grpc/internal/transport/testutils" + "google.golang.org/grpc/internal/testutils" + transporttestutils "google.golang.org/grpc/internal/transport/testutils" "google.golang.org/grpc/resolver" ) func (s) TestHTTPConnectWithServerHello(t *testing.T) { serverMessage := []byte("server-hello") - blis, err := net.Listen("tcp", "localhost:0") + blis, err := testutils.LocalTCPListener() if err != nil { t.Fatalf("failed to listen: %v", err) } @@ -53,7 +54,7 @@ func (s) TestHTTPConnectWithServerHello(t *testing.T) { t.Error(err) } } - pServer := testutils.SetupProxy(t, reqCheck, true) + pServer := transporttestutils.HTTPProxy(t, reqCheck, true) msg := []byte{4, 3, 5, 2} recvBuf := make([]byte, len(msg)) diff --git a/internal/transport/testutils/testutils.go b/internal/transport/testutils/testutils.go index f35d88b3ad3c..318846627396 100644 --- a/internal/transport/testutils/testutils.go +++ b/internal/transport/testutils/testutils.go @@ -60,7 +60,9 @@ func (p *ProxyServer) handleRequest(t *testing.T, in net.Conn, waitForServerHell t.Errorf("failed to read CONNECT req: %v", err) return } - + if req.Method != http.MethodConnect { + t.Fatalf("unexpected Method %q, want %q", req.Method, http.MethodConnect) + } p.onRequest(req) t.Logf("Dialing to %s", req.URL.Host) @@ -98,11 +100,11 @@ func (p *ProxyServer) handleRequest(t *testing.T, in net.Conn, waitForServerHell go io.Copy(p.out, p.in) } -// SetupProxy initializes and starts a proxy server, registers a cleanup to +// HTTPProxy initializes and starts a proxy server, registers a cleanup to // stop it, and returns the proxy's listener and helper channels. -func SetupProxy(t *testing.T, reqCheck func(*http.Request), waitForServerHello bool) *ProxyServer { +func HTTPProxy(t *testing.T, reqCheck func(*http.Request), waitForServerHello bool) *ProxyServer { t.Helper() - pLis, err := net.Listen("tcp", "localhost:0") + pLis, err := testutils.LocalTCPListener() if err != nil { t.Fatalf("failed to listen: %v", err) } From 2df2f2e67ffcb2b3566ba466635a69fa8c57b741 Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Fri, 10 Jan 2025 11:56:20 +0530 Subject: [PATCH 40/53] new test for env variable --- internal/transport/proxy_ext_test.go | 47 ++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/internal/transport/proxy_ext_test.go b/internal/transport/proxy_ext_test.go index f101a4f6997e..b3ebb7815ec1 100644 --- a/internal/transport/proxy_ext_test.go +++ b/internal/transport/proxy_ext_test.go @@ -115,7 +115,7 @@ func (s) TestGRPCDialWithProxy(t *testing.T) { } if !proxyStarted { - t.Fatalf("Proxy not started") + t.Fatalf("Proxy not connected") } } @@ -172,7 +172,7 @@ func (s) TestGRPCDialWithDNSAndProxy(t *testing.T) { } if !proxyStarted { - t.Fatalf("Proxy not started") + t.Fatalf("Proxy not connected") } } @@ -222,7 +222,7 @@ func (s) TestNewClientWithProxy(t *testing.T) { t.Fatalf("EmptyCall failed: %v", err) } if !proxyStarted { - t.Fatalf("Proxy not started") + t.Fatalf("Proxy not connected") } } @@ -283,7 +283,7 @@ func (s) TestNewClientWithProxyAndCustomResolver(t *testing.T) { } if !proxyStarted { - t.Fatalf("Proxy not started") + t.Fatalf("Proxy not connected") } } @@ -339,7 +339,7 @@ func (s) TestNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { } if !proxyStarted { - t.Fatalf("Proxy not started") + t.Fatalf("Proxy not connected") } } @@ -500,6 +500,41 @@ func (s) TestBasicAuthInNewClientWithProxy(t *testing.T) { } if !proxyStarted { - t.Fatalf("Proxy not started") + t.Fatalf("Proxy not connected") + } +} + +// Tests the scenario where a gRPC client retrieves the proxy address from the +// environment variable `HTTPS_PROXY`. The test sets up an HTTP proxy server +// and verifies that the client connects to this proxy, sends the unresolved +// target URI in the HTTP CONNECT request. +func (s) TestHTTPProxyFromEnvironment(t *testing.T) { + unresolvedTargetURI := "example.test.com" + proxyStarted := false + reqCheck := func(req *http.Request) { + proxyStarted = true + if got, want := req.URL.Host, unresolvedTargetURI; got != want { + t.Errorf(" Unexpected request host: %s , want = %s ", got, want) + } + } + pServer := transporttestutils.HTTPProxy(t, reqCheck, false) + + // Overwrite the function in the test and restore them in defer. + t.Setenv("HTTPS_PROXY", pServer.Addr) + + ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) + defer cancel() + conn, err := grpc.NewClient(unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + t.Fatalf("grpc.NewClient(%s) failed: %v", unresolvedTargetURI, err) + } + defer conn.Close() + + // Send an empty RPC. + client := testgrpc.NewTestServiceClient(conn) + client.EmptyCall(ctx, &testgrpc.Empty{}) + + if !proxyStarted { + t.Fatalf("Proxy not connected") } } From 9e52674e325d88da07dc954999d18f3b1fb9f258 Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Fri, 10 Jan 2025 12:38:40 +0530 Subject: [PATCH 41/53] test --- internal/transport/proxy_ext_test.go | 70 ++++++++++++++-------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/internal/transport/proxy_ext_test.go b/internal/transport/proxy_ext_test.go index b3ebb7815ec1..466b8789b2ce 100644 --- a/internal/transport/proxy_ext_test.go +++ b/internal/transport/proxy_ext_test.go @@ -69,6 +69,41 @@ func isIPAddr(addr string) bool { return err == nil } +// Tests the scenario where a gRPC client retrieves the proxy address from the +// environment variable `HTTPS_PROXY`. The test sets up an HTTP proxy server +// and verifies that the client connects to this proxy, sends the unresolved +// target URI in the HTTP CONNECT request. +func (s) TestHTTPProxyFromEnvironment(t *testing.T) { + unresolvedTargetURI := "example.test.com" + proxyStarted := false + reqCheck := func(req *http.Request) { + proxyStarted = true + if got, want := req.URL.Host, unresolvedTargetURI; got != want { + t.Errorf(" Unexpected request host: %s , want = %s ", got, want) + } + } + pServer := transporttestutils.HTTPProxy(t, reqCheck, false) + + // Overwrite the function in the test and restore them in defer. + t.Setenv("HTTPS_PROXY", pServer.Addr) + + ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) + defer cancel() + conn, err := grpc.NewClient(unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + t.Fatalf("grpc.NewClient(%s) failed: %v", unresolvedTargetURI, err) + } + defer conn.Close() + + // Send an empty RPC. + client := testgrpc.NewTestServiceClient(conn) + client.EmptyCall(ctx, &testgrpc.Empty{}) + + if !proxyStarted { + t.Fatalf("Proxy not connected") + } +} + // Tests the scenario where grpc.Dial is performed using a proxy with the // default resolver in the target URI. The test verifies that the connection is // established to the proxy server, sends the unresolved target URI in the HTTP @@ -503,38 +538,3 @@ func (s) TestBasicAuthInNewClientWithProxy(t *testing.T) { t.Fatalf("Proxy not connected") } } - -// Tests the scenario where a gRPC client retrieves the proxy address from the -// environment variable `HTTPS_PROXY`. The test sets up an HTTP proxy server -// and verifies that the client connects to this proxy, sends the unresolved -// target URI in the HTTP CONNECT request. -func (s) TestHTTPProxyFromEnvironment(t *testing.T) { - unresolvedTargetURI := "example.test.com" - proxyStarted := false - reqCheck := func(req *http.Request) { - proxyStarted = true - if got, want := req.URL.Host, unresolvedTargetURI; got != want { - t.Errorf(" Unexpected request host: %s , want = %s ", got, want) - } - } - pServer := transporttestutils.HTTPProxy(t, reqCheck, false) - - // Overwrite the function in the test and restore them in defer. - t.Setenv("HTTPS_PROXY", pServer.Addr) - - ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) - defer cancel() - conn, err := grpc.NewClient(unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) - if err != nil { - t.Fatalf("grpc.NewClient(%s) failed: %v", unresolvedTargetURI, err) - } - defer conn.Close() - - // Send an empty RPC. - client := testgrpc.NewTestServiceClient(conn) - client.EmptyCall(ctx, &testgrpc.Empty{}) - - if !proxyStarted { - t.Fatalf("Proxy not connected") - } -} From 7d230bc222c921088ce5f95ccf6bb717d0f11d05 Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Fri, 10 Jan 2025 14:19:14 +0530 Subject: [PATCH 42/53] remove mocking and check enviornment --- internal/transport/proxy_ext_test.go | 149 +++++++++------------- internal/transport/testutils/testutils.go | 3 +- 2 files changed, 59 insertions(+), 93 deletions(-) diff --git a/internal/transport/proxy_ext_test.go b/internal/transport/proxy_ext_test.go index 466b8789b2ce..f040a80cd19e 100644 --- a/internal/transport/proxy_ext_test.go +++ b/internal/transport/proxy_ext_test.go @@ -29,6 +29,7 @@ import ( "testing" "time" + "golang.org/x/net/http/httpproxy" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/internal/grpctest" @@ -51,7 +52,7 @@ func Test(t *testing.T) { grpctest.RunSubTests(t, s{}) } -func startBackendServer(t *testing.T) string { +func startBackendServer(t *testing.T) *stubserver.StubServer { t.Helper() backend := &stubserver.StubServer{ EmptyCallF: func(context.Context, *testgrpc.Empty) (*testgrpc.Empty, error) { return &testgrpc.Empty{}, nil }, @@ -61,7 +62,7 @@ func startBackendServer(t *testing.T) string { } t.Logf("Started TestService backend at: %q", backend.Address) t.Cleanup(backend.Stop) - return backend.Address + return backend } func isIPAddr(addr string) bool { @@ -69,52 +70,21 @@ func isIPAddr(addr string) bool { return err == nil } -// Tests the scenario where a gRPC client retrieves the proxy address from the -// environment variable `HTTPS_PROXY`. The test sets up an HTTP proxy server -// and verifies that the client connects to this proxy, sends the unresolved -// target URI in the HTTP CONNECT request. -func (s) TestHTTPProxyFromEnvironment(t *testing.T) { - unresolvedTargetURI := "example.test.com" - proxyStarted := false - reqCheck := func(req *http.Request) { - proxyStarted = true - if got, want := req.URL.Host, unresolvedTargetURI; got != want { - t.Errorf(" Unexpected request host: %s , want = %s ", got, want) - } - } - pServer := transporttestutils.HTTPProxy(t, reqCheck, false) - - // Overwrite the function in the test and restore them in defer. - t.Setenv("HTTPS_PROXY", pServer.Addr) - - ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) - defer cancel() - conn, err := grpc.NewClient(unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) - if err != nil { - t.Fatalf("grpc.NewClient(%s) failed: %v", unresolvedTargetURI, err) - } - defer conn.Close() - - // Send an empty RPC. - client := testgrpc.NewTestServiceClient(conn) - client.EmptyCall(ctx, &testgrpc.Empty{}) - - if !proxyStarted { - t.Fatalf("Proxy not connected") - } -} - // Tests the scenario where grpc.Dial is performed using a proxy with the // default resolver in the target URI. The test verifies that the connection is // established to the proxy server, sends the unresolved target URI in the HTTP // CONNECT request and is successfully connected to the backend server. func (s) TestGRPCDialWithProxy(t *testing.T) { - backendAddr := startBackendServer(t) - unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backendAddr)) - proxyStarted := false + backend := startBackendServer(t) + unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backend.Address)) + proxyCalled := false reqCheck := func(req *http.Request) { - proxyStarted = true - if got, want := req.URL.Host, unresolvedTargetURI; got != want { + proxyCalled = true + host, _, err := net.SplitHostPort(req.URL.Host) + if err != nil { + t.Error(err) + } + if got, want := host, "localhost"; got != want { t.Errorf(" Unexpected request host: %s , want = %s ", got, want) } } @@ -149,7 +119,7 @@ func (s) TestGRPCDialWithProxy(t *testing.T) { t.Fatalf("EmptyCall failed: %v", err) } - if !proxyStarted { + if !proxyCalled { t.Fatalf("Proxy not connected") } } @@ -161,11 +131,11 @@ func (s) TestGRPCDialWithProxy(t *testing.T) { // established to the proxy server, with the resolved target URI sent in the // HTTP CONNECT request, successfully connecting to the backend server. func (s) TestGRPCDialWithDNSAndProxy(t *testing.T) { - backendAddr := startBackendServer(t) - unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backendAddr)) - proxyStarted := false + backend := startBackendServer(t) + unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backend.Address)) + proxyCalled := false reqCheck := func(req *http.Request) { - proxyStarted = true + proxyCalled = true host, _, err := net.SplitHostPort(req.URL.Host) if err != nil { @@ -206,7 +176,7 @@ func (s) TestGRPCDialWithDNSAndProxy(t *testing.T) { t.Fatalf("EmptyCall failed: %v", err) } - if !proxyStarted { + if !proxyCalled { t.Fatalf("Proxy not connected") } } @@ -217,12 +187,16 @@ func (s) TestGRPCDialWithDNSAndProxy(t *testing.T) { // unresolved target URI in the HTTP CONNECT request, and successfully // establishes a connection to the backend server. func (s) TestNewClientWithProxy(t *testing.T) { - backendAddr := startBackendServer(t) - unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backendAddr)) - proxyStarted := false + backend := startBackendServer(t) + unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backend.Address)) + proxyCalled := false reqCheck := func(req *http.Request) { - proxyStarted = true - if got, want := req.URL.Host, unresolvedTargetURI; got != want { + proxyCalled = true + host, _, err := net.SplitHostPort(req.URL.Host) + if err != nil { + t.Error(err) + } + if got, want := host, "localhost"; got != want { t.Errorf(" Unexpected request host: %s , want = %s ", got, want) } } @@ -256,7 +230,7 @@ func (s) TestNewClientWithProxy(t *testing.T) { if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { t.Fatalf("EmptyCall failed: %v", err) } - if !proxyStarted { + if !proxyCalled { t.Fatalf("Proxy not connected") } } @@ -267,11 +241,11 @@ func (s) TestNewClientWithProxy(t *testing.T) { // includes the resolved target URI in the HTTP CONNECT request, and // establishes a connection to the backend server. func (s) TestNewClientWithProxyAndCustomResolver(t *testing.T) { - backendAddr := startBackendServer(t) - unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backendAddr)) - proxyStarted := false + backend := startBackendServer(t) + unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backend.Address)) + proxyCalled := false reqCheck := func(req *http.Request) { - proxyStarted = true + proxyCalled = true host, _, err := net.SplitHostPort(req.URL.Host) if err != nil { t.Error(err) @@ -300,7 +274,7 @@ func (s) TestNewClientWithProxyAndCustomResolver(t *testing.T) { // Create and update a custom resolver for target URI. targetResolver := manual.NewBuilderWithScheme("test") resolver.Register(targetResolver) - targetResolver.InitialState(resolver.State{Endpoints: []resolver.Endpoint{{Addresses: []resolver.Address{{Addr: backendAddr}}}}}) + targetResolver.InitialState(resolver.State{Endpoints: []resolver.Endpoint{{Addresses: []resolver.Address{{Addr: backend.Address}}}}}) // Dial to the proxy server. ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) @@ -317,7 +291,7 @@ func (s) TestNewClientWithProxyAndCustomResolver(t *testing.T) { t.Fatalf("EmptyCall() failed: %v", err) } - if !proxyStarted { + if !proxyCalled { t.Fatalf("Proxy not connected") } } @@ -329,11 +303,11 @@ func (s) TestNewClientWithProxyAndCustomResolver(t *testing.T) { // CONNECT request, the proxy URI is resolved correctly, and the connection is // successfully established with the backend server through the proxy. func (s) TestNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { - backendAddr := startBackendServer(t) - unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backendAddr)) - proxyStarted := false + backend := startBackendServer(t) + unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backend.Address)) + proxyCalled := false reqCheck := func(req *http.Request) { - proxyStarted = true + proxyCalled = true host, _, err := net.SplitHostPort(req.URL.Host) if err != nil { t.Error(err) @@ -373,7 +347,7 @@ func (s) TestNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { t.Fatalf("EmptyCall failed: %v", err) } - if !proxyStarted { + if !proxyCalled { t.Fatalf("Proxy not connected") } } @@ -384,8 +358,8 @@ func (s) TestNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { // that the proxy resolution function is not called and that the proxy server // never receives a connection request. func (s) TestNewClientWithNoProxy(t *testing.T) { - backendAddr := startBackendServer(t) - unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backendAddr)) + backend := startBackendServer(t) + unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backend.Address)) reqCheck := func(_ *http.Request) { t.Error("proxy server should not have received a Connect request") } pServer := transporttestutils.HTTPProxy(t, reqCheck, false) @@ -429,8 +403,8 @@ func (s) TestNewClientWithNoProxy(t *testing.T) { // proxy resolution function is not triggered, and the custom dialer is invoked // as expected. func (s) TestNewClientWithContextDialer(t *testing.T) { - backendAddr := startBackendServer(t) - unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backendAddr)) + backend := startBackendServer(t) + unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backend.Address)) reqCheck := func(_ *http.Request) { t.Error("proxy server should not have received a Connect request") } pServer := transporttestutils.HTTPProxy(t, reqCheck, false) @@ -480,16 +454,15 @@ func (s) TestNewClientWithContextDialer(t *testing.T) { // the CONNECT request. The test also ensures that target resolution does not // happen on the client. func (s) TestBasicAuthInNewClientWithProxy(t *testing.T) { - backendAddr := startBackendServer(t) - unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backendAddr)) + unresolvedTargetURI := "example.test" const ( user = "notAUser" password = "notAPassword" ) - proxyStarted := false + proxyCalled := false reqCheck := func(req *http.Request) { - proxyStarted = true - if got, want := req.URL.Host, unresolvedTargetURI; got != want { + proxyCalled = true + if got, want := req.URL.Host, "example.test"; got != want { t.Errorf(" Unexpected request host: %s , want = %s ", got, want) } wantProxyAuthStr := "Basic " + base64.StdEncoding.EncodeToString([]byte(user+":"+password)) @@ -504,21 +477,15 @@ func (s) TestBasicAuthInNewClientWithProxy(t *testing.T) { } pServer := transporttestutils.HTTPProxy(t, reqCheck, false) - // Overwrite the function in the test and restore them in defer. - hpfe := func(req *http.Request) (*url.URL, error) { - if req.URL.Host == unresolvedTargetURI { - return &url.URL{ - Scheme: "https", - User: url.UserPassword(user, password), - Host: pServer.Addr, - }, nil - } - t.Errorf("Unexpected request host to proxy: %s want %s", req.URL.Host, unresolvedTargetURI) - return nil, nil + t.Setenv("HTTPS_PROXY", user+":"+password+"@"+pServer.Addr) + + origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment + delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { + return httpproxy.FromEnvironment().ProxyFunc()(req.URL) } - orighpfe := delegatingresolver.HTTPSProxyFromEnvironment - delegatingresolver.HTTPSProxyFromEnvironment = hpfe - defer func() { delegatingresolver.HTTPSProxyFromEnvironment = orighpfe }() + defer func() { + delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment + }() ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() @@ -530,11 +497,9 @@ func (s) TestBasicAuthInNewClientWithProxy(t *testing.T) { // Send an empty RPC to the backend through the proxy. client := testgrpc.NewTestServiceClient(conn) - if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil { - t.Fatalf("EmptyCall failed: %v", err) - } + client.EmptyCall(ctx, &testgrpc.Empty{}) - if !proxyStarted { + if !proxyCalled { t.Fatalf("Proxy not connected") } } diff --git a/internal/transport/testutils/testutils.go b/internal/transport/testutils/testutils.go index 318846627396..9efd161a797f 100644 --- a/internal/transport/testutils/testutils.go +++ b/internal/transport/testutils/testutils.go @@ -61,7 +61,7 @@ func (p *ProxyServer) handleRequest(t *testing.T, in net.Conn, waitForServerHell return } if req.Method != http.MethodConnect { - t.Fatalf("unexpected Method %q, want %q", req.Method, http.MethodConnect) + t.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) } p.onRequest(req) @@ -126,6 +126,7 @@ func HTTPProxy(t *testing.T, reqCheck func(*http.Request), waitForServerHello bo p.handleRequest(t, in, waitForServerHello) } }() + t.Logf("Started proxy at: %q", pLis.Addr()) t.Cleanup(p.stop) p.Addr = fmt.Sprintf("localhost:%d", testutils.ParsePort(t, pLis.Addr().String())) return p From 27cc05571c44cd4e6281d27b582b904b9fb57d69 Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Fri, 10 Jan 2025 16:10:09 +0530 Subject: [PATCH 43/53] comment for httpproxy --- internal/transport/proxy_ext_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/transport/proxy_ext_test.go b/internal/transport/proxy_ext_test.go index f040a80cd19e..db5211ca37a5 100644 --- a/internal/transport/proxy_ext_test.go +++ b/internal/transport/proxy_ext_test.go @@ -479,6 +479,11 @@ func (s) TestBasicAuthInNewClientWithProxy(t *testing.T) { t.Setenv("HTTPS_PROXY", user+":"+password+"@"+pServer.Addr) + // Use the httpproxy package functions instead of `http.ProxyFromEnvironment` + // because the latter reads proxy-related environment variables only once at + // initialization. This behavior causes issues when running test multiple + // times, as changes to environment variables during tests would be ignored. + // By using `httpproxy.FromEnvironment()`, we ensure proxy settings are read dynamically. origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) { return httpproxy.FromEnvironment().ProxyFunc()(req.URL) From 227c748162322c4aac15c8a6149ff17f1634f1bf Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Tue, 14 Jan 2025 16:08:06 +0530 Subject: [PATCH 44/53] address comments --- clientconn.go | 5 +- dialoptions.go | 25 ++-- internal/transport/http2_client.go | 7 +- internal/transport/proxy_ext_test.go | 22 ++-- internal/transport/proxy_test.go | 105 ++++++++++++++++- internal/transport/testutils/testutils.go | 133 ---------------------- resolver_wrapper.go | 4 +- 7 files changed, 134 insertions(+), 167 deletions(-) delete mode 100644 internal/transport/testutils/testutils.go diff --git a/clientconn.go b/clientconn.go index 0538a517f89b..030a7ef10318 100644 --- a/clientconn.go +++ b/clientconn.go @@ -226,11 +226,11 @@ func DialContext(ctx context.Context, target string, opts ...DialOption) (conn * // At the end of this method, we kick the channel out of idle, rather than // waiting for the first rpc. // - // WithTargetResolutionEnabled dial option in `grpc.Dial` ensures that it + // WithLocalDNSResolution dial option in `grpc.Dial` ensures that it // preserves behavior: when default scheme passthrough is used, skip // hostname resolution, when "dns" is used for resolution, perform // resolution on the client. - opts = append([]DialOption{withDefaultScheme("passthrough"), WithTargetResolutionEnabled()}, opts...) + opts = append([]DialOption{withDefaultScheme("passthrough"), WithLocalDNSResolution()}, opts...) cc, err := NewClient(target, opts...) if err != nil { return nil, err @@ -1327,7 +1327,6 @@ func (ac *addrConn) tryAllAddrs(ctx context.Context, addrs []resolver.Address, c if err == nil { return nil } - if firstConnErr == nil { firstConnErr = err } diff --git a/dialoptions.go b/dialoptions.go index 2eae24fa573b..e565365fee19 100644 --- a/dialoptions.go +++ b/dialoptions.go @@ -94,7 +94,7 @@ type dialOptions struct { idleTimeout time.Duration defaultScheme string maxCallAttempts int - targetResolutionEnabled bool // Specifies if target hostnames should be resolved when proxying is enabled. + enableLocalDNSResolution bool // Specifies if target hostnames should be resolved when proxying is enabled. useProxy bool // Specifies if a server should be connected via proxy. } @@ -383,17 +383,18 @@ func WithNoProxy() DialOption { }) } -// WithTargetResolutionEnabled returns a DialOption which enables target -// resolution on client when a proxy is used along with the the "dns" scheme. -// This is ignored if WithNoProxy is used. +// WithLocalDNSResolution forces local DNS name resolution even when a proxy is +// specified in the environment. By default, the server name is provided +// directly to the proxy as part of the CONNECT handshake. This is ignored if +// WithNoProxy is used. // // # Experimental // // Notice: This API is EXPERIMENTAL and may be changed or removed in a // later release. -func WithTargetResolutionEnabled() DialOption { +func WithLocalDNSResolution() DialOption { return newFuncDialOption(func(o *dialOptions) { - o.targetResolutionEnabled = true + o.enableLocalDNSResolution = true }) } @@ -686,12 +687,12 @@ func defaultDialOptions() dialOptions { UserAgent: grpcUA, BufferPool: mem.DefaultBufferPool(), }, - bs: internalbackoff.DefaultExponential, - idleTimeout: 30 * time.Minute, - defaultScheme: "dns", - maxCallAttempts: defaultMaxCallAttempts, - useProxy: true, - targetResolutionEnabled: false, + bs: internalbackoff.DefaultExponential, + idleTimeout: 30 * time.Minute, + defaultScheme: "dns", + maxCallAttempts: defaultMaxCallAttempts, + useProxy: true, + enableLocalDNSResolution: false, } } diff --git a/internal/transport/http2_client.go b/internal/transport/http2_client.go index c67b3e97ba53..513dbb93d550 100644 --- a/internal/transport/http2_client.go +++ b/internal/transport/http2_client.go @@ -156,10 +156,6 @@ type http2Client struct { func dial(ctx context.Context, fn func(context.Context, string) (net.Conn, error), addr resolver.Address, grpcUA string) (net.Conn, error) { address := addr.Addr - - if opts, present := proxyattributes.Get(addr); present { - return proxyDial(ctx, addr, grpcUA, opts) - } networkType, ok := networktype.Get(addr) if fn != nil { // Special handling for unix scheme with custom dialer. Back in the day, @@ -182,6 +178,9 @@ func dial(ctx context.Context, fn func(context.Context, string) (net.Conn, error if !ok { networkType, address = parseDialTarget(address) } + if opts, present := proxyattributes.Get(addr); present { + return proxyDial(ctx, addr, grpcUA, opts) + } return internal.NetDialerWithTCPKeepalive().DialContext(ctx, networkType, address) } diff --git a/internal/transport/proxy_ext_test.go b/internal/transport/proxy_ext_test.go index db5211ca37a5..b9daa44b33a7 100644 --- a/internal/transport/proxy_ext_test.go +++ b/internal/transport/proxy_ext_test.go @@ -36,7 +36,7 @@ import ( "google.golang.org/grpc/internal/resolver/delegatingresolver" "google.golang.org/grpc/internal/stubserver" "google.golang.org/grpc/internal/testutils" - transporttestutils "google.golang.org/grpc/internal/transport/testutils" + "google.golang.org/grpc/internal/transport" testgrpc "google.golang.org/grpc/interop/grpc_testing" "google.golang.org/grpc/resolver" "google.golang.org/grpc/resolver/manual" @@ -88,7 +88,7 @@ func (s) TestGRPCDialWithProxy(t *testing.T) { t.Errorf(" Unexpected request host: %s , want = %s ", got, want) } } - pServer := transporttestutils.HTTPProxy(t, reqCheck, false) + pServer := transport.HTTPProxy(t, reqCheck, false) // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { @@ -145,7 +145,7 @@ func (s) TestGRPCDialWithDNSAndProxy(t *testing.T) { t.Errorf("isIPAddr(%q) = %t, want = %t", host, got, want) } } - pServer := transporttestutils.HTTPProxy(t, reqCheck, false) + pServer := transport.HTTPProxy(t, reqCheck, false) // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { @@ -200,7 +200,7 @@ func (s) TestNewClientWithProxy(t *testing.T) { t.Errorf(" Unexpected request host: %s , want = %s ", got, want) } } - pServer := transporttestutils.HTTPProxy(t, reqCheck, false) + pServer := transport.HTTPProxy(t, reqCheck, false) // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { @@ -254,7 +254,7 @@ func (s) TestNewClientWithProxyAndCustomResolver(t *testing.T) { t.Errorf("isIPAddr(%q) = %t, want = %t", host, got, want) } } - pServer := transporttestutils.HTTPProxy(t, reqCheck, false) + pServer := transport.HTTPProxy(t, reqCheck, false) // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { @@ -297,7 +297,7 @@ func (s) TestNewClientWithProxyAndCustomResolver(t *testing.T) { } // Tests the scenario where grpc.NewClient is used with the default "dns" -// resolver and the dial option grpc.WithTargetResolutionEnabled() is set, +// resolver and the dial option grpc.WithLocalDNSResolution() is set, // enabling target resolution on the client. The test verifies that target // resolution happens on the client by sending resolved target URI in HTTP // CONNECT request, the proxy URI is resolved correctly, and the connection is @@ -316,7 +316,7 @@ func (s) TestNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { t.Errorf("isIPAddr(%q) = %t, want = %t", host, got, want) } } - pServer := transporttestutils.HTTPProxy(t, reqCheck, false) + pServer := transport.HTTPProxy(t, reqCheck, false) // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { @@ -335,7 +335,7 @@ func (s) TestNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() - conn, err := grpc.NewClient(unresolvedTargetURI, grpc.WithTargetResolutionEnabled(), grpc.WithTransportCredentials(insecure.NewCredentials())) + conn, err := grpc.NewClient(unresolvedTargetURI, grpc.WithLocalDNSResolution(), grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { t.Fatalf("grpc.NewClient(%s) failed: %v", unresolvedTargetURI, err) } @@ -361,7 +361,7 @@ func (s) TestNewClientWithNoProxy(t *testing.T) { backend := startBackendServer(t) unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backend.Address)) reqCheck := func(_ *http.Request) { t.Error("proxy server should not have received a Connect request") } - pServer := transporttestutils.HTTPProxy(t, reqCheck, false) + pServer := transport.HTTPProxy(t, reqCheck, false) // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { @@ -406,7 +406,7 @@ func (s) TestNewClientWithContextDialer(t *testing.T) { backend := startBackendServer(t) unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backend.Address)) reqCheck := func(_ *http.Request) { t.Error("proxy server should not have received a Connect request") } - pServer := transporttestutils.HTTPProxy(t, reqCheck, false) + pServer := transport.HTTPProxy(t, reqCheck, false) // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { @@ -475,7 +475,7 @@ func (s) TestBasicAuthInNewClientWithProxy(t *testing.T) { t.Errorf("unexpected auth %q (%q), want %q (%q)", got, gotDecoded, wantProxyAuthStr, wantDecoded) } } - pServer := transporttestutils.HTTPProxy(t, reqCheck, false) + pServer := transport.HTTPProxy(t, reqCheck, false) t.Setenv("HTTPS_PROXY", user+":"+password+"@"+pServer.Addr) diff --git a/internal/transport/proxy_test.go b/internal/transport/proxy_test.go index a1b6db099d3c..2cd52b3c7141 100644 --- a/internal/transport/proxy_test.go +++ b/internal/transport/proxy_test.go @@ -22,7 +22,11 @@ package transport import ( + "bufio" + "bytes" "context" + "fmt" + "io" "net" "net/http" "net/netip" @@ -31,10 +35,107 @@ import ( "google.golang.org/grpc/internal/proxyattributes" "google.golang.org/grpc/internal/testutils" - transporttestutils "google.golang.org/grpc/internal/transport/testutils" "google.golang.org/grpc/resolver" ) +// ProxyServer represents a test proxy server. +type ProxyServer struct { + lis net.Listener + in net.Conn // Connection from the client to the proxy. + out net.Conn // Connection from the proxy to the backend. + onRequest func(*http.Request) // Function to check the request sent to proxy. + Addr string // Address of the proxy +} + +// Stop closes the ProxyServer and its connections to client and server. +func (p *ProxyServer) stop() { + p.lis.Close() + if p.in != nil { + p.in.Close() + } + if p.out != nil { + p.out.Close() + } +} + +func (p *ProxyServer) handleRequest(t *testing.T, in net.Conn, waitForServerHello bool) { + req, err := http.ReadRequest(bufio.NewReader(in)) + if err != nil { + t.Errorf("failed to read CONNECT req: %v", err) + return + } + if req.Method != http.MethodConnect { + t.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) + } + p.onRequest(req) + + t.Logf("Dialing to %s", req.URL.Host) + out, err := net.Dial("tcp", req.URL.Host) + if err != nil { + in.Close() + t.Logf("failed to dial to server: %v", err) + return + } + out.SetDeadline(time.Now().Add(defaultTestTimeout)) + resp := http.Response{StatusCode: http.StatusOK, Proto: "HTTP/1.0"} + var buf bytes.Buffer + resp.Write(&buf) + + if waitForServerHello { + // Batch the first message from the server with the http connect + // response. This is done to test the cases in which the grpc client has + // the response to the connect request and proxied packets from the + // destination server when it reads the transport. + b := make([]byte, 50) + bytesRead, err := out.Read(b) + if err != nil { + t.Errorf("Got error while reading server hello: %v", err) + in.Close() + out.Close() + return + } + buf.Write(b[0:bytesRead]) + } + p.in = in + p.in.Write(buf.Bytes()) + p.out = out + + go io.Copy(p.in, p.out) + go io.Copy(p.out, p.in) +} + +// HTTPProxy initializes and starts a proxy server, registers a cleanup to +// stop it, and returns the proxy's listener and helper channels. +func HTTPProxy(t *testing.T, reqCheck func(*http.Request), waitForServerHello bool) *ProxyServer { + t.Helper() + pLis, err := testutils.LocalTCPListener() + if err != nil { + t.Fatalf("failed to listen: %v", err) + } + + p := &ProxyServer{ + lis: pLis, + onRequest: reqCheck, + } + + // Start the proxy server. + go func() { + for { + in, err := p.lis.Accept() + if err != nil { + return + } + // p.handleRequest is not invoked in a goroutine because the test + // proxy currently supports handling only one connection at a time. + p.handleRequest(t, in, waitForServerHello) + } + }() + t.Logf("Started proxy at: %q", pLis.Addr()) + t.Cleanup(p.stop) + p.Addr = fmt.Sprintf("localhost:%d", testutils.ParsePort(t, pLis.Addr().String())) + return p +} + func (s) TestHTTPConnectWithServerHello(t *testing.T) { serverMessage := []byte("server-hello") blis, err := testutils.LocalTCPListener() @@ -54,7 +155,7 @@ func (s) TestHTTPConnectWithServerHello(t *testing.T) { t.Error(err) } } - pServer := transporttestutils.HTTPProxy(t, reqCheck, true) + pServer := HTTPProxy(t, reqCheck, true) msg := []byte{4, 3, 5, 2} recvBuf := make([]byte, len(msg)) diff --git a/internal/transport/testutils/testutils.go b/internal/transport/testutils/testutils.go deleted file mode 100644 index 9efd161a797f..000000000000 --- a/internal/transport/testutils/testutils.go +++ /dev/null @@ -1,133 +0,0 @@ -/* - * - * 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 testutils contains helpers for transport tests. -package testutils - -import ( - "bufio" - "bytes" - "fmt" - "io" - "net" - "net/http" - "testing" - "time" - - "google.golang.org/grpc/internal/testutils" -) - -const defaultTestTimeout = 10 * time.Second - -// ProxyServer represents a test proxy server. -type ProxyServer struct { - lis net.Listener - in net.Conn // Connection from the client to the proxy. - out net.Conn // Connection from the proxy to the backend. - onRequest func(*http.Request) // Function to check the request sent to proxy. - Addr string // Address of the proxy -} - -// Stop closes the ProxyServer and its connections to client and server. -func (p *ProxyServer) stop() { - p.lis.Close() - if p.in != nil { - p.in.Close() - } - if p.out != nil { - p.out.Close() - } -} - -func (p *ProxyServer) handleRequest(t *testing.T, in net.Conn, waitForServerHello bool) { - req, err := http.ReadRequest(bufio.NewReader(in)) - if err != nil { - t.Errorf("failed to read CONNECT req: %v", err) - return - } - if req.Method != http.MethodConnect { - t.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) - } - p.onRequest(req) - - t.Logf("Dialing to %s", req.URL.Host) - out, err := net.Dial("tcp", req.URL.Host) - if err != nil { - in.Close() - t.Logf("failed to dial to server: %v", err) - return - } - out.SetDeadline(time.Now().Add(defaultTestTimeout)) - resp := http.Response{StatusCode: http.StatusOK, Proto: "HTTP/1.0"} - var buf bytes.Buffer - resp.Write(&buf) - - if waitForServerHello { - // Batch the first message from the server with the http connect - // response. This is done to test the cases in which the grpc client has - // the response to the connect request and proxied packets from the - // destination server when it reads the transport. - b := make([]byte, 50) - bytesRead, err := out.Read(b) - if err != nil { - t.Errorf("Got error while reading server hello: %v", err) - in.Close() - out.Close() - return - } - buf.Write(b[0:bytesRead]) - } - p.in = in - p.in.Write(buf.Bytes()) - p.out = out - - go io.Copy(p.in, p.out) - go io.Copy(p.out, p.in) -} - -// HTTPProxy initializes and starts a proxy server, registers a cleanup to -// stop it, and returns the proxy's listener and helper channels. -func HTTPProxy(t *testing.T, reqCheck func(*http.Request), waitForServerHello bool) *ProxyServer { - t.Helper() - pLis, err := testutils.LocalTCPListener() - if err != nil { - t.Fatalf("failed to listen: %v", err) - } - - p := &ProxyServer{ - lis: pLis, - onRequest: reqCheck, - } - - // Start the proxy server. - go func() { - for { - in, err := p.lis.Accept() - if err != nil { - return - } - // p.handleRequest is not invoked in a goroutine because the test - // proxy currently supports handling only one connection at a time. - p.handleRequest(t, in, waitForServerHello) - } - }() - t.Logf("Started proxy at: %q", pLis.Addr()) - t.Cleanup(p.stop) - p.Addr = fmt.Sprintf("localhost:%d", testutils.ParsePort(t, pLis.Addr().String())) - return p -} diff --git a/resolver_wrapper.go b/resolver_wrapper.go index 7420ff5a215c..9f5e12fc03c9 100644 --- a/resolver_wrapper.go +++ b/resolver_wrapper.go @@ -80,14 +80,14 @@ func (ccr *ccResolverWrapper) start() error { } var err error // The delegating resolver is used unless: - // - A custom dialer is provided via WithContextDialer dialoption. + // - A custom dialer is provided via WithContextDialer dialoption or // - Proxy usage is disabled through WithNoProxy dialoption. // In these cases, the resolver is built based on the scheme of target, // using the appropriate resolver builder. if ccr.cc.dopts.copts.Dialer != nil || !ccr.cc.dopts.useProxy { ccr.resolver, err = ccr.cc.resolverBuilder.Build(ccr.cc.parsedTarget, ccr, opts) } else { - ccr.resolver, err = delegatingresolver.New(ccr.cc.parsedTarget, ccr, opts, ccr.cc.resolverBuilder, ccr.cc.dopts.targetResolutionEnabled) + ccr.resolver, err = delegatingresolver.New(ccr.cc.parsedTarget, ccr, opts, ccr.cc.resolverBuilder, ccr.cc.dopts.enableLocalDNSResolution) } errCh <- err }) From bdbe5015bf5267fc04c21e4f34982ac61bff4ec7 Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Wed, 15 Jan 2025 11:28:10 +0530 Subject: [PATCH 45/53] remove helper --- internal/transport/proxy_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/transport/proxy_test.go b/internal/transport/proxy_test.go index 2cd52b3c7141..d2235c5e2804 100644 --- a/internal/transport/proxy_test.go +++ b/internal/transport/proxy_test.go @@ -107,7 +107,6 @@ func (p *ProxyServer) handleRequest(t *testing.T, in net.Conn, waitForServerHell // HTTPProxy initializes and starts a proxy server, registers a cleanup to // stop it, and returns the proxy's listener and helper channels. func HTTPProxy(t *testing.T, reqCheck func(*http.Request), waitForServerHello bool) *ProxyServer { - t.Helper() pLis, err := testutils.LocalTCPListener() if err != nil { t.Fatalf("failed to listen: %v", err) From 8a43a916af9cf3a37d78420f56c747aa088196df Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Wed, 15 Jan 2025 11:46:50 +0530 Subject: [PATCH 46/53] proxy helper function --- internal/transport/proxy_helper_test.go | 130 ++++++++++++++++++++++++ internal/transport/proxy_test.go | 101 ------------------ 2 files changed, 130 insertions(+), 101 deletions(-) create mode 100644 internal/transport/proxy_helper_test.go diff --git a/internal/transport/proxy_helper_test.go b/internal/transport/proxy_helper_test.go new file mode 100644 index 000000000000..cd3128abe419 --- /dev/null +++ b/internal/transport/proxy_helper_test.go @@ -0,0 +1,130 @@ +/* + * + * 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 transport + +import ( + "bufio" + "bytes" + "fmt" + "io" + "net" + "net/http" + "testing" + "time" + + "google.golang.org/grpc/internal/testutils" +) + +// ProxyServer represents a test proxy server. +type ProxyServer struct { + lis net.Listener + in net.Conn // Connection from the client to the proxy. + out net.Conn // Connection from the proxy to the backend. + onRequest func(*http.Request) // Function to check the request sent to proxy. + Addr string // Address of the proxy +} + +// Stop closes the ProxyServer and its connections to client and server. +func (p *ProxyServer) stop() { + p.lis.Close() + if p.in != nil { + p.in.Close() + } + if p.out != nil { + p.out.Close() + } +} + +func (p *ProxyServer) handleRequest(t *testing.T, in net.Conn, waitForServerHello bool) { + req, err := http.ReadRequest(bufio.NewReader(in)) + if err != nil { + t.Errorf("failed to read CONNECT req: %v", err) + return + } + if req.Method != http.MethodConnect { + t.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) + } + p.onRequest(req) + + t.Logf("Dialing to %s", req.URL.Host) + out, err := net.Dial("tcp", req.URL.Host) + if err != nil { + in.Close() + t.Logf("failed to dial to server: %v", err) + return + } + out.SetDeadline(time.Now().Add(defaultTestTimeout)) + resp := http.Response{StatusCode: http.StatusOK, Proto: "HTTP/1.0"} + var buf bytes.Buffer + resp.Write(&buf) + + if waitForServerHello { + // Batch the first message from the server with the http connect + // response. This is done to test the cases in which the grpc client has + // the response to the connect request and proxied packets from the + // destination server when it reads the transport. + b := make([]byte, 50) + bytesRead, err := out.Read(b) + if err != nil { + t.Errorf("Got error while reading server hello: %v", err) + in.Close() + out.Close() + return + } + buf.Write(b[0:bytesRead]) + } + p.in = in + p.in.Write(buf.Bytes()) + p.out = out + + go io.Copy(p.in, p.out) + go io.Copy(p.out, p.in) +} + +// HTTPProxy initializes and starts a proxy server, registers a cleanup to +// stop it, and returns the proxy's listener and helper channels. +func HTTPProxy(t *testing.T, reqCheck func(*http.Request), waitForServerHello bool) *ProxyServer { + t.Helper() + pLis, err := testutils.LocalTCPListener() + if err != nil { + t.Fatalf("failed to listen: %v", err) + } + + p := &ProxyServer{ + lis: pLis, + onRequest: reqCheck, + } + + // Start the proxy server. + go func() { + for { + in, err := p.lis.Accept() + if err != nil { + return + } + // p.handleRequest is not invoked in a goroutine because the test + // proxy currently supports handling only one connection at a time. + p.handleRequest(t, in, waitForServerHello) + } + }() + t.Logf("Started proxy at: %q", pLis.Addr()) + t.Cleanup(p.stop) + p.Addr = fmt.Sprintf("localhost:%d", testutils.ParsePort(t, pLis.Addr().String())) + return p +} diff --git a/internal/transport/proxy_test.go b/internal/transport/proxy_test.go index d2235c5e2804..400a9b3d375c 100644 --- a/internal/transport/proxy_test.go +++ b/internal/transport/proxy_test.go @@ -22,11 +22,7 @@ package transport import ( - "bufio" - "bytes" "context" - "fmt" - "io" "net" "net/http" "net/netip" @@ -38,103 +34,6 @@ import ( "google.golang.org/grpc/resolver" ) -// ProxyServer represents a test proxy server. -type ProxyServer struct { - lis net.Listener - in net.Conn // Connection from the client to the proxy. - out net.Conn // Connection from the proxy to the backend. - onRequest func(*http.Request) // Function to check the request sent to proxy. - Addr string // Address of the proxy -} - -// Stop closes the ProxyServer and its connections to client and server. -func (p *ProxyServer) stop() { - p.lis.Close() - if p.in != nil { - p.in.Close() - } - if p.out != nil { - p.out.Close() - } -} - -func (p *ProxyServer) handleRequest(t *testing.T, in net.Conn, waitForServerHello bool) { - req, err := http.ReadRequest(bufio.NewReader(in)) - if err != nil { - t.Errorf("failed to read CONNECT req: %v", err) - return - } - if req.Method != http.MethodConnect { - t.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) - } - p.onRequest(req) - - t.Logf("Dialing to %s", req.URL.Host) - out, err := net.Dial("tcp", req.URL.Host) - if err != nil { - in.Close() - t.Logf("failed to dial to server: %v", err) - return - } - out.SetDeadline(time.Now().Add(defaultTestTimeout)) - resp := http.Response{StatusCode: http.StatusOK, Proto: "HTTP/1.0"} - var buf bytes.Buffer - resp.Write(&buf) - - if waitForServerHello { - // Batch the first message from the server with the http connect - // response. This is done to test the cases in which the grpc client has - // the response to the connect request and proxied packets from the - // destination server when it reads the transport. - b := make([]byte, 50) - bytesRead, err := out.Read(b) - if err != nil { - t.Errorf("Got error while reading server hello: %v", err) - in.Close() - out.Close() - return - } - buf.Write(b[0:bytesRead]) - } - p.in = in - p.in.Write(buf.Bytes()) - p.out = out - - go io.Copy(p.in, p.out) - go io.Copy(p.out, p.in) -} - -// HTTPProxy initializes and starts a proxy server, registers a cleanup to -// stop it, and returns the proxy's listener and helper channels. -func HTTPProxy(t *testing.T, reqCheck func(*http.Request), waitForServerHello bool) *ProxyServer { - pLis, err := testutils.LocalTCPListener() - if err != nil { - t.Fatalf("failed to listen: %v", err) - } - - p := &ProxyServer{ - lis: pLis, - onRequest: reqCheck, - } - - // Start the proxy server. - go func() { - for { - in, err := p.lis.Accept() - if err != nil { - return - } - // p.handleRequest is not invoked in a goroutine because the test - // proxy currently supports handling only one connection at a time. - p.handleRequest(t, in, waitForServerHello) - } - }() - t.Logf("Started proxy at: %q", pLis.Addr()) - t.Cleanup(p.stop) - p.Addr = fmt.Sprintf("localhost:%d", testutils.ParsePort(t, pLis.Addr().String())) - return p -} - func (s) TestHTTPConnectWithServerHello(t *testing.T) { serverMessage := []byte("server-hello") blis, err := testutils.LocalTCPListener() From a73d511c3987dbdb4008638d611c6acb333c1fa5 Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Wed, 15 Jan 2025 11:51:01 +0530 Subject: [PATCH 47/53] proxy helper function --- internal/transport/proxy_helper_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/transport/proxy_helper_test.go b/internal/transport/proxy_helper_test.go index cd3128abe419..5fb52e3740ff 100644 --- a/internal/transport/proxy_helper_test.go +++ b/internal/transport/proxy_helper_test.go @@ -100,7 +100,6 @@ func (p *ProxyServer) handleRequest(t *testing.T, in net.Conn, waitForServerHell // HTTPProxy initializes and starts a proxy server, registers a cleanup to // stop it, and returns the proxy's listener and helper channels. func HTTPProxy(t *testing.T, reqCheck func(*http.Request), waitForServerHello bool) *ProxyServer { - t.Helper() pLis, err := testutils.LocalTCPListener() if err != nil { t.Fatalf("failed to listen: %v", err) From 0c154cf33268ff86b3a96e9030038ac47e882f42 Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Wed, 15 Jan 2025 11:51:31 +0530 Subject: [PATCH 48/53] proxy helper function --- internal/transport/proxy_helper_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/transport/proxy_helper_test.go b/internal/transport/proxy_helper_test.go index 5fb52e3740ff..bfea3cac117d 100644 --- a/internal/transport/proxy_helper_test.go +++ b/internal/transport/proxy_helper_test.go @@ -99,7 +99,7 @@ func (p *ProxyServer) handleRequest(t *testing.T, in net.Conn, waitForServerHell // HTTPProxy initializes and starts a proxy server, registers a cleanup to // stop it, and returns the proxy's listener and helper channels. -func HTTPProxy(t *testing.T, reqCheck func(*http.Request), waitForServerHello bool) *ProxyServer { +func WjhjwgbrhjProxy(t *testing.T, reqCheck func(*http.Request), waitForServerHello bool) *ProxyServer { pLis, err := testutils.LocalTCPListener() if err != nil { t.Fatalf("failed to listen: %v", err) From d396d314a7ed9c5d138b2de575d11a3cb89e920c Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Thu, 16 Jan 2025 12:34:27 +0530 Subject: [PATCH 49/53] change proxy_utils --- internal/testutils/proxy/proxy.go | 133 +++++++++++++++++++++++++++ internal/transport/proxy_ext_test.go | 18 ++-- internal/transport/proxy_test.go | 3 +- 3 files changed, 144 insertions(+), 10 deletions(-) create mode 100644 internal/testutils/proxy/proxy.go diff --git a/internal/testutils/proxy/proxy.go b/internal/testutils/proxy/proxy.go new file mode 100644 index 000000000000..001ba36afe02 --- /dev/null +++ b/internal/testutils/proxy/proxy.go @@ -0,0 +1,133 @@ +/* + * + * 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 proxy contains utilities for proxy server and HTTP CONNECT e2e tests. +package proxy + +import ( + "bufio" + "bytes" + "fmt" + "io" + "net" + "net/http" + "testing" + "time" + + "google.golang.org/grpc/internal/testutils" +) + +// ProxyServer represents a test proxy server. +type ProxyServer struct { + lis net.Listener + in net.Conn // Connection from the client to the proxy. + out net.Conn // Connection from the proxy to the backend. + onRequest func(*http.Request) // Function to check the request sent to proxy. + Addr string // Address of the proxy +} + +const defaultTestTimeout = 10 * time.Second + +// Stop closes the ProxyServer and its connections to client and server. +func (p *ProxyServer) stop() { + p.lis.Close() + if p.in != nil { + p.in.Close() + } + if p.out != nil { + p.out.Close() + } +} + +func (p *ProxyServer) handleRequest(t *testing.T, in net.Conn, waitForServerHello bool) { + req, err := http.ReadRequest(bufio.NewReader(in)) + if err != nil { + t.Errorf("failed to read CONNECT req: %v", err) + return + } + if req.Method != http.MethodConnect { + t.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) + } + p.onRequest(req) + + t.Logf("Dialing to %s", req.URL.Host) + out, err := net.Dial("tcp", req.URL.Host) + if err != nil { + in.Close() + t.Logf("failed to dial to server: %v", err) + return + } + out.SetDeadline(time.Now().Add(defaultTestTimeout)) + resp := http.Response{StatusCode: http.StatusOK, Proto: "HTTP/1.0"} + var buf bytes.Buffer + resp.Write(&buf) + + if waitForServerHello { + // Batch the first message from the server with the http connect + // response. This is done to test the cases in which the grpc client has + // the response to the connect request and proxied packets from the + // destination server when it reads the transport. + b := make([]byte, 50) + bytesRead, err := out.Read(b) + if err != nil { + t.Errorf("Got error while reading server hello: %v", err) + in.Close() + out.Close() + return + } + buf.Write(b[0:bytesRead]) + } + p.in = in + p.in.Write(buf.Bytes()) + p.out = out + + go io.Copy(p.in, p.out) + go io.Copy(p.out, p.in) +} + +// HTTPProxy initializes and starts a proxy server, registers a cleanup to +// stop it, and returns the proxy's listener and helper channels. +func HTTPProxy(t *testing.T, reqCheck func(*http.Request), waitForServerHello bool) *ProxyServer { + t.Helper() + pLis, err := testutils.LocalTCPListener() + if err != nil { + t.Fatalf("failed to listen: %v", err) + } + + p := &ProxyServer{ + lis: pLis, + onRequest: reqCheck, + } + + // Start the proxy server. + go func() { + for { + in, err := p.lis.Accept() + if err != nil { + return + } + // p.handleRequest is not invoked in a goroutine because the test + // proxy currently supports handling only one connection at a time. + p.handleRequest(t, in, waitForServerHello) + } + }() + t.Logf("Started proxy at: %q", pLis.Addr()) + t.Cleanup(p.stop) + p.Addr = fmt.Sprintf("localhost:%d", testutils.ParsePort(t, pLis.Addr().String())) + return p +} diff --git a/internal/transport/proxy_ext_test.go b/internal/transport/proxy_ext_test.go index b9daa44b33a7..165b653e9385 100644 --- a/internal/transport/proxy_ext_test.go +++ b/internal/transport/proxy_ext_test.go @@ -36,7 +36,7 @@ import ( "google.golang.org/grpc/internal/resolver/delegatingresolver" "google.golang.org/grpc/internal/stubserver" "google.golang.org/grpc/internal/testutils" - "google.golang.org/grpc/internal/transport" + "google.golang.org/grpc/internal/testutils/proxy" testgrpc "google.golang.org/grpc/interop/grpc_testing" "google.golang.org/grpc/resolver" "google.golang.org/grpc/resolver/manual" @@ -88,7 +88,7 @@ func (s) TestGRPCDialWithProxy(t *testing.T) { t.Errorf(" Unexpected request host: %s , want = %s ", got, want) } } - pServer := transport.HTTPProxy(t, reqCheck, false) + pServer := proxy.HTTPProxy(t, reqCheck, false) // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { @@ -145,7 +145,7 @@ func (s) TestGRPCDialWithDNSAndProxy(t *testing.T) { t.Errorf("isIPAddr(%q) = %t, want = %t", host, got, want) } } - pServer := transport.HTTPProxy(t, reqCheck, false) + pServer := proxy.HTTPProxy(t, reqCheck, false) // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { @@ -200,7 +200,7 @@ func (s) TestNewClientWithProxy(t *testing.T) { t.Errorf(" Unexpected request host: %s , want = %s ", got, want) } } - pServer := transport.HTTPProxy(t, reqCheck, false) + pServer := proxy.HTTPProxy(t, reqCheck, false) // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { @@ -254,7 +254,7 @@ func (s) TestNewClientWithProxyAndCustomResolver(t *testing.T) { t.Errorf("isIPAddr(%q) = %t, want = %t", host, got, want) } } - pServer := transport.HTTPProxy(t, reqCheck, false) + pServer := proxy.HTTPProxy(t, reqCheck, false) // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { @@ -316,7 +316,7 @@ func (s) TestNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { t.Errorf("isIPAddr(%q) = %t, want = %t", host, got, want) } } - pServer := transport.HTTPProxy(t, reqCheck, false) + pServer := proxy.HTTPProxy(t, reqCheck, false) // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { @@ -361,7 +361,7 @@ func (s) TestNewClientWithNoProxy(t *testing.T) { backend := startBackendServer(t) unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backend.Address)) reqCheck := func(_ *http.Request) { t.Error("proxy server should not have received a Connect request") } - pServer := transport.HTTPProxy(t, reqCheck, false) + pServer := proxy.HTTPProxy(t, reqCheck, false) // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { @@ -406,7 +406,7 @@ func (s) TestNewClientWithContextDialer(t *testing.T) { backend := startBackendServer(t) unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backend.Address)) reqCheck := func(_ *http.Request) { t.Error("proxy server should not have received a Connect request") } - pServer := transport.HTTPProxy(t, reqCheck, false) + pServer := proxy.HTTPProxy(t, reqCheck, false) // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { @@ -475,7 +475,7 @@ func (s) TestBasicAuthInNewClientWithProxy(t *testing.T) { t.Errorf("unexpected auth %q (%q), want %q (%q)", got, gotDecoded, wantProxyAuthStr, wantDecoded) } } - pServer := transport.HTTPProxy(t, reqCheck, false) + pServer := proxy.HTTPProxy(t, reqCheck, false) t.Setenv("HTTPS_PROXY", user+":"+password+"@"+pServer.Addr) diff --git a/internal/transport/proxy_test.go b/internal/transport/proxy_test.go index 400a9b3d375c..514c49a51017 100644 --- a/internal/transport/proxy_test.go +++ b/internal/transport/proxy_test.go @@ -31,6 +31,7 @@ import ( "google.golang.org/grpc/internal/proxyattributes" "google.golang.org/grpc/internal/testutils" + "google.golang.org/grpc/internal/testutils/proxy" "google.golang.org/grpc/resolver" ) @@ -53,7 +54,7 @@ func (s) TestHTTPConnectWithServerHello(t *testing.T) { t.Error(err) } } - pServer := HTTPProxy(t, reqCheck, true) + pServer := proxy.HTTPProxy(t, reqCheck, true) msg := []byte{4, 3, 5, 2} recvBuf := make([]byte, len(msg)) From fd088d169108bc1eadd24d2c1d9bd01f9b9a6e37 Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Thu, 16 Jan 2025 12:34:54 +0530 Subject: [PATCH 50/53] change proxy_utils --- internal/transport/proxy_helper_test.go | 129 ------------------------ 1 file changed, 129 deletions(-) delete mode 100644 internal/transport/proxy_helper_test.go diff --git a/internal/transport/proxy_helper_test.go b/internal/transport/proxy_helper_test.go deleted file mode 100644 index bfea3cac117d..000000000000 --- a/internal/transport/proxy_helper_test.go +++ /dev/null @@ -1,129 +0,0 @@ -/* - * - * 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 transport - -import ( - "bufio" - "bytes" - "fmt" - "io" - "net" - "net/http" - "testing" - "time" - - "google.golang.org/grpc/internal/testutils" -) - -// ProxyServer represents a test proxy server. -type ProxyServer struct { - lis net.Listener - in net.Conn // Connection from the client to the proxy. - out net.Conn // Connection from the proxy to the backend. - onRequest func(*http.Request) // Function to check the request sent to proxy. - Addr string // Address of the proxy -} - -// Stop closes the ProxyServer and its connections to client and server. -func (p *ProxyServer) stop() { - p.lis.Close() - if p.in != nil { - p.in.Close() - } - if p.out != nil { - p.out.Close() - } -} - -func (p *ProxyServer) handleRequest(t *testing.T, in net.Conn, waitForServerHello bool) { - req, err := http.ReadRequest(bufio.NewReader(in)) - if err != nil { - t.Errorf("failed to read CONNECT req: %v", err) - return - } - if req.Method != http.MethodConnect { - t.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect) - } - p.onRequest(req) - - t.Logf("Dialing to %s", req.URL.Host) - out, err := net.Dial("tcp", req.URL.Host) - if err != nil { - in.Close() - t.Logf("failed to dial to server: %v", err) - return - } - out.SetDeadline(time.Now().Add(defaultTestTimeout)) - resp := http.Response{StatusCode: http.StatusOK, Proto: "HTTP/1.0"} - var buf bytes.Buffer - resp.Write(&buf) - - if waitForServerHello { - // Batch the first message from the server with the http connect - // response. This is done to test the cases in which the grpc client has - // the response to the connect request and proxied packets from the - // destination server when it reads the transport. - b := make([]byte, 50) - bytesRead, err := out.Read(b) - if err != nil { - t.Errorf("Got error while reading server hello: %v", err) - in.Close() - out.Close() - return - } - buf.Write(b[0:bytesRead]) - } - p.in = in - p.in.Write(buf.Bytes()) - p.out = out - - go io.Copy(p.in, p.out) - go io.Copy(p.out, p.in) -} - -// HTTPProxy initializes and starts a proxy server, registers a cleanup to -// stop it, and returns the proxy's listener and helper channels. -func WjhjwgbrhjProxy(t *testing.T, reqCheck func(*http.Request), waitForServerHello bool) *ProxyServer { - pLis, err := testutils.LocalTCPListener() - if err != nil { - t.Fatalf("failed to listen: %v", err) - } - - p := &ProxyServer{ - lis: pLis, - onRequest: reqCheck, - } - - // Start the proxy server. - go func() { - for { - in, err := p.lis.Accept() - if err != nil { - return - } - // p.handleRequest is not invoked in a goroutine because the test - // proxy currently supports handling only one connection at a time. - p.handleRequest(t, in, waitForServerHello) - } - }() - t.Logf("Started proxy at: %q", pLis.Addr()) - t.Cleanup(p.stop) - p.Addr = fmt.Sprintf("localhost:%d", testutils.ParsePort(t, pLis.Addr().String())) - return p -} From 32fa83c411351e459891760dabb7c06222518a3e Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Thu, 16 Jan 2025 12:50:01 +0530 Subject: [PATCH 51/53] change proxy_utils --- internal/testutils/{proxy => }/proxy.go | 11 ++++------- internal/transport/proxy_ext_test.go | 17 ++++++++--------- internal/transport/proxy_test.go | 3 +-- 3 files changed, 13 insertions(+), 18 deletions(-) rename internal/testutils/{proxy => }/proxy.go (91%) diff --git a/internal/testutils/proxy/proxy.go b/internal/testutils/proxy.go similarity index 91% rename from internal/testutils/proxy/proxy.go rename to internal/testutils/proxy.go index 001ba36afe02..391bcd20c74c 100644 --- a/internal/testutils/proxy/proxy.go +++ b/internal/testutils/proxy.go @@ -1,6 +1,6 @@ /* * - * Copyright 2024 gRPC authors. + * Copyright 2023 gRPC authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,7 @@ * */ -// Package proxy contains utilities for proxy server and HTTP CONNECT e2e tests. -package proxy +package testutils import ( "bufio" @@ -28,8 +27,6 @@ import ( "net/http" "testing" "time" - - "google.golang.org/grpc/internal/testutils" ) // ProxyServer represents a test proxy server. @@ -104,7 +101,7 @@ func (p *ProxyServer) handleRequest(t *testing.T, in net.Conn, waitForServerHell // stop it, and returns the proxy's listener and helper channels. func HTTPProxy(t *testing.T, reqCheck func(*http.Request), waitForServerHello bool) *ProxyServer { t.Helper() - pLis, err := testutils.LocalTCPListener() + pLis, err := LocalTCPListener() if err != nil { t.Fatalf("failed to listen: %v", err) } @@ -128,6 +125,6 @@ func HTTPProxy(t *testing.T, reqCheck func(*http.Request), waitForServerHello bo }() t.Logf("Started proxy at: %q", pLis.Addr()) t.Cleanup(p.stop) - p.Addr = fmt.Sprintf("localhost:%d", testutils.ParsePort(t, pLis.Addr().String())) + p.Addr = fmt.Sprintf("localhost:%d", ParsePort(t, pLis.Addr().String())) return p } diff --git a/internal/transport/proxy_ext_test.go b/internal/transport/proxy_ext_test.go index 165b653e9385..44c03c6ec91f 100644 --- a/internal/transport/proxy_ext_test.go +++ b/internal/transport/proxy_ext_test.go @@ -36,7 +36,6 @@ import ( "google.golang.org/grpc/internal/resolver/delegatingresolver" "google.golang.org/grpc/internal/stubserver" "google.golang.org/grpc/internal/testutils" - "google.golang.org/grpc/internal/testutils/proxy" testgrpc "google.golang.org/grpc/interop/grpc_testing" "google.golang.org/grpc/resolver" "google.golang.org/grpc/resolver/manual" @@ -88,7 +87,7 @@ func (s) TestGRPCDialWithProxy(t *testing.T) { t.Errorf(" Unexpected request host: %s , want = %s ", got, want) } } - pServer := proxy.HTTPProxy(t, reqCheck, false) + pServer := testutils.HTTPProxy(t, reqCheck, false) // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { @@ -145,7 +144,7 @@ func (s) TestGRPCDialWithDNSAndProxy(t *testing.T) { t.Errorf("isIPAddr(%q) = %t, want = %t", host, got, want) } } - pServer := proxy.HTTPProxy(t, reqCheck, false) + pServer := testutils.HTTPProxy(t, reqCheck, false) // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { @@ -200,7 +199,7 @@ func (s) TestNewClientWithProxy(t *testing.T) { t.Errorf(" Unexpected request host: %s , want = %s ", got, want) } } - pServer := proxy.HTTPProxy(t, reqCheck, false) + pServer := testutils.HTTPProxy(t, reqCheck, false) // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { @@ -254,7 +253,7 @@ func (s) TestNewClientWithProxyAndCustomResolver(t *testing.T) { t.Errorf("isIPAddr(%q) = %t, want = %t", host, got, want) } } - pServer := proxy.HTTPProxy(t, reqCheck, false) + pServer := testutils.HTTPProxy(t, reqCheck, false) // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { @@ -316,7 +315,7 @@ func (s) TestNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { t.Errorf("isIPAddr(%q) = %t, want = %t", host, got, want) } } - pServer := proxy.HTTPProxy(t, reqCheck, false) + pServer := testutils.HTTPProxy(t, reqCheck, false) // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { @@ -361,7 +360,7 @@ func (s) TestNewClientWithNoProxy(t *testing.T) { backend := startBackendServer(t) unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backend.Address)) reqCheck := func(_ *http.Request) { t.Error("proxy server should not have received a Connect request") } - pServer := proxy.HTTPProxy(t, reqCheck, false) + pServer := testutils.HTTPProxy(t, reqCheck, false) // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { @@ -406,7 +405,7 @@ func (s) TestNewClientWithContextDialer(t *testing.T) { backend := startBackendServer(t) unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backend.Address)) reqCheck := func(_ *http.Request) { t.Error("proxy server should not have received a Connect request") } - pServer := proxy.HTTPProxy(t, reqCheck, false) + pServer := testutils.HTTPProxy(t, reqCheck, false) // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { @@ -475,7 +474,7 @@ func (s) TestBasicAuthInNewClientWithProxy(t *testing.T) { t.Errorf("unexpected auth %q (%q), want %q (%q)", got, gotDecoded, wantProxyAuthStr, wantDecoded) } } - pServer := proxy.HTTPProxy(t, reqCheck, false) + pServer := testutils.HTTPProxy(t, reqCheck, false) t.Setenv("HTTPS_PROXY", user+":"+password+"@"+pServer.Addr) diff --git a/internal/transport/proxy_test.go b/internal/transport/proxy_test.go index 514c49a51017..39c79ab52bb6 100644 --- a/internal/transport/proxy_test.go +++ b/internal/transport/proxy_test.go @@ -31,7 +31,6 @@ import ( "google.golang.org/grpc/internal/proxyattributes" "google.golang.org/grpc/internal/testutils" - "google.golang.org/grpc/internal/testutils/proxy" "google.golang.org/grpc/resolver" ) @@ -54,7 +53,7 @@ func (s) TestHTTPConnectWithServerHello(t *testing.T) { t.Error(err) } } - pServer := proxy.HTTPProxy(t, reqCheck, true) + pServer := testutils.HTTPProxy(t, reqCheck, true) msg := []byte{4, 3, 5, 2} recvBuf := make([]byte, len(msg)) From 382bcb18eec62ce0cea4699bc967c2eb69888ae8 Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Fri, 17 Jan 2025 03:30:44 +0530 Subject: [PATCH 52/53] addr comments --- .../{proxy.go => proxyserver/proxyserver.go} | 16 ++++++++++------ internal/transport/proxy_ext_test.go | 17 +++++++++-------- internal/transport/proxy_test.go | 3 ++- 3 files changed, 21 insertions(+), 15 deletions(-) rename internal/testutils/{proxy.go => proxyserver/proxyserver.go} (87%) diff --git a/internal/testutils/proxy.go b/internal/testutils/proxyserver/proxyserver.go similarity index 87% rename from internal/testutils/proxy.go rename to internal/testutils/proxyserver/proxyserver.go index 391bcd20c74c..8b1752bbe8e9 100644 --- a/internal/testutils/proxy.go +++ b/internal/testutils/proxyserver/proxyserver.go @@ -1,6 +1,6 @@ /* * - * Copyright 2023 gRPC authors. + * 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. @@ -16,7 +16,9 @@ * */ -package testutils +// Package proxyserver provides a implementation of proxy server for tests. +// The proxy server only supports one connection at a time. +package proxyserver import ( "bufio" @@ -27,6 +29,8 @@ import ( "net/http" "testing" "time" + + "google.golang.org/grpc/internal/testutils" ) // ProxyServer represents a test proxy server. @@ -98,10 +102,10 @@ func (p *ProxyServer) handleRequest(t *testing.T, in net.Conn, waitForServerHell } // HTTPProxy initializes and starts a proxy server, registers a cleanup to -// stop it, and returns the proxy's listener and helper channels. +// stop it, and returns a ProxyServer. func HTTPProxy(t *testing.T, reqCheck func(*http.Request), waitForServerHello bool) *ProxyServer { t.Helper() - pLis, err := LocalTCPListener() + pLis, err := testutils.LocalTCPListener() if err != nil { t.Fatalf("failed to listen: %v", err) } @@ -109,6 +113,7 @@ func HTTPProxy(t *testing.T, reqCheck func(*http.Request), waitForServerHello bo p := &ProxyServer{ lis: pLis, onRequest: reqCheck, + Addr: fmt.Sprintf("localhost:%d", testutils.ParsePort(t, pLis.Addr().String())), } // Start the proxy server. @@ -123,8 +128,7 @@ func HTTPProxy(t *testing.T, reqCheck func(*http.Request), waitForServerHello bo p.handleRequest(t, in, waitForServerHello) } }() - t.Logf("Started proxy at: %q", pLis.Addr()) + t.Logf("Started proxy at: %q", pLis.Addr().String()) t.Cleanup(p.stop) - p.Addr = fmt.Sprintf("localhost:%d", ParsePort(t, pLis.Addr().String())) return p } diff --git a/internal/transport/proxy_ext_test.go b/internal/transport/proxy_ext_test.go index 44c03c6ec91f..deea6d47fc3a 100644 --- a/internal/transport/proxy_ext_test.go +++ b/internal/transport/proxy_ext_test.go @@ -36,6 +36,7 @@ import ( "google.golang.org/grpc/internal/resolver/delegatingresolver" "google.golang.org/grpc/internal/stubserver" "google.golang.org/grpc/internal/testutils" + "google.golang.org/grpc/internal/testutils/proxyserver" testgrpc "google.golang.org/grpc/interop/grpc_testing" "google.golang.org/grpc/resolver" "google.golang.org/grpc/resolver/manual" @@ -87,7 +88,7 @@ func (s) TestGRPCDialWithProxy(t *testing.T) { t.Errorf(" Unexpected request host: %s , want = %s ", got, want) } } - pServer := testutils.HTTPProxy(t, reqCheck, false) + pServer := proxyserver.HTTPProxy(t, reqCheck, false) // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { @@ -144,7 +145,7 @@ func (s) TestGRPCDialWithDNSAndProxy(t *testing.T) { t.Errorf("isIPAddr(%q) = %t, want = %t", host, got, want) } } - pServer := testutils.HTTPProxy(t, reqCheck, false) + pServer := proxyserver.HTTPProxy(t, reqCheck, false) // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { @@ -199,7 +200,7 @@ func (s) TestNewClientWithProxy(t *testing.T) { t.Errorf(" Unexpected request host: %s , want = %s ", got, want) } } - pServer := testutils.HTTPProxy(t, reqCheck, false) + pServer := proxyserver.HTTPProxy(t, reqCheck, false) // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { @@ -253,7 +254,7 @@ func (s) TestNewClientWithProxyAndCustomResolver(t *testing.T) { t.Errorf("isIPAddr(%q) = %t, want = %t", host, got, want) } } - pServer := testutils.HTTPProxy(t, reqCheck, false) + pServer := proxyserver.HTTPProxy(t, reqCheck, false) // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { @@ -315,7 +316,7 @@ func (s) TestNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) { t.Errorf("isIPAddr(%q) = %t, want = %t", host, got, want) } } - pServer := testutils.HTTPProxy(t, reqCheck, false) + pServer := proxyserver.HTTPProxy(t, reqCheck, false) // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { @@ -360,7 +361,7 @@ func (s) TestNewClientWithNoProxy(t *testing.T) { backend := startBackendServer(t) unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backend.Address)) reqCheck := func(_ *http.Request) { t.Error("proxy server should not have received a Connect request") } - pServer := testutils.HTTPProxy(t, reqCheck, false) + pServer := proxyserver.HTTPProxy(t, reqCheck, false) // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { @@ -405,7 +406,7 @@ func (s) TestNewClientWithContextDialer(t *testing.T) { backend := startBackendServer(t) unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backend.Address)) reqCheck := func(_ *http.Request) { t.Error("proxy server should not have received a Connect request") } - pServer := testutils.HTTPProxy(t, reqCheck, false) + pServer := proxyserver.HTTPProxy(t, reqCheck, false) // Overwrite the function in the test and restore them in defer. hpfe := func(req *http.Request) (*url.URL, error) { @@ -474,7 +475,7 @@ func (s) TestBasicAuthInNewClientWithProxy(t *testing.T) { t.Errorf("unexpected auth %q (%q), want %q (%q)", got, gotDecoded, wantProxyAuthStr, wantDecoded) } } - pServer := testutils.HTTPProxy(t, reqCheck, false) + pServer := proxyserver.HTTPProxy(t, reqCheck, false) t.Setenv("HTTPS_PROXY", user+":"+password+"@"+pServer.Addr) diff --git a/internal/transport/proxy_test.go b/internal/transport/proxy_test.go index 39c79ab52bb6..8d7cddc49fd7 100644 --- a/internal/transport/proxy_test.go +++ b/internal/transport/proxy_test.go @@ -31,6 +31,7 @@ import ( "google.golang.org/grpc/internal/proxyattributes" "google.golang.org/grpc/internal/testutils" + "google.golang.org/grpc/internal/testutils/proxyserver" "google.golang.org/grpc/resolver" ) @@ -53,7 +54,7 @@ func (s) TestHTTPConnectWithServerHello(t *testing.T) { t.Error(err) } } - pServer := testutils.HTTPProxy(t, reqCheck, true) + pServer := proxyserver.HTTPProxy(t, reqCheck, true) msg := []byte{4, 3, 5, 2} recvBuf := make([]byte, len(msg)) From 69037b04c7d9b017c57e0db2a45eece0ffa7a324 Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Fri, 17 Jan 2025 14:13:56 +0530 Subject: [PATCH 53/53] add userSet option --- internal/proxyattributes/proxyattributes.go | 1 + internal/proxyattributes/proxyattributes_test.go | 14 ++++++++++++-- .../delegatingresolver/delegatingresolver.go | 4 ++++ internal/testutils/proxyserver/proxyserver.go | 5 +++-- internal/transport/proxy.go | 3 ++- 5 files changed, 22 insertions(+), 5 deletions(-) diff --git a/internal/proxyattributes/proxyattributes.go b/internal/proxyattributes/proxyattributes.go index d25d33efd373..67f1abfc2c4d 100644 --- a/internal/proxyattributes/proxyattributes.go +++ b/internal/proxyattributes/proxyattributes.go @@ -34,6 +34,7 @@ const proxyOptionsKey = keyType("grpc.resolver.delegatingresolver.proxyOptions") // handshake. type Options struct { User url.Userinfo + UserSet bool ConnectAddr string } diff --git a/internal/proxyattributes/proxyattributes_test.go b/internal/proxyattributes/proxyattributes_test.go index 225b2919d5d9..62cd1158f4f8 100644 --- a/internal/proxyattributes/proxyattributes_test.go +++ b/internal/proxyattributes/proxyattributes_test.go @@ -43,6 +43,7 @@ func (s) TestGet(t *testing.T) { addr resolver.Address wantConnectAddr string wantUser url.Userinfo + wantUserSet bool wantAttrPresent bool }{ { @@ -61,10 +62,12 @@ func (s) TestGet(t *testing.T) { addr: resolver.Address{ Addr: "test-address", Attributes: attributes.New(proxyOptionsKey, Options{ - User: *user, + User: *user, + UserSet: true, }), }, wantUser: *user, + wantUserSet: true, wantAttrPresent: true, }, { @@ -88,6 +91,9 @@ func (s) TestGet(t *testing.T) { if gotOption.User != tt.wantUser { t.Errorf("User(%v) = %v, want %v", tt.addr, gotOption.User, tt.wantUser) } + if gotOption.UserSet != tt.wantUserSet { + t.Errorf("UserSet(%v) = %v, want %v", tt.addr, gotOption.UserSet, tt.wantUserSet) + } }) } } @@ -98,6 +104,7 @@ func (s) TestSet(t *testing.T) { addr := resolver.Address{Addr: "test-address"} pOpts := Options{ User: *url.UserPassword("username", "password"), + UserSet: true, ConnectAddr: "proxy-address", } @@ -108,9 +115,12 @@ func (s) TestSet(t *testing.T) { t.Errorf("Get(%v) = %v, want %v ", populatedAddr, attrPresent, true) } if got, want := gotOption.ConnectAddr, pOpts.ConnectAddr; got != want { - t.Errorf("Unexpected ConnectAddr proxy atrribute = %v, want %v", got, want) + t.Errorf("unexpected ConnectAddr proxy atrribute = %v, want %v", got, want) } if got, want := gotOption.User, pOpts.User; got != want { t.Errorf("unexpected User proxy attribute = %v, want %v", got, want) } + if got, want := gotOption.UserSet, pOpts.UserSet; got != want { + t.Errorf("unexpected UserSet proxy attribute = %v, want %v", got, want) + } } diff --git a/internal/resolver/delegatingresolver/delegatingresolver.go b/internal/resolver/delegatingresolver/delegatingresolver.go index 6050e3d055bb..b9d5d1c5cc3d 100644 --- a/internal/resolver/delegatingresolver/delegatingresolver.go +++ b/internal/resolver/delegatingresolver/delegatingresolver.go @@ -206,12 +206,15 @@ func (r *delegatingResolver) updateClientConnStateLocked() error { } var addresses []resolver.Address var user url.Userinfo + var userSet bool if r.proxyURL.User != nil { user = *r.proxyURL.User + userSet = true } for _, targetAddr := range (*r.targetResolverState).Addresses { addresses = append(addresses, proxyattributes.Set(proxyAddr, proxyattributes.Options{ User: user, + UserSet: userSet, ConnectAddr: targetAddr.Addr, })) } @@ -230,6 +233,7 @@ func (r *delegatingResolver) updateClientConnStateLocked() error { for _, targetAddr := range endpt.Addresses { addrs = append(addrs, proxyattributes.Set(proxyAddr, proxyattributes.Options{ User: user, + UserSet: userSet, ConnectAddr: targetAddr.Addr, })) } diff --git a/internal/testutils/proxyserver/proxyserver.go b/internal/testutils/proxyserver/proxyserver.go index 8b1752bbe8e9..93d4163536ef 100644 --- a/internal/testutils/proxyserver/proxyserver.go +++ b/internal/testutils/proxyserver/proxyserver.go @@ -16,8 +16,9 @@ * */ -// Package proxyserver provides a implementation of proxy server for tests. -// The proxy server only supports one connection at a time. +// Package proxyserver provides an implementation of a proxy server for testing purposes. +// The server supports only a single incoming connection at a time and is not concurrent. +// It handles only HTTP CONNECT requests; other HTTP methods are not supported. package proxyserver import ( diff --git a/internal/transport/proxy.go b/internal/transport/proxy.go index 4fb3da4ab4ac..802f60a7eba7 100644 --- a/internal/transport/proxy.go +++ b/internal/transport/proxy.go @@ -67,7 +67,8 @@ func doHTTPConnectHandshake(ctx context.Context, conn net.Conn, grpcUA string, o Header: map[string][]string{"User-Agent": {grpcUA}}, } - if user := opts.User; user != (url.Userinfo{}) { + if userSet := opts.UserSet; userSet { + user := opts.User u := user.Username() p, _ := user.Password() req.Header.Add(proxyAuthHeaderKey, "Basic "+basicAuth(u, p))