Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

*: Correct gRFC A1 implementation #7881

Open
wants to merge 29 commits into
base: master
Choose a base branch
from

Conversation

eshitachandwani
Copy link
Member

@eshitachandwani eshitachandwani commented Nov 29, 2024

Fixes: #7556

  • Remove environment variable detection during transport creation. Introduce a per-address attribute to specify the proxy CONNECT string. This change aligns with the xDS design and provides more control over proxy behavior.
  • Create a new resolver that handles proxy configuration and delegates to child resolvers for target and proxy address resolution.
  • Introduce a new DialOption to preserve the current behavior of grpc.Dial, where the resolved target address is used in the proxy CONNECT request. This option ensures backward compatibility for users relying on the existing behavior.

RELEASE NOTES:

  • Target resolution is no longer performed on the client when using default resolvers with grpc.NewClient in conjunction with proxy environment variables.
  • New WithTargetResolutionEnabled() dial option added to explicitly force target URI resolution on the client.
  • When non-default resolvers are used alongside proxy environment variables, target resolution is now performed on the client.

@eshitachandwani eshitachandwani added the Area: Client Includes Channel/Subchannel/Streams, Connectivity States, RPC Retries, Dial/Call Options and more. label Nov 29, 2024
@eshitachandwani eshitachandwani added this to the 1.69 Release milestone Nov 29, 2024
Copy link

codecov bot commented Nov 29, 2024

Codecov Report

Attention: Patch coverage is 77.77778% with 30 lines in your changes missing coverage. Please review.

Project coverage is 82.11%. Comparing base (724f450) to head (ef927ce).

Files with missing lines Patch % Lines
internal/transport/proxy_utils.go 67.39% 23 Missing and 7 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #7881      +/-   ##
==========================================
+ Coverage   82.05%   82.11%   +0.06%     
==========================================
  Files         381      382       +1     
  Lines       38539    38628      +89     
==========================================
+ Hits        31622    31720      +98     
+ Misses       5602     5591      -11     
- Partials     1315     1317       +2     
Files with missing lines Coverage Δ
clientconn.go 92.45% <100.00%> (-0.33%) ⬇️
dialoptions.go 90.80% <100.00%> (+1.82%) ⬆️
.../resolver/delegatingresolver/delegatingresolver.go 74.85% <100.00%> (+5.98%) ⬆️
internal/transport/http2_client.go 91.40% <100.00%> (+<0.01%) ⬆️
internal/transport/proxy.go 59.61% <100.00%> (-8.39%) ⬇️
internal/transport/transport.go 91.56% <ø> (+6.82%) ⬆️
resolver_wrapper.go 84.16% <100.00%> (+1.28%) ⬆️
internal/transport/proxy_utils.go 67.39% <67.39%> (ø)

... and 24 files with indirect coverage changes

}

// 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 {
Copy link
Contributor

Choose a reason for hiding this comment

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

may be we can do something like this fake server in its own package

// Package fakeserver provides a fake implementation of the management server.
? wdyt?

New should only create the object and StartServer/Run should start the go routine to accept requests

address := addr.Addr

//if the ProxyConnectAddr is set in the aattribute, do a proxy dial.
Copy link
Contributor

Choose a reason for hiding this comment

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

typo: attribute

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.

p, _ := t.Password()
if user := attributes.User(address); user != nil {
u := user.Username()
p, _ := user.Password()
Copy link
Contributor

Choose a reason for hiding this comment

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

hmmm...we should not ignore the bool. What happens if password is not set?

Copy link
Member Author

Choose a reason for hiding this comment

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

It will append an empty string with the username. I have referenced it from the earlier code itself.

Copy link
Contributor

Choose a reason for hiding this comment

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

then user.Password() should just return empty string? it looks weird to ignore or atleast we should log a warning. So, if password is empty will the connection still succeed or we should do early failure?

Copy link
Member Author

Choose a reason for hiding this comment

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

From what I understand, it will still succeed.

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
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 remove the necessary part now? It will always dial proxy if called?

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

}

// TestGrpcDialWithProxy tests grpc.Dial using a proxy and default
// resolver in the target URI.and verifies that it connects to the proxy server
Copy link
Contributor

Choose a reason for hiding this comment

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

typo space and remove period after target URI

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

