-
Notifications
You must be signed in to change notification settings - Fork 4.4k
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
credentials: local creds implementation #3517
Conversation
@dfawley, could you please review this PR? @jiangtaoli2016 F.Y.I. |
Thanks @easwars for the review! All comments are addressed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changes look good to me. Please do wait for either Doug or Jiangtao to approve as well. Thank you.
Friendly ping @dfawley |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you please also add a test in grpc/test
as an end-to-end test? I.e. grpc.Dial
with these credentials on localhost, uds, and maybe (if possible) a non-127-loopback address to make sure they work as expected. Thanks!
credentials/local/local.go
Outdated
func getSecurityLevel(network, addr string) (credentials.SecurityLevel, error) { | ||
switch { | ||
// Local TCP connection | ||
case strings.HasPrefix(addr, "127."), strings.HasPrefix(addr, "localhost:"), strings.HasPrefix(addr, "[::1]:"): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is addr the address as passed to net.Dial
?
If so, the following will not work:
/etc/hosts:
127.0.0.1 workstation
foo.go:
grpc.Dial("workstation", local.NewCredentials())
Maybe this is fine, but I wanted to point that out. If there's a way to retrieve the actual IP address, that would be better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, it's not the address passed to net.dial
but the one derived from RemoteAddr().String()
of an established net.Conn
, which I believe is the actual IP address.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If that is the case, then is "localhost:" needed?
Thanks @dfawley for the review. All comments are addressed. In |
Please resolve the conflicts in your branch to unblock travis. |
test/end2end_test.go
Outdated
@@ -626,6 +662,9 @@ func (te *test) listenAndServe(ts testpb.TestServiceServer, listen func(network, | |||
te.t.Fatalf("Failed to generate credentials %v", err) | |||
} | |||
sopts = append(sopts, grpc.Creds(creds)) | |||
case "local": | |||
creds := local.NewCredentials() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: remove local variable
test/end2end_test.go
Outdated
@@ -803,6 +842,9 @@ func (te *test) configDial(opts ...grpc.DialOption) ([]grpc.DialOption, string) | |||
te.t.Fatalf("Failed to load credentials: %v", err) | |||
} | |||
opts = append(opts, grpc.WithTransportCredentials(creds)) | |||
case "local": | |||
creds := local.NewCredentials() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: remove local variable
credentials/local/local.go
Outdated
return "local" | ||
} | ||
|
||
// localTC is the credentials required to eatablish a local connection. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
*establish
credentials/local/local.go
Outdated
func getSecurityLevel(network, addr string) (credentials.SecurityLevel, error) { | ||
switch { | ||
// Local TCP connection | ||
case strings.HasPrefix(addr, "127."), strings.HasPrefix(addr, "localhost:"), strings.HasPrefix(addr, "[::1]:"): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If that is the case, then is "localhost:" needed?
credentials/local/local.go
Outdated
return credentials.PrivacyAndIntegrity, nil | ||
// Not a local connection and should fail | ||
default: | ||
return credentials.Invalid, fmt.Errorf("credentials: local credentials should be used in a local connection") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe: "local credentials rejected connection to non-local address %q", addr
test/end2end_test.go
Outdated
tcpClearRREnv = env{name: "tcp-clear", network: "tcp", balancer: "round_robin"} | ||
tcpTLSRREnv = env{name: "tcp-tls", network: "tcp", security: "tls", balancer: "round_robin"} | ||
handlerEnv = env{name: "handler-tls", network: "tcp", security: "tls", httpHandler: true, balancer: "round_robin"} | ||
noBalancerEnv = env{name: "no-balancer", network: "tcp", security: "tls"} | ||
allEnv = []env{tcpClearEnv, tcpTLSEnv, tcpClearRREnv, tcpTLSRREnv, handlerEnv, noBalancerEnv} | ||
allEnv = []env{tcpClearEnv, tcpTLSEnv, tcpLocalEnv, tcpClearRREnv, tcpTLSRREnv, handlerEnv, noBalancerEnv} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's leave this out of allEnv
entirely and just test it specifically in one test case.
Thanks @dfawley. All comments are addressed. PTAL |
test/end2end_test.go
Outdated
@@ -421,6 +448,7 @@ func (e env) dialer(addr string, timeout time.Duration) (net.Conn, error) { | |||
var ( | |||
tcpClearEnv = env{name: "tcp-clear-v1-balancer", network: "tcp", balancer: "v1"} | |||
tcpTLSEnv = env{name: "tcp-tls-v1-balancer", network: "tcp", security: "tls", balancer: "v1"} | |||
tcpLocalEnv = env{name: "tcp-local-v1-balancer", network: "tcp", listenerAddr: "[::1]:0", security: "local", balancer: "v1"} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is now no test that uses this. (And I think that's OK.)
Please add an end-to-end test specifically for the local credentials. It should ideally:
- use local creds on client and server with a TCP dial to "localhost" that succeeds
- use local creds on client and server with a UDS connection that succeeds
- use local creds on client and server with a loopback-but-non-127.* TCP dial that fails (observe failure on client, then use WithInsecure on client and observe failure on server)
I'm not sure how to do that last one in a portable way, but it should be possible if you limit it to linux systems.
EDIT: by wrapping the listener (which wraps the returned conns) and the dialer, this should be fairly simple.
I think you can just do all this ~manually using a stubServer
, and not modify the global testServer
.
Ping, please fix the conflicts and add tests. Thanks |
Sorry, I was distracted by another task. I will get back to it early next week. |
Sorry for the long delay. I just added E2E tests, and PTAL. |
credentials/local/local_test.go
Outdated
} | ||
got, err := serverAndClientHandshake(lis) | ||
if got != tc.want { | ||
t.Fatalf("ServerAndClientHandshake(%s, %s) returned %s but want %s. Error: %v", tc.testNetwork, tc.testAddr, got.String(), tc.want.String(), err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Go style:
t.Fatalf("serverAndClientHandshake(%s, %s) = %v, %v; want %v, nil", tc.testNetwork, tc.testAddr, got, err, want)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
test/end2end_test.go
Outdated
@@ -214,18 +215,43 @@ func (s *testServer) UnaryCall(ctx context.Context, in *testpb.SimpleRequest) (* | |||
if s.security != "" { | |||
// Check Auth info | |||
var authType, serverName string | |||
var secLevel credentials.SecurityLevel |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are these changes still necessary? If at all possible, we should avoid any changes to testServer
. Use a stubServer
instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will revert the changes in end2end_test.go
, and instead added the security level check (which is all we changed in end2end_test.go
) to local_creds_test.go
.
test/local_creds_test.go
Outdated
go s.Serve(lis) | ||
|
||
var cc *grpc.ClientConn | ||
if network == "unix" { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
switch network {
case "unix":
...
case "tcp":
...
default: return fmt.Errorf("unsupported network %q", network)
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the suggestion. Done.
test/local_creds_test.go
Outdated
|
||
var cc *grpc.ClientConn | ||
if network == "unix" { | ||
cc, err = grpc.Dial("passthrough:///"+address, grpc.WithTransportCredentials(local.NewCredentials()), grpc.WithContextDialer( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
passthrough
is the default; do you need this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No it's not needed. I removed it.
test/local_creds_test.go
Outdated
} | ||
|
||
func (s) TestLocalhost(t *testing.T) { | ||
err := testE2ESucceed("tcp", "localhost:0") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit:
if err := ...; err != nil {
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
test/local_creds_test.go
Outdated
if err != nil { | ||
return nil, err | ||
} | ||
return connWrapper{c, remoteAddrs[1]}, nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar, but unfortunately a little uglier:
func spoofDialer(addr string) func(string, time.Duration) (net.Conn, error) {
return func(t string, d time.Duration) (net.Conn, error) {
c, err := net.DialTimeout("tcp", t, d)
if err != nil {
return nil, err
}
return connWrapper{c, addr}, nil
}
}
``
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aha, good suggestion!
test/local_creds_test.go
Outdated
if useLocal { | ||
cc, err = grpc.Dial(lis.Addr().String(), grpc.WithTransportCredentials(local.NewCredentials()), grpc.WithDialer(dialer)) | ||
} else { | ||
cc, err = grpc.Dial(lis.Addr().String(), grpc.WithInsecure(), grpc.WithDialer(dialer)) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Idea: pass dopts []grpc.DialOption
as a parameter to this function, then:
cc, err := grpc.Dial(lis.Addr().String(), append(dopts, grpc.WithDialer(dialer))...)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. The logic will be simpler if we do that. Thanks.
test/local_creds_test.go
Outdated
// Use local creds at client-side which should lead to client-side failure. | ||
err := testE2EFail(true /*useLocal*/) | ||
if err == nil || !strings.Contains(err.Error(), "local credentials rejected connection to non-local address") { | ||
t.Fatalf("testE2EFail(%v) = _; want security handshake fails, %v", false, err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks wrong. You have the returned error being printed as the desired error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. The test is revised.
test/local_creds_test.go
Outdated
// Use insecure at client-side which should lead to server-side failure. | ||
err := testE2EFail(false /*useLocal*/) | ||
if err == nil { | ||
t.Fatalf("testE2EFail(%v) = _; want security handshake fails, %v", true, err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same. What kind of error do we expect to see here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should expected connection closed
error. I updated the test.
test/local_creds_test.go
Outdated
func (s) TestClientFail(t *testing.T) { | ||
// Use local creds at client-side which should lead to client-side failure. | ||
err := testE2EFail(true /*useLocal*/) | ||
if err == nil || !strings.Contains(err.Error(), "local credentials rejected connection to non-local address") { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we also check status.Code(err)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
All comments are addressed. PTAL. |
FYI: travis seems to be failing:
Seems like there's raciness determining what kind of error appears to the client when this happens. |
test/local_creds_test.go
Outdated
switch network { | ||
case "unix": | ||
if secLevel != credentials.PrivacyAndIntegrity { | ||
return nil, status.Errorf(codes.Unauthenticated, "Wrong security level: got %q, want %q", secLevel.String(), credentials.PrivacyAndIntegrity.String()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am pretty sure fmt.Sprintf
(which status.Errorf
uses) will call .String()
for you in this case (with %q
) - won't it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, you are right. Thanks for the suggestion. The code is updated.
test/local_creds_test.go
Outdated
func(ctx context.Context, addr string) (net.Conn, error) { | ||
return net.Dial("unix", address) | ||
})) | ||
} else { | ||
case "tcp": | ||
cc, err = grpc.Dial(lis.Addr().String(), grpc.WithTransportCredentials(local.NewCredentials())) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this also dial address
to be consistent with the above?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By dialing address
it will dial with port 0, which is incorrect. I updated the test to use lisAddr
in both unix
and tcp
cases.
test/local_creds_test.go
Outdated
} | ||
|
||
if err != nil { | ||
return fmt.Errorf("Failed to dial server: %v, %v", err, lis.Addr().String()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same: address
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I updated it to use lisAddr
instead.
test/local_creds_test.go
Outdated
if status.Code(got) == status.Code(want) && strings.Contains(status.Convert(got).Message(), status.Convert(want).Message()) { | ||
return true | ||
} | ||
return false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Simplify: return <condition>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
test/local_creds_test.go
Outdated
@@ -48,11 +49,11 @@ func testE2ESucceed(network, address string) error { | |||
switch network { | |||
case "unix": | |||
if secLevel != credentials.PrivacyAndIntegrity { | |||
return nil, status.Errorf(codes.Unauthenticated, "Wrong security level: got %q, want %q", secLevel.String(), credentials.PrivacyAndIntegrity.String()) | |||
return nil, status.Errorf(codes.Unauthenticated, fmt.Sprintf("Wrong security level: got %q, want %q", secLevel, credentials.PrivacyAndIntegrity)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
status.Errorf
uses fmt.Sprintf
already. Remove this call here.
test/local_creds_test.go
Outdated
if err != nil { | ||
return fmt.Errorf("Failed to parse listener address: %v", err) | ||
} | ||
lisAddr = "localhost:" + port |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you not do: lisAddr = lis.Addr().String()
and remove the parsing?
test/local_creds_test.go
Outdated
want := status.Error(codes.Unavailable, "") | ||
if err := testE2EFail(opts); !isExpected(err, want) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Simplify:
if err := testE2EFail(opts); status.Code(err) != codes.Unavailable {
Friendly ping @dfawley |
test/local_creds_test.go
Outdated
testpb "google.golang.org/grpc/test/grpc_testing" | ||
) | ||
|
||
func testE2ESucceed(network, address string) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know this is in a file called local_creds_test.go
, but it's in a huge testing package. Please give this a name that includes LocalCreds
. Same for everything global in this file, especially the Test____
functions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
Thanks a lot Doug for the detailed review! Really appreciate it. |
This reverts commit 9eb3e7d.
This PR implements gRPC Go local credentials.
The local credentials should be used in either a UDS and local TCP connection. The former will be associated with the security level
PrigvacyAndIntegrity
while the latter is associated withNoSecurity
. Notice that local credentials will be used to migrate the call sites of WithInsecure used in local connections.