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
45 changes: 41 additions & 4 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,18 @@ func New(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOpti
targetResolver: nopResolver{},
}

addr := target.Endpoint()
var err error
r.proxyURL, err = proxyURLForTarget(target.Endpoint())
if target.URL.Scheme == "dns" && !targetResolutionEnabled && envconfig.EnableDefaultPortForProxyTarget {
addr, err = parseTarget(addr)
if err != nil {
return nil, fmt.Errorf("delegating_resolver: invalid target address %q: %v", target.Endpoint(), err)
}
}

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 @@ -132,8 +144,8 @@ func New(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOpti
// bypass the target resolver and store the unresolved target address.
if target.URL.Scheme == "dns" && !targetResolutionEnabled {
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 +214,31 @@ func needsProxyResolver(state *resolver.State) bool {
return false
}

func parseTarget(target string) (string, error) {
if target == "" {
return "", fmt.Errorf("missing address")
}
Comment on lines +218 to +220
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this even possible since we already parse the target URI specified by the user in clientconn.go before we get here?

Ideally, it would be nicer to avoid checking conditions that are not possible, since it would simplify the code.

if host, port, err := net.SplitHostPort(target); err == nil {
if port == "" {
Copy link
Contributor

Choose a reason for hiding this comment

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

Based on the documentation of net.SplitHostPort, the port seems to be mandatory and it will return a missing port in address error in that case. So, I don't think we could have err == nil and port == "".

// SplitHostPort splits a network address of the form "host:port",
// "host%zone:port", "[host]:port" or "[host%zone]:port" into host or
// host%zone and port.

Please correct me if I'm wrong.

// If the port field is empty (target ends with colon), e.g. "[::1]:",
// this is an error.
Comment on lines +223 to +224
Copy link
Contributor

Choose a reason for hiding this comment

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

Optional: I think it would be better to group these inline comments into a docstring for the function. That way, one can just read that docstring and get to know what the function is doing instead of reading it one piece at a time.

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
}
if host, port, err := net.SplitHostPort(target + ":" + defaultPort); err == nil {
// target doesn't have port
return net.JoinHostPort(host, port), nil
}
return net.JoinHostPort(target, defaultPort), nil
}

func skipProxy(address resolver.Address) bool {
// Avoid proxy when network is not tcp.
networkType, ok := networktype.Get(address)
Expand Down
132 changes: 92 additions & 40 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,54 +248,104 @@ 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 (
targetTestAddr = "test.com"
envProxyAddr = "proxytest.com"
resolvedProxyTestAddr1 = "11.11.11.11:7687"
)
overrideTestHTTPSProxy(t, envProxyAddr)

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

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)
}

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},
tests := []struct {
name string
target string
wantConnectAddress string
wantErrorSubstring string
disableDefaultPortEnvVar bool
}{
{
name: "no port in target",
Copy link
Contributor

Choose a reason for hiding this comment

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

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",
},
{
name: "add default port env variable diabled, no port in target",
target: "test.com",
wantConnectAddress: "test.com",
disableDefaultPortEnvVar: true,
},
})

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 <-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)
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if test.disableDefaultPortEnvVar {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do you have to check this field? Why can't you set the value of the env var to the one specified by the test unconditionally? That way, the code will be immune to changes in the default value of the env var.

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

targetResolver := manual.NewBuilderWithScheme("dns")
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't you have to unregister the original dns resolver and re-register it at the end of the test, for this test to not affect other tests?

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

Choose a reason for hiding this comment

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

Oh I see that this helper does re-register the original dns resolver. But maybe this section can still be a little more explicit/readable etc. I'm not able to think of the exact way to make this better right now.


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()
Comment on lines +307 to +323
Copy link
Contributor

Choose a reason for hiding this comment

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

This test logic seems complicated enough to me that you can actually split the tests into two: go/gotip/episodes/50


// 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)
}
})
}
}

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