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

transport,grpc: Integrate delegating resolver and introduce dial options for target host resolution #7881

Merged
merged 60 commits into from
Jan 24, 2025

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 WithLocalDNSResolution() 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 86.08696% with 16 lines in your changes missing coverage. Please review.

Project coverage is 82.34%. Comparing base (66f6471) to head (dd7ee47).
Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
internal/testutils/proxyserver/proxyserver.go 78.37% 12 Missing and 4 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #7881      +/-   ##
==========================================
+ Coverage   82.14%   82.34%   +0.20%     
==========================================
  Files         382      383       +1     
  Lines       38711    38776      +65     
==========================================
+ Hits        31798    31929     +131     
+ Misses       5583     5531      -52     
+ Partials     1330     1316      -14     
Files with missing lines Coverage Δ
clientconn.go 92.18% <100.00%> (+0.03%) ⬆️
dialoptions.go 90.80% <100.00%> (+1.82%) ⬆️
internal/proxyattributes/proxyattributes.go 100.00% <ø> (ø)
.../resolver/delegatingresolver/delegatingresolver.go 76.68% <100.00%> (+7.82%) ⬆️
internal/transport/http2_client.go 92.23% <100.00%> (+0.52%) ⬆️
internal/transport/proxy.go 70.58% <100.00%> (+2.58%) ⬆️
internal/transport/transport.go 91.56% <ø> (ø)
resolver_wrapper.go 84.16% <100.00%> (+1.28%) ⬆️
internal/testutils/proxyserver/proxyserver.go 78.37% <78.37%> (ø)

... and 14 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.

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

@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: "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.


// 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.


// 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.

}()
t.Logf("Started proxy at: %q", pLis.Addr())
t.Cleanup(p.stop)
p.Addr = fmt.Sprintf("localhost:%d", ParsePort(t, pLis.Addr().String()))
Copy link
Member

Choose a reason for hiding this comment

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

Also why can't this just be pLis.Addr().String()? There should be a comment explaining why it's doing all this extra stuff to seemingly just convert localhost:123 into localhost:123.

Copy link
Member Author

Choose a reason for hiding this comment

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

pLis..Addr() will be a IP address, we convert it to localhost:port to make sure the DNS resolver is working correctly for the proxy and connecting to the proxy server.

Copy link
Member

Choose a reason for hiding this comment

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

Oh true it is converted to IP address. But why is that a problem?

If it's for testing the delegating resolver -> DNS resolver path, then it seems like this should just use the raw listener address, and the tests that care about that should convert it.

Either way, definitely needs a comment.

u := t.Username()
p, _ := t.Password()