test/proxy_test.go Outdated Show resolved Hide resolved
test/proxy_test.go Outdated Show resolved Hide resolved
@arjan-bal arjan-bal self-requested a review December 5, 2024 07:31
@arjan-bal arjan-bal self-assigned this Dec 5, 2024
@purnesh42H purnesh42H modified the milestones: 1.69 Release, 1.70 Release Dec 5, 2024
addr := resolver.Address{
Addr: "test-address",
}

Copy link
Contributor

Choose a reason for hiding this comment

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

nix new line

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.

Addr: "test-address",
Attributes: attributes.New(userAndConnectAddrKey, attr{user: user, addr: ""}),
}

Copy link
Contributor

Choose a reason for hiding this comment

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

nix new line

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.

Addr: "test-address",
Attributes: attributes.New(userAndConnectAddrKey, attr{user: nil, addr: "proxy-address"}),
}

Copy link
Contributor

Choose a reason for hiding this comment

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

nix new line

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.

// 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) {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: TestGRPC

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.

// 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) {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: TestGRPC

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.


// 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.
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: mention the dial option WithTargetResolutionEnabled()

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.

case <-resolutionCh:
t.Logf("target resolution happened on client")
default:
t.Fatalf("Client-side resolution should be called but wasn't")
Copy link
Contributor

@purnesh42H purnesh42H Dec 5, 2024

Choose a reason for hiding this comment

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

"target resolution did not happen on client" ?

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.


// 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) {
Copy link
Contributor

Choose a reason for hiding this comment

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

for the NoProxy and CustomDialer case, should we still override overrideHTTPSProxyFromEnvironment? That will make sure that even though proxy env is set we should skip it because of NoProxy and CustomDialer or is that not intended?

Copy link
Member Author

Choose a reason for hiding this comment

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

If WithNoProxy or WithContextDialer are set, the HTTPSProxyFromEnviornment function will never be called, but we can still override and add error if it is being called. Is it a good practice to do so? @purnesh42H

Copy link
Contributor

Choose a reason for hiding this comment

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

