Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions internal/envconfig/envconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,14 @@ var (
// ALTSHandshakerKeepaliveParams is set if we should add the
// KeepaliveParams when dial the ALTS handshaker service.
ALTSHandshakerKeepaliveParams = boolFromEnv("GRPC_EXPERIMENTAL_ALTS_HANDSHAKER_KEEPALIVE_PARAMS", false)

// EnableDefaultPortForProxyTarget controls whether the resolver adds a default port 443
// to a target address that lacks one. This flag only has an effect when all of
// the following conditions are met:
// - A connect proxy is being used.
// - Target resolution is disabled.
// - The DNS resolver is being used.
EnableDefaultPortForProxyTarget = boolFromEnv("GRPC_EXPERIMENTAL_ENABLE_DEFAULT_PORT_FOR_PROXY_TARGET", true)
)

func boolFromEnv(envVar string, def bool) bool {
Expand Down
48 changes: 43 additions & 5 deletions internal/resolver/delegatingresolver/delegatingresolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ package delegatingresolver

import (
"fmt"
"net"
"net/http"
"net/url"
"sync"

"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/internal/envconfig"
"google.golang.org/grpc/internal/proxyattributes"
"google.golang.org/grpc/internal/transport"
"google.golang.org/grpc/internal/transport/networktype"
Expand All @@ -40,6 +42,8 @@ var (
HTTPSProxyFromEnvironment = http.ProxyFromEnvironment
)

const defaultPort = "443"

// delegatingResolver manages both target URI and proxy address resolution by
// delegating these tasks to separate child resolvers. Essentially, it acts as
// an intermediary between the gRPC ClientConn and the child resolvers.
Expand Down Expand Up @@ -107,10 +111,13 @@ func New(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOpti
targetResolver: nopResolver{},
}

var err error
r.proxyURL, err = proxyURLForTarget(target.Endpoint())
addr, err := parseTarget(target.Endpoint())
if err != nil {
return nil, fmt.Errorf("delegating_resolver: invalid target address %q: %v", target.Endpoint(), err)
}
Comment on lines +114 to +117
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should guard this call to parseTarget behind the feature flag since it can potentially lead to new/different failures after this PR is merged.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can have only one block guarded by the feature flag if we instead do the following:

addr := target.Endpoint()
if target.URL.Scheme == "dns" && !targetResolutionEnabled && envconfig.AddDefaultPort {
 addr, err = parseTarget(target.Endpoint(), defaultPort)
  if err != nil {
    // return some error
  }
}

This way we can use the parseTarget function from the dns resolver without any modifications.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thinking was we should do the basic check and add localhost for all addresses if no host is present irrespective of the resolver being used, but adding default port 443 is only for DNS resolver with target resolution disabled. And the first part should not be behind the flag , but only 2nd part should. That is my understanding, let me know if I am missing something.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my understanding, only the DNS resolver was defaulting the hostname to localhost. Applying this logic to all resolvers would be a behaviour change which could potentially break some custom resolver that isn't expecting this. I would recommend avoiding such a change to be safe.


Also, since we're providing a env variable flag, we should ensure the flag can revert all (if not most) changes in this PR.

r.proxyURL, err = proxyURLForTarget(addr)
if err != nil {
return nil, fmt.Errorf("delegating_resolver: failed to determine proxy URL for target %s: %v", target, err)
return nil, fmt.Errorf("delegating_resolver: failed to determine proxy URL for target %q: %v", target, err)
}

// proxy is not configured or proxy address excluded using `NO_PROXY` env
Expand All @@ -131,9 +138,12 @@ 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 {
if envconfig.EnableDefaultPortForProxyTarget {
addr = maybeAddDefaultPort(addr, defaultPort)
}
r.targetResolverState = &resolver.State{
Addresses: []resolver.Address{{Addr: target.Endpoint()}},
Endpoints: []resolver.Endpoint{{Addresses: []resolver.Address{{Addr: target.Endpoint()}}}},
Addresses: []resolver.Address{{Addr: addr}},
Endpoints: []resolver.Endpoint{{Addresses: []resolver.Address{{Addr: addr}}}},
}
r.updateTargetResolverState(*r.targetResolverState)
return r, nil
Expand Down Expand Up @@ -202,6 +212,34 @@ func needsProxyResolver(state *resolver.State) bool {
return false
}

func parseTarget(target string) (string, error) {
host, port, err := net.SplitHostPort(target)
if err != nil {
return target, nil
}
if port == "" {
// If the port field is empty (target ends with colon), e.g. "[::1]:",
// this is an error.
return "", fmt.Errorf("missing port after port-separator colon")
}
// target has port, i.e ipv4-host:port, [ipv6-host]:port, host-name:port
if host == "" {
// Keep consistent with net.Dial(): If the host is empty, as in ":80",
// the local system is assumed.
host = "localhost"
}
return net.JoinHostPort(host, port), nil
}

func maybeAddDefaultPort(target, defaultPort string) string {
if _, _, err := net.SplitHostPort(target); err == nil {
// target already has port
return target
}
return net.JoinHostPort(target, defaultPort)

}

func skipProxy(address resolver.Address) bool {
// Avoid proxy when network is not tcp.
networkType, ok := networktype.Get(address)
Expand Down
105 changes: 100 additions & 5 deletions internal/resolver/delegatingresolver/delegatingresolver_ext_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ import (
"errors"
"net/http"
"net/url"
"strings"
"testing"
"time"

"github.com/google/go-cmp/cmp"
"google.golang.org/grpc/internal/envconfig"
"google.golang.org/grpc/internal/grpctest"
"google.golang.org/grpc/internal/proxyattributes"
"google.golang.org/grpc/internal/resolver/delegatingresolver"
Expand Down Expand Up @@ -246,17 +248,110 @@ func (s) TestDelegatingResolverwithDNSAndProxyWithTargetResolution(t *testing.T)
}
}

// 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.
// Tests the creation of a delegating resolver when a proxy is configured. It
// verifies both successful creation for valid targets and correct error
// handling for invalid ones.
//
// For successful cases, it ensures the final address is from the proxy resolver
// and contains the original, correctly-formatted target address as an
// attribute.
func (s) TestDelegatingResolverwithDNSAndProxyWithNoTargetResolution(t *testing.T) {
const (
envProxyAddr = "proxytest.com"
resolvedProxyTestAddr1 = "11.11.11.11:7687"
)
tests := []struct {
name string
target string
wantConnectAddress string
wantErrorSubstring string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add another test param for the env variable and tests cases for the env variable being disabled?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I already have a seperate test for it : TestDelegatingResolverEnvVarForDefaultPortDisable

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we combine the tests using an additional param? That should help reduce duplicate code.

}{
{
name: "no port in target",
target: "test.com",
wantConnectAddress: "test.com:443",
},
{
name: "port specified in target",
target: "test.com:8080",
wantConnectAddress: "test.com:8080",
},
{
name: "colon after host in target but no post",
target: "test.com:",
wantErrorSubstring: "missing port after port-separator colon",
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
overrideTestHTTPSProxy(t, envProxyAddr)

targetResolver := manual.NewBuilderWithScheme("dns")
target := targetResolver.Scheme() + ":///" + test.target
// Set up a manual DNS resolver to control the proxy address resolution.
proxyResolver, proxyResolverBuilt := setupDNS(t)

tcc, stateCh, _ := createTestResolverClientConn(t)
_, err := delegatingresolver.New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, targetResolver, false)
if test.wantErrorSubstring != "" {
// Case 1: We expected an error.
if err == nil {
t.Fatalf("Delegating resolver created, want error containing %q", test.wantErrorSubstring)
}
if !strings.Contains(err.Error(), test.wantErrorSubstring) {
t.Fatalf("Delegating resolver failed with error %v, want error containing %v", err.Error(), test.wantErrorSubstring)
}
return
}

// Case 2: We did NOT expect an error.
if err != nil {
t.Fatalf("Delegating resolver creation failed unexpectedly with error: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()

// Wait for the proxy resolver to be built before calling UpdateState.
mustBuildResolver(ctx, t, proxyResolverBuilt)
proxyResolver.UpdateState(resolver.State{
Addresses: []resolver.Address{
{Addr: resolvedProxyTestAddr1},
},
})

wantState := resolver.State{
Addresses: []resolver.Address{proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, test.wantConnectAddress)},
Endpoints: []resolver.Endpoint{{Addresses: []resolver.Address{proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, test.wantConnectAddress)}}},
}

var gotState resolver.State
select {
case gotState = <-stateCh:
case <-ctx.Done():
t.Fatal("Context timed out 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
// "dns", target resolution is disabled, and the environment variable to add
// default port is disabled. The test verifies that the addresses returned by
// the delegating resolver include the resolved proxy address and the unresolved
// target address without a port as attributes of the proxy address.
func (s) TestDelegatingResolverEnvVarForDefaultPortDisabled(t *testing.T) {
const (
targetTestAddr = "test.com"
envProxyAddr = "proxytest.com"
resolvedProxyTestAddr1 = "11.11.11.11:7687"
)

testutils.SetEnvConfig(t, &envconfig.EnableDefaultPortForProxyTarget, false)
overrideTestHTTPSProxy(t, envProxyAddr)

targetResolver := manual.NewBuilderWithScheme("dns")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,7 @@ func Test(t *testing.T) {
grpctest.RunSubTests(t, s{})
}

const (
targetTestAddr = "test.com"
envProxyAddr = "proxytest.com"
)
const targetTestAddr = "test.com"

// overrideHTTPSProxyFromEnvironment function overwrites HTTPSProxyFromEnvironment and
// returns a function to restore the default values.
Expand Down
2 changes: 1 addition & 1 deletion internal/transport/proxy_ext_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ func (s) TestBasicAuthInNewClientWithProxy(t *testing.T) {
proxyCalled := false
reqCheck := func(req *http.Request) {
proxyCalled = true
if got, want := req.URL.Host, "example.test"; got != want {
if got, want := req.URL.Host, "example.test:443"; got != want {
t.Errorf(" Unexpected request host: %s, want = %s ", got, want)
}
wantProxyAuthStr := "Basic " + base64.StdEncoding.EncodeToString([]byte(user+":"+password))
Expand Down