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
55 changes: 51 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,41 @@ func needsProxyResolver(state *resolver.State) bool {
return false
}

// parseTarget takes a target string and ensures it is a valid "host:port" target.
//
// It does the following:
// 1. If the target already has a port (e.g., "host:port", "[ipv6]:port"),
// it is returned as is.
// 2. If the host part is empty (e.g., ":80"), it defaults to "localhost",
// returning "localhost:80".
// 3. If the target is missing a port (e.g., "host", "ipv6"), the defaultPort
// is added.
//
// An error is returned for empty targets or targets with a trailing colon
// but no port (e.g., "host:").
func parseTarget(target string) (string, error) {
if target == "" {
return "", fmt.Errorf("missing address")
}
Comment on lines +230 to +232
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.

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 dont see an explicit check for missing target in the clientconn.go and this is similar to the check we already have in dns.

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.

Copy link
Member Author

Choose a reason for hiding this comment

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

err == nil and port == "" would be case where the target is something like : host: i.e. they have the colon but no port specified after it.

// If the port field is empty (target ends with colon), e.g. "[::1]:",
// this is an error.
Comment on lines +235 to +236
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.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

return "", fmt.Errorf("missing port after port-separator colon")
}
// target has port
if host == "" {
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
150 changes: 114 additions & 36 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,130 @@ 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.
func (s) TestDelegatingResolverwithDNSAndProxyWithNoTargetResolution(t *testing.T) {
// Tests the creation of a delegating resolver when a proxy is configured. It
// verifies successful creation for valid targets and ensures the final address
// is from the proxy resolver and contains the original, correctly-formatted
// target address as an attribute.
func (s) TestDelegatingResolverwithDNSAndProxyWithNoTargetResolutionHappyPaths(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)
tests := []struct {
name string
target string
wantConnectAddress string
defaultPortEnvVar bool
}{
{
name: "no port ",
target: "test.com",
wantConnectAddress: "test.com:443",
defaultPortEnvVar: true,
},
{
name: "complete target",
target: "test.com:8080",
wantConnectAddress: "test.com:8080",
defaultPortEnvVar: true,
},
{
name: "no port with default port env variable diabled",
target: "test.com",
wantConnectAddress: "test.com",
defaultPortEnvVar: false,
},
}

ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
testutils.SetEnvConfig(t, &envconfig.EnableDefaultPortForProxyTarget, test.defaultPortEnvVar)
overrideTestHTTPSProxy(t, envProxyAddr)

// Wait for the proxy resolver to be built before calling UpdateState.
mustBuildResolver(ctx, t, proxyResolverBuilt)
proxyResolver.UpdateState(resolver.State{
Addresses: []resolver.Address{
{Addr: resolvedProxyTestAddr1},
},
})
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.

Copy link
Member Author

Choose a reason for hiding this comment

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

This is what we had earlier too..in the tests.


wantState := resolver.State{
Addresses: []resolver.Address{proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, targetTestAddr)},
Endpoints: []resolver.Endpoint{{Addresses: []resolver.Address{proxyAddressWithTargetAttribute(resolvedProxyTestAddr1, targetTestAddr)}}},
tcc, stateCh, _ := createTestResolverClientConn(t)
if _, err := delegatingresolver.New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, targetResolver, false); 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)
}
})
}
}

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")
// Tests the creation of a delegating resolver when a proxy is configured. It
// verifies correct error handling for invalid targets.
func (s) TestDelegatingResolverwithDNSAndProxyWithNoTargetResolutionWithErrorTargets(t *testing.T) {
const (
envProxyAddr = "proxytest.com"
resolvedProxyTestAddr1 = "11.11.11.11:7687"
)
tests := []struct {
name string
target string
wantErrorSubstring string
defaultPortEnvVar bool
}{

{
name: "colon after host but no port",
target: "test.com:",
wantErrorSubstring: "missing port after port-separator colon",
defaultPortEnvVar: true,
},
{
name: "empty target",
target: "",
wantErrorSubstring: "missing address",
defaultPortEnvVar: true,
},
}

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) {
testutils.SetEnvConfig(t, &envconfig.EnableDefaultPortForProxyTarget, test.defaultPortEnvVar)
overrideTestHTTPSProxy(t, envProxyAddr)

targetResolver := manual.NewBuilderWithScheme("dns")
target := targetResolver.Scheme() + ":///" + test.target

tcc, _, _ := createTestResolverClientConn(t)
_, err := delegatingresolver.New(resolver.Target{URL: *testutils.MustParseURL(target)}, tcc, resolver.BuildOptions{}, targetResolver, false)
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)
}
})
}
}

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