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

credentials: local creds implementation #3517

Merged
merged 9 commits into from
May 20, 2020
Merged

Conversation

yihuazhang
Copy link
Contributor

@yihuazhang yihuazhang commented Apr 10, 2020

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 with NoSecurity. Notice that local credentials will be used to migrate the call sites of WithInsecure used in local connections.

@yihuazhang
Copy link
Contributor Author

@dfawley, could you please review this PR?

@jiangtaoli2016 F.Y.I.

credentials/local/local.go Outdated Show resolved Hide resolved
credentials/local/local.go Outdated Show resolved Hide resolved
credentials/local/local.go Outdated Show resolved Hide resolved
credentials/local/local.go Outdated Show resolved Hide resolved
credentials/local/local.go Outdated Show resolved Hide resolved
credentials/local/local_test.go Outdated Show resolved Hide resolved
credentials/local/local.go Outdated Show resolved Hide resolved
credentials/local/local_test.go Outdated Show resolved Hide resolved
credentials/local/local_test.go Outdated Show resolved Hide resolved
credentials/local/local_test.go Outdated Show resolved Hide resolved
credentials/local/local.go Outdated Show resolved Hide resolved
credentials/local/local_test.go Outdated Show resolved Hide resolved
@yihuazhang
Copy link
Contributor Author

Thanks @easwars for the review! All comments are addressed.

@easwars easwars added the Type: Feature New features or improvements in behavior label Apr 13, 2020
@easwars easwars added this to the 1.30 Release milestone Apr 13, 2020
credentials/local/local_test.go Outdated Show resolved Hide resolved
credentials/local/local_test.go Outdated Show resolved Hide resolved
credentials/local/local_test.go Outdated Show resolved Hide resolved
credentials/local/local_test.go Outdated Show resolved Hide resolved
credentials/local/local_test.go Show resolved Hide resolved
credentials/local/local_test.go Outdated Show resolved Hide resolved
credentials/local/local_test.go Outdated Show resolved Hide resolved
Copy link
Contributor

@easwars easwars left a 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.

credentials/local/local_test.go Outdated Show resolved Hide resolved
credentials/local/local_test.go Outdated Show resolved Hide resolved
@yihuazhang
Copy link
Contributor Author

Friendly ping @dfawley

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.

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 Show resolved Hide resolved
credentials/local/local.go Outdated Show resolved Hide resolved
credentials/local/local.go Outdated Show resolved Hide resolved
credentials/local/local.go Outdated Show resolved Hide resolved
credentials/local/local.go Outdated Show resolved Hide resolved
credentials/local/local.go Outdated Show resolved Hide resolved
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]:"):
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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?

@yihuazhang
Copy link
Contributor Author

Thanks @dfawley for the review. All comments are addressed.

In end2end_test.go, I skipped TestServerMaxHeaderListSizeClientIntentionalViolation and TestClientMaxHeaderListSizeServerIntentionalViolation for local credentials as it results in timeout for UDS connections and I could not figure out why. Could you please shed some lights on it?

@dfawley
Copy link
Member

dfawley commented Apr 22, 2020

Please resolve the conflicts in your branch to unblock travis.

@@ -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()
Copy link
Member

Choose a reason for hiding this comment

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

Nit: remove local variable

@@ -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()
Copy link
Member

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 Show resolved Hide resolved
credentials/local/local.go Outdated Show resolved Hide resolved
credentials/local/local.go Outdated Show resolved Hide resolved
return "local"
}

// localTC is the credentials required to eatablish a local connection.
Copy link
Member

Choose a reason for hiding this comment

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

*establish

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]:"):
Copy link
Member

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?

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")
Copy link
Member

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

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}
Copy link
Member

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.

@dfawley dfawley assigned yihuazhang and unassigned dfawley Apr 22, 2020
@yihuazhang
Copy link
Contributor Author

Thanks @dfawley. All comments are addressed. PTAL

@yihuazhang yihuazhang removed their assignment Apr 22, 2020
@@ -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"}
Copy link
Member