yeah so right now if HTTPSProxyFromEnviornment is not present, it doesn't matter if WithNoProxy or WithContextDialer is set? The resolution will happen at client anyways? So having it present and not being called in these two cases is what we want to verify.

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.

}
wantProxyAuthStr := "Basic " + base64.StdEncoding.EncodeToString([]byte(user+":"+password))
if got := req.Header.Get("Proxy-Authorization"); got != wantProxyAuthStr {
gotDecoded, _ := base64.StdEncoding.DecodeString(got)
Copy link
Contributor

Choose a reason for hiding this comment

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

we should not ignore decoding errors. it should return error if decoding fails

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.

p, _ := t.Password()
if user := attributes.User(address); user != nil {
u := user.Username()
p, _ := user.Password()
Copy link
Contributor

Choose a reason for hiding this comment

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

then user.Password() should just return empty string? it looks weird to ignore or atleast we should log a warning. So, if password is empty will the connection still succeed or we should do early failure?

Copy link
Contributor

@arjan-bal arjan-bal left a comment

Choose a reason for hiding this comment

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

I haven't reviewed the tests yet, will take a look at them next.

clientconn.go Outdated
//
// 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,
Copy link
Contributor

Choose a reason for hiding this comment

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

any other scheme like "dns"

From my understanding, WithTargetResolutionEnabled ONLY effects the dns resolver's behaviour.

https://github.com/grpc/grpc-go/blob/063d352de07403a582ef33f8f5f8149e3b57c47e/internal/resolver/delegatingresolver/delegatingresolver.go#L117C36-L117C59

If this is the case, we should mention this in the godoc of WithTargetResolutionEnabled and change this comment to say when "dns" is used. Comments should be precise and concise.

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.

clientconn.go Outdated
// 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.
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Remove the the ending as expected, it doesn't seem to add any extra information.

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.

clientconn.go Show resolved Hide resolved
dialoptions.go Outdated Show resolved Hide resolved
internal/transport/http2_client.go Outdated Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason for moving proxy tests out of internal/transport? It's preferable to keep tests in separate packages as it allows tests to run in parallel.

Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like these tests are deleted, not moved. We need these tests.

Copy link
Member Author

Choose a reason for hiding this comment

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

Whatever was being tested in the tests is being tested in the E2E tests, basic auth and connect requests and connecting to the proxy server. The mapAddress function was moved to delegating resolver and that is being tested in delegating resolver. Do we need to test is again in unit tests?

Copy link
Contributor

@arjan-bal arjan-bal Dec 24, 2024

Choose a reason for hiding this comment

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

TestHTTPConnectWithServerHello is added to verify that the optimization added in #7424 doesn't effect correctness. It is difficult to test it in en e2e fashion because it needs the server to send the first message after creation of the TCP connection and the proxy to buffer this message along with the CONNECT response. I think the other tests can be replaced.

internal/testutils/proxy.go Outdated Show resolved Hide resolved
@arjan-bal arjan-bal removed their assignment Dec 23, 2024
dialoptions.go Outdated
@@ -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
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 mention when a proxy is used along with the the "dns" scheme.

Copy link
Contributor

@arjan-bal arjan-bal Dec 24, 2024

Choose a reason for hiding this comment

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

TestHTTPConnectWithServerHello is added to verify that the optimization added in #7424 doesn't effect correctness. It is difficult to test it in en e2e fashion because it needs the server to send the first message after creation of the TCP connection and the proxy to buffer this message along with the CONNECT response. I think the other tests can be replaced.

u := user.Username()
p, pSet := user.Password()
if !pSet && logger.V(2) {
logger.Warningf("password not set for basic authentication for proxy dialing")
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this warrant a log? I believe the password is allowed to be empty in HTTP Basic auth.

}

// 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 {
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 move these tests back to the transport directory? It will be easier to review the delta in the proxy server code instead of the whole code.

Comment on lines 129 to 133
if resOnClient {
out, err = net.Dial("tcp", req.URL.Host)
} else {
out, err = net.Dial("tcp", backendAddr)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

The proxy server should always connect to the address received in the request. The address in the request can be either an IP or a hostname depending on how the client is configured.

// and server to client.
go io.Copy(p.in, p.out)
go io.Copy(p.out, p.in)
close(doneCh)
Copy link
Contributor

Choose a reason for hiding this comment

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

What is the intended use of doneCh here? The go routines for the proxy are running in the background when doneCh is closed. If the intention is to track the completion of the CONNECT request, we should rename the variable to something more appropriate like connectDone.

}
t.Logf("Started TestService backend at: %q", backend.Address)
t.Cleanup(backend.Stop)
return backend.Address
Copy link
Contributor

Choose a reason for hiding this comment

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

This is always an ip:port. Instead, you could use testutils.ParsePort to get the port and return localhost:port from here. This would allow you to test if the proxy receives an IP address or host based on the test case.

t.Errorf("EmptyCall failed: %v", err)
}

select {
Copy link
Contributor

Choose a reason for hiding this comment

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

Please add another branch to check for timeouts and fail the test in finite time.

if err := p.requestCheck(req); err != nil {
resp := http.Response{StatusCode: http.StatusMethodNotAllowed}
resp.Write(p.in)
p.in.Close()
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 log an error here instead of using the error channel, similar to the existing implementation. The write to the error channel can block indefinitely if no one reads and the test may pass if it doesn't read the errors in the channel, hiding real failures.

}()

// Configure manual resolvers for both proxy and target backends
targetResolver := 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.

We can use the real DNS resolver to resolve "localhost". Why use a fake hostname and manual resolver?

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 cannot use a local host because the http.FromProxyEnvirnoment as well as httpproxy.FromEnviornment returns a nil proxy URL if the target URI is local host. The documentation says :

http.FromProxyEnvirnoment
As a special case, if req.URL.Host is "localhost" (with or without a port number), then a nil URL and nil error will be returned.

http.FromProxyEnvirnoment
As a special case, if req.URL.Host is "localhost" or a loopback address (with or without a port number), then a nil URL and nil error will be returned.

@arjan-bal arjan-bal removed the Area: Client Includes Channel/Subchannel/Streams, Connectivity States, RPC Retries, Dial/Call Options and more. label Dec 24, 2024
Comment on lines 202 to 203
os.Setenv("HTTPS_PROXY", pLis.Addr().String())
defer func() { os.Setenv("HTTPS_PROXY", proxyEnv) }()
Copy link
Contributor

Choose a reason for hiding this comment

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

Can use t.SetEnv that automatically resets the value as part of test cleanup.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

NewClient functions behaviour is incompatible with secure forward-proxies
3 participants