if user := opts.User; user != (url.Userinfo{}) {
Copy link
Member

Choose a reason for hiding this comment

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

This comparison with a zero-initialized Userinfo looks a little off to me. Should this field in opts be a pointer instead to determine whether it was set intentionally?

Or should it check if either Username() or Password() are non-empty?

I'm not sure exactly what the right behavior is, but this seems questionable, as it's including any unexported fields in the comparison.

Related but potential existing problem to investigate: what if username is set but password is not? Is that valid? If so, is the proper behavior to encode it as username:, as it does currently, or should the : be omitted?

Copy link
Member Author

Choose a reason for hiding this comment

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

The RFC doesn't clearly mention what is the correct syntax if password is absent , so I think either way should be fine, but adding a semicolon like username: would be more explicit in indication username has ended and the password is empty.

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 have added a UserSet option as changing it to a pointer can allow the caller to modify the use info.

Copy link
Member

Choose a reason for hiding this comment

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

SGTM on the behavior of username:.

changing it to a pointer can allow the caller to modify the use info.

"don't do that"?

An API that is impossible to use incorrectly is better than one that can be used incorrectly...but sometimes tradeoffs have to be made.

And with a separate field that has to be manually set, we run the risk of it being set incorrectly, so this alternative doesn't seem any better to me.

If you really want to have a bool then it should be unexported and handled automatically, like how the passwordSet field works in the userinfo itself:

https://cs.opensource.google/go/go/+/refs/tags/go1.23.5:src/net/url/url.go;l=409

Otherwise I think a pointer, and a note telling people not to modify it after setting it, is pretty reasonable and simpler. And doesn't that simplify things? Then we can just directly copy the User field from the URL without all this extra work. Simpler is almost always better.

Copy link
Member Author

Choose a reason for hiding this comment

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

Got it ! Done.

@dfawley dfawley assigned eshitachandwani and unassigned dfawley Jan 16, 2025
@@ -98,17 +103,18 @@ 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 {
Copy link
Member

Choose a reason for hiding this comment

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

New?

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.

u := t.Username()
p, _ := t.Password()

if user := opts.User; user != (url.Userinfo{}) {
Copy link
Member

Choose a reason for hiding this comment

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

SGTM on the behavior of username:.

changing it to a pointer can allow the caller to modify the use info.

"don't do that"?

An API that is impossible to use incorrectly is better than one that can be used incorrectly...but sometimes tradeoffs have to be made.

And with a separate field that has to be manually set, we run the risk of it being set incorrectly, so this alternative doesn't seem any better to me.

If you really want to have a bool then it should be unexported and handled automatically, like how the passwordSet field works in the userinfo itself:

https://cs.opensource.google/go/go/+/refs/tags/go1.23.5:src/net/url/url.go;l=409

Otherwise I think a pointer, and a note telling people not to modify it after setting it, is pretty reasonable and simpler. And doesn't that simplify things? Then we can just directly copy the User field from the URL without all this extra work. Simpler is almost always better.

}()
t.Logf("Started proxy at: %q", pLis.Addr())
t.Cleanup(p.stop)
p.Addr = fmt.Sprintf("localhost:%d", ParsePort(t, pLis.Addr().String()))
Copy link
Member

Choose a reason for hiding this comment

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

Oh true it is converted to IP address. But why is that a problem?

If it's for testing the delegating resolver -> DNS resolver path, then it seems like this should just use the raw listener address, and the tests that care about that should convert it.

Either way, definitely needs a comment.

@arjan-bal arjan-bal modified the milestones: 1.70 Release, 1.71 Release Jan 22, 2025
Copy link
Member

@dfawley dfawley left a comment

Choose a reason for hiding this comment

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

One last thing otherwise LGTM. Thanks!

// stop it, and returns a ProxyServer.
func HTTPProxy(t *testing.T, reqCheck func(*http.Request), waitForServerHello bool) *ProxyServer {
func NewHTTPProxy(t *testing.T, reqCheck func(*http.Request), waitForServerHello bool) *ProxyServer {
Copy link
Member

Choose a reason for hiding this comment

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

The thing it returns is a ProxyServer from package proxyserver so this should just be New.

Copy link
Member Author

Choose a reason for hiding this comment

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

Right! Done!

@dfawley dfawley assigned eshitachandwani and unassigned dfawley Jan 23, 2025
@eshitachandwani eshitachandwani merged commit 2fd426d into grpc:master Jan 24, 2025
15 checks passed
purnesh42H pushed a commit to purnesh42H/grpc-go that referenced this pull request Jan 30, 2025
…ons for target host resolution (grpc#7881)

* Change proxy behaviour
janardhanvissa pushed a commit to janardhanvissa/grpc-go that referenced this pull request Feb 13, 2025
…ons for target host resolution (grpc#7881)

* Change proxy behaviour
janardhanvissa pushed a commit to janardhanvissa/grpc-go that referenced this pull request Feb 13, 2025
…ons for target host resolution (grpc#7881)

* Change proxy behaviour
janardhanvissa pushed a commit to janardhanvissa/grpc-go that referenced this pull request Feb 15, 2025
…ons for target host resolution (grpc#7881)

* Change proxy behaviour
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
5 participants