@dfawley dfawley Apr 22, 2020

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:

  1. use local creds on client and server with a TCP dial to "localhost" that succeeds
  2. use local creds on client and server with a UDS connection that succeeds
  3. 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.

@menghanl
Copy link
Contributor

Ping, please fix the conflicts and add tests. Thanks

@yihuazhang
Copy link
Contributor Author

Sorry, I was distracted by another task. I will get back to it early next week.

@yihuazhang
Copy link
Contributor Author

Sorry for the long delay. I just added E2E tests, and PTAL.

@yihuazhang yihuazhang removed their assignment May 11, 2020
}
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)
Copy link
Member

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)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

@@ -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
Copy link
Member

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.

Copy link
Contributor Author

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.

go s.Serve(lis)

var cc *grpc.ClientConn
if network == "unix" {
Copy link
Member

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

Copy link
Contributor Author

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.


var cc *grpc.ClientConn
if network == "unix" {
cc, err = grpc.Dial("passthrough:///"+address, grpc.WithTransportCredentials(local.NewCredentials()), grpc.WithContextDialer(
Copy link
Member

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?

Copy link
Contributor Author

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.

}

func (s) TestLocalhost(t *testing.T) {
err := testE2ESucceed("tcp", "localhost:0")
Copy link
Member

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 {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

if err != nil {
return nil, err
}
return connWrapper{c, remoteAddrs[1]}, nil
Copy link
Member

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Aha, good suggestion!

Comment on lines 160 to 164
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))
}
Copy link
Member

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

Copy link
Contributor Author

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.

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

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.

Copy link
Contributor Author

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.

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

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?

Copy link
Contributor Author

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.

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") {
Copy link
Member

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

@dfawley dfawley assigned yihuazhang and unassigned dfawley May 13, 2020
@yihuazhang
Copy link
Contributor Author

All comments are addressed. PTAL.

@yihuazhang yihuazhang removed their assignment May 13, 2020
@dfawley
Copy link
Member

dfawley commented May 14, 2020

FYI: travis seems to be failing:

local_creds_test.go:216: testE2EFail() = rpc error: code = Unavailable desc = write tcp 127.0.0.1:34484->127.0.0.1:45229: write: broken pipe; want rpc error: code = Unavailable desc = connection closed

Seems like there's raciness determining what kind of error appears to the client when this happens.

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())
Copy link
Member

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?

Copy link
Contributor Author

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.

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()))
Copy link
Member

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?

Copy link
Contributor Author

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.

}

if err != nil {
return fmt.Errorf("Failed to dial server: %v, %v", err, lis.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.

same: address ?

Copy link
Contributor Author

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.

Comment on lines 196 to 199
if status.Code(got) == status.Code(want) && strings.Contains(status.Convert(got).Message(), status.Convert(want).Message()) {
return true
}
return false
Copy link
Member

Choose a reason for hiding this comment

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

Simplify: return <condition>

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@@ -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))
Copy link
Member

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.

if err != nil {
return fmt.Errorf("Failed to parse listener address: %v", err)
}
lisAddr = "localhost:" + port
Copy link
Member

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?

Comment on lines 220 to 221
want := status.Error(codes.Unavailable, "")
if err := testE2EFail(opts); !isExpected(err, want) {
Copy link
Member

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 {

@yihuazhang
Copy link
Contributor Author

Friendly ping @dfawley

testpb "google.golang.org/grpc/test/grpc_testing"
)

func testE2ESucceed(network, address string) error {
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

@dfawley dfawley changed the title Local credential implementations credentials: local creds implementation May 20, 2020
@dfawley dfawley merged commit 9eb3e7d into grpc:master May 20, 2020
@yihuazhang
Copy link
Contributor Author

Thanks a lot Doug for the detailed review! Really appreciate it.

yihuazhang added a commit to yihuazhang/grpc-go that referenced this pull request Jun 1, 2020
@github-actions github-actions bot locked as resolved and limited conversation to collaborators May 25, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Status: Requires Reporter Clarification Type: Feature New features or improvements in behavior
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants