mirror of
https://github.com/fullstorydev/grpcurl.git
synced 2026-06-11 13:31:46 +03:00
This PR allows TLS 1.3, by removing the MaxVersion in the client config. This would silently swallow errors, so e.g. a client without cert dialing a server that requires client certs would lead to an error which gets ignored, leading to retries until timeout. In this PR, we wrap the connection and if an error occurs we send it to the existing `result` channel. I think this matches @jhump's comment in https://github.com/fullstorydev/grpcurl/issues/387#issuecomment-1517098394 **Testing** ```console # Start the test server (in another tab) go run ./internal/testing/cmd/testserver \ -cert internal/testing/tls/server.crt \ -key internal/testing/tls/server.key \ -cacert internal/testing/tls/ca.crt \ -requirecert -p 9999 # Old behavior $ grpcurl -cacert internal/testing/tls/ca.crt \ localhost:9999 list Failed to dial target host "localhost:9999": context deadline exceeded # New behavior $ go run ./cmd/grpcurl -cacert internal/testing/tls/ca.crt \ localhost:9999 list Failed to dial target host "localhost:9999": remote error: tls: certificate required exit status 1 ``` The old behavior is to hang until we hit the deadline. The new behavior is to return immediately with an error. Fixes #563
438 lines
14 KiB
Go
438 lines
14 KiB
Go
package grpcurl_test
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"net"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials"
|
|
"google.golang.org/grpc/peer"
|
|
|
|
. "github.com/fullstorydev/grpcurl"
|
|
grpcurl_testing "github.com/fullstorydev/grpcurl/internal/testing"
|
|
)
|
|
|
|
func TestPlainText(t *testing.T) {
|
|
e, err := createTestServerAndClient(nil, nil)
|
|
if err != nil {
|
|
t.Fatalf("failed to setup server and client: %v", err)
|
|
}
|
|
defer e.Close()
|
|
|
|
simpleTest(t, e.cc)
|
|
}
|
|
|
|
func TestBasicTLS(t *testing.T) {
|
|
serverCreds, err := ServerTransportCredentials("", "internal/testing/tls/server.crt", "internal/testing/tls/server.key", false)
|
|
if err != nil {
|
|
t.Fatalf("failed to create server creds: %v", err)
|
|
}
|
|
clientCreds, err := ClientTransportCredentials(false, "internal/testing/tls/ca.crt", "", "")
|
|
if err != nil {
|
|
t.Fatalf("failed to create server creds: %v", err)
|
|
}
|
|
|
|
e, err := createTestServerAndClient(serverCreds, clientCreds)
|
|
if err != nil {
|
|
t.Fatalf("failed to setup server and client: %v", err)
|
|
}
|
|
defer e.Close()
|
|
|
|
simpleTest(t, e.cc)
|
|
}
|
|
|
|
func TestInsecureClientTLS(t *testing.T) {
|
|
serverCreds, err := ServerTransportCredentials("", "internal/testing/tls/server.crt", "internal/testing/tls/server.key", false)
|
|
if err != nil {
|
|
t.Fatalf("failed to create server creds: %v", err)
|
|
}
|
|
clientCreds, err := ClientTransportCredentials(true, "", "", "")
|
|
if err != nil {
|
|
t.Fatalf("failed to create server creds: %v", err)
|
|
}
|
|
|
|
e, err := createTestServerAndClient(serverCreds, clientCreds)
|
|
if err != nil {
|
|
t.Fatalf("failed to setup server and client: %v", err)
|
|
}
|
|
defer e.Close()
|
|
|
|
simpleTest(t, e.cc)
|
|
}
|
|
|
|
func TestClientCertTLS(t *testing.T) {
|
|
serverCreds, err := ServerTransportCredentials("internal/testing/tls/ca.crt", "internal/testing/tls/server.crt", "internal/testing/tls/server.key", false)
|
|
if err != nil {
|
|
t.Fatalf("failed to create server creds: %v", err)
|
|
}
|
|
clientCreds, err := ClientTransportCredentials(false, "internal/testing/tls/ca.crt", "internal/testing/tls/client.crt", "internal/testing/tls/client.key")
|
|
if err != nil {
|
|
t.Fatalf("failed to create server creds: %v", err)
|
|
}
|
|
|
|
e, err := createTestServerAndClient(serverCreds, clientCreds)
|
|
if err != nil {
|
|
t.Fatalf("failed to setup server and client: %v", err)
|
|
}
|
|
defer e.Close()
|
|
|
|
simpleTest(t, e.cc)
|
|
}
|
|
|
|
func TestRequireClientCertTLS(t *testing.T) {
|
|
serverCreds, err := ServerTransportCredentials("internal/testing/tls/ca.crt", "internal/testing/tls/server.crt", "internal/testing/tls/server.key", true)
|
|
if err != nil {
|
|
t.Fatalf("failed to create server creds: %v", err)
|
|
}
|
|
clientCreds, err := ClientTransportCredentials(false, "internal/testing/tls/ca.crt", "internal/testing/tls/client.crt", "internal/testing/tls/client.key")
|
|
if err != nil {
|
|
t.Fatalf("failed to create server creds: %v", err)
|
|
}
|
|
|
|
e, err := createTestServerAndClient(serverCreds, clientCreds)
|
|
if err != nil {
|
|
t.Fatalf("failed to setup server and client: %v", err)
|
|
}
|
|
defer e.Close()
|
|
|
|
simpleTest(t, e.cc)
|
|
}
|
|
|
|
func TestTLS12(t *testing.T) {
|
|
serverCreds, err := ServerTransportCredentials("", "internal/testing/tls/server.crt", "internal/testing/tls/server.key", false)
|
|
if err != nil {
|
|
t.Fatalf("failed to create server creds: %v", err)
|
|
}
|
|
tlsConf, err := ClientTLSConfig(false, "internal/testing/tls/ca.crt", "", "")
|
|
if err != nil {
|
|
t.Fatalf("failed to create client TLS config: %v", err)
|
|
}
|
|
tlsConf.MaxVersion = tls.VersionTLS12
|
|
|
|
e, err := createTestServerAndClient(serverCreds, credentials.NewTLS(tlsConf))
|
|
if err != nil {
|
|
t.Fatalf("failed to setup server and client: %v", err)
|
|
}
|
|
defer e.Close()
|
|
|
|
tlsVersion := negotiatedTLSVersion(t, e.cc)
|
|
if tlsVersion != tls.VersionTLS12 {
|
|
t.Errorf("expected TLS 1.2, got 0x%04x", tlsVersion)
|
|
}
|
|
}
|
|
|
|
func TestTLS13(t *testing.T) {
|
|
serverCreds, err := ServerTransportCredentials("", "internal/testing/tls/server.crt", "internal/testing/tls/server.key", false)
|
|
if err != nil {
|
|
t.Fatalf("failed to create server creds: %v", err)
|
|
}
|
|
clientCreds, err := ClientTransportCredentials(false, "internal/testing/tls/ca.crt", "", "")
|
|
if err != nil {
|
|
t.Fatalf("failed to create client creds: %v", err)
|
|
}
|
|
|
|
e, err := createTestServerAndClient(serverCreds, clientCreds)
|
|
if err != nil {
|
|
t.Fatalf("failed to setup server and client: %v", err)
|
|
}
|
|
defer e.Close()
|
|
|
|
tlsVersion := negotiatedTLSVersion(t, e.cc)
|
|
if tlsVersion != tls.VersionTLS13 {
|
|
t.Errorf("expected TLS 1.3, got 0x%04x", tlsVersion)
|
|
}
|
|
}
|
|
|
|
func negotiatedTLSVersion(t *testing.T, cc *grpc.ClientConn) uint16 {
|
|
t.Helper()
|
|
cl := grpcurl_testing.NewTestServiceClient(cc)
|
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
|
defer cancel()
|
|
var p peer.Peer
|
|
_, err := cl.UnaryCall(ctx, &grpcurl_testing.SimpleRequest{}, grpc.WaitForReady(true), grpc.Peer(&p))
|
|
if err != nil {
|
|
t.Fatalf("RPC failed: %v", err)
|
|
}
|
|
tlsInfo, ok := p.AuthInfo.(credentials.TLSInfo)
|
|
if !ok {
|
|
t.Fatalf("expected TLS auth info, got %T", p.AuthInfo)
|
|
}
|
|
return tlsInfo.State.Version
|
|
}
|
|
|
|
func TestBrokenTLS_ClientPlainText(t *testing.T) {
|
|
serverCreds, err := ServerTransportCredentials("", "internal/testing/tls/server.crt", "internal/testing/tls/server.key", false)
|
|
if err != nil {
|
|
t.Fatalf("failed to create server creds: %v", err)
|
|
}
|
|
|
|
// client connection (usually) succeeds since client is not waiting for TLS handshake
|
|
// (we try several times, but if we never get a connection and the error message is
|
|
// a known/expected possibility, we'll just bail)
|
|
var e testEnv
|
|
failCount := 0
|
|
for {
|
|
e, err = createTestServerAndClient(serverCreds, nil)
|
|
if err == nil {
|
|
// success!
|
|
defer e.Close()
|
|
break
|
|
}
|
|
|
|
if strings.Contains(err.Error(), "deadline exceeded") ||
|
|
strings.Contains(err.Error(), "use of closed network connection") {
|
|
// It is possible that the connection never becomes healthy:
|
|
// 1) grpc connects successfully
|
|
// 2) grpc client tries to send HTTP/2 preface and settings frame
|
|
// 3) server, expecting handshake, closes the connection
|
|
// 4) in the client, the write fails, so the connection never
|
|
// becomes ready
|
|
// The client will attempt to reconnect on transient errors, so
|
|
// may eventually bump into the connect time limit. This used to
|
|
// result in a "deadline exceeded" error, but more recent versions
|
|
// of the grpc library report any underlying I/O error instead, so
|
|
// we also check for "use of closed network connection".
|
|
failCount++
|
|
if failCount > 5 {
|
|
return // bail...
|
|
}
|
|
// we'll try again
|
|
|
|
} else {
|
|
// some other error occurred, so we'll consider that a test failure
|
|
t.Fatalf("failed to setup server and client: %v", err)
|
|
}
|
|
}
|
|
|
|
// but request fails because server closes connection upon seeing request
|
|
// bytes that are not a TLS handshake
|
|
cl := grpcurl_testing.NewTestServiceClient(e.cc)
|
|
_, err = cl.UnaryCall(context.Background(), &grpcurl_testing.SimpleRequest{})
|
|
if err == nil {
|
|
t.Fatal("expecting failure")
|
|
}
|
|
// various errors possible when server closes connection
|
|
if !strings.Contains(err.Error(), "transport is closing") &&
|
|
!strings.Contains(err.Error(), "connection is unavailable") &&
|
|
!strings.Contains(err.Error(), "use of closed network connection") &&
|
|
!strings.Contains(err.Error(), "all SubConns are in TransientFailure") {
|
|
|
|
t.Fatalf("expecting transport failure, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestBrokenTLS_ServerPlainText(t *testing.T) {
|
|
clientCreds, err := ClientTransportCredentials(false, "internal/testing/tls/ca.crt", "", "")
|
|
if err != nil {
|
|
t.Fatalf("failed to create server creds: %v", err)
|
|
}
|
|
|
|
e, err := createTestServerAndClient(nil, clientCreds)
|
|
if err == nil {
|
|
e.Close()
|
|
t.Fatal("expecting TLS failure setting up server and client")
|
|
}
|
|
if !strings.Contains(err.Error(), "first record does not look like a TLS handshake") {
|
|
t.Fatalf("expecting TLS handshake failure, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestBrokenTLS_ServerUsesWrongCert(t *testing.T) {
|
|
serverCreds, err := ServerTransportCredentials("", "internal/testing/tls/other.crt", "internal/testing/tls/other.key", false)
|
|
if err != nil {
|
|
t.Fatalf("failed to create server creds: %v", err)
|
|
}
|
|
clientCreds, err := ClientTransportCredentials(false, "internal/testing/tls/ca.crt", "", "")
|
|
if err != nil {
|
|
t.Fatalf("failed to create server creds: %v", err)
|
|
}
|
|
|
|
e, err := createTestServerAndClient(serverCreds, clientCreds)
|
|
if err == nil {
|
|
e.Close()
|
|
t.Fatal("expecting TLS failure setting up server and client")
|
|
}
|
|
if !strings.Contains(err.Error(), "certificate is valid for") {
|
|
t.Fatalf("expecting TLS certificate error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestBrokenTLS_ClientHasExpiredCert(t *testing.T) {
|
|
serverCreds, err := ServerTransportCredentials("internal/testing/tls/ca.crt", "internal/testing/tls/server.crt", "internal/testing/tls/server.key", false)
|
|
if err != nil {
|
|
t.Fatalf("failed to create server creds: %v", err)
|
|
}
|
|
clientCreds, err := ClientTransportCredentials(false, "internal/testing/tls/ca.crt", "internal/testing/tls/expired.crt", "internal/testing/tls/expired.key")
|
|
if err != nil {
|
|
t.Fatalf("failed to create server creds: %v", err)
|
|
}
|
|
|
|
e, err := createTestServerAndClient(serverCreds, clientCreds)
|
|
if err == nil {
|
|
e.Close()
|
|
t.Fatal("expecting TLS failure setting up server and client")
|
|
}
|
|
if !strings.Contains(err.Error(), "certificate") {
|
|
t.Fatalf("expecting TLS certificate error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestBrokenTLS_ServerHasExpiredCert(t *testing.T) {
|
|
serverCreds, err := ServerTransportCredentials("", "internal/testing/tls/expired.crt", "internal/testing/tls/expired.key", false)
|
|
if err != nil {
|
|
t.Fatalf("failed to create server creds: %v", err)
|
|
}
|
|
clientCreds, err := ClientTransportCredentials(false, "internal/testing/tls/ca.crt", "", "")
|
|
if err != nil {
|
|
t.Fatalf("failed to create server creds: %v", err)
|
|
}
|
|
|
|
e, err := createTestServerAndClient(serverCreds, clientCreds)
|
|
if err == nil {
|
|
e.Close()
|
|
t.Fatal("expecting TLS failure setting up server and client")
|
|
}
|
|
if !strings.Contains(err.Error(), "certificate has expired or is not yet valid") {
|
|
t.Fatalf("expecting TLS certificate expired, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestBrokenTLS_ClientNotTrusted(t *testing.T) {
|
|
serverCreds, err := ServerTransportCredentials("internal/testing/tls/ca.crt", "internal/testing/tls/server.crt", "internal/testing/tls/server.key", true)
|
|
if err != nil {
|
|
t.Fatalf("failed to create server creds: %v", err)
|
|
}
|
|
clientCreds, err := ClientTransportCredentials(false, "internal/testing/tls/ca.crt", "internal/testing/tls/wrong-client.crt", "internal/testing/tls/wrong-client.key")
|
|
if err != nil {
|
|
t.Fatalf("failed to create server creds: %v", err)
|
|
}
|
|
|
|
e, err := createTestServerAndClient(serverCreds, clientCreds)
|
|
if err == nil {
|
|
e.Close()
|
|
t.Fatal("expecting TLS failure setting up server and client")
|
|
}
|
|
// The exact TLS alert varies by Go version and TLS version negotiated:
|
|
// - TLS 1.2: "bad certificate" (Go <=1.24) or "handshake failure" (Go 1.25+)
|
|
// - TLS 1.3: "certificate required" (server rejects after handshake)
|
|
errMsg := err.Error()
|
|
if !strings.Contains(errMsg, "bad certificate") &&
|
|
!strings.Contains(errMsg, "handshake failure") &&
|
|
!strings.Contains(errMsg, "certificate required") {
|
|
t.Fatalf("expecting a TLS certificate error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestBrokenTLS_ServerNotTrusted(t *testing.T) {
|
|
serverCreds, err := ServerTransportCredentials("", "internal/testing/tls/server.crt", "internal/testing/tls/server.key", false)
|
|
if err != nil {
|
|
t.Fatalf("failed to create server creds: %v", err)
|
|
}
|
|
clientCreds, err := ClientTransportCredentials(false, "", "internal/testing/tls/client.crt", "internal/testing/tls/client.key")
|
|
if err != nil {
|
|
t.Fatalf("failed to create server creds: %v", err)
|
|
}
|
|
|
|
e, err := createTestServerAndClient(serverCreds, clientCreds)
|
|
if err == nil {
|
|
e.Close()
|
|
t.Fatal("expecting TLS failure setting up server and client")
|
|
}
|
|
if !strings.Contains(err.Error(), "certificate") {
|
|
t.Fatalf("expecting TLS certificate error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestBrokenTLS_RequireClientCertButNonePresented(t *testing.T) {
|
|
serverCreds, err := ServerTransportCredentials("internal/testing/tls/ca.crt", "internal/testing/tls/server.crt", "internal/testing/tls/server.key", true)
|
|
if err != nil {
|
|
t.Fatalf("failed to create server creds: %v", err)
|
|
}
|
|
clientCreds, err := ClientTransportCredentials(false, "internal/testing/tls/ca.crt", "", "")
|
|
if err != nil {
|
|
t.Fatalf("failed to create server creds: %v", err)
|
|
}
|
|
|
|
e, err := createTestServerAndClient(serverCreds, clientCreds)
|
|
if err == nil {
|
|
e.Close()
|
|
t.Fatal("expecting TLS failure setting up server and client")
|
|
}
|
|
// The exact TLS alert varies by Go version and TLS version negotiated:
|
|
// - TLS 1.2: "bad certificate" (Go <=1.24) or "handshake failure" (Go 1.25+)
|
|
// - TLS 1.3: "certificate required" (server rejects after handshake)
|
|
errMsg := err.Error()
|
|
if !strings.Contains(errMsg, "bad certificate") &&
|
|
!strings.Contains(errMsg, "handshake failure") &&
|
|
!strings.Contains(errMsg, "certificate required") {
|
|
t.Fatalf("expecting a TLS certificate error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func simpleTest(t *testing.T, cc *grpc.ClientConn) {
|
|
cl := grpcurl_testing.NewTestServiceClient(cc)
|
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
|
defer cancel()
|
|
_, err := cl.UnaryCall(ctx, &grpcurl_testing.SimpleRequest{}, grpc.WaitForReady(true))
|
|
if err != nil {
|
|
t.Errorf("simple RPC failed: %v", err)
|
|
}
|
|
}
|
|
|
|
func createTestServerAndClient(serverCreds, clientCreds credentials.TransportCredentials) (testEnv, error) {
|
|
var e testEnv
|
|
completed := false
|
|
defer func() {
|
|
if !completed {
|
|
e.Close()
|
|
}
|
|
}()
|
|
|
|
var svrOpts []grpc.ServerOption
|
|
if serverCreds != nil {
|
|
svrOpts = []grpc.ServerOption{grpc.Creds(serverCreds)}
|
|
}
|
|
svr := grpc.NewServer(svrOpts...)
|
|
grpcurl_testing.RegisterTestServiceServer(svr, grpcurl_testing.TestServer{})
|
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
return e, err
|
|
}
|
|
port := l.Addr().(*net.TCPAddr).Port
|
|
go svr.Serve(l)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
|
defer cancel()
|
|
|
|
cc, err := BlockingDial(ctx, "tcp", fmt.Sprintf("127.0.0.1:%d", port), clientCreds)
|
|
if err != nil {
|
|
return e, err
|
|
}
|
|
|
|
e.svr = svr
|
|
e.cc = cc
|
|
completed = true
|
|
return e, nil
|
|
}
|
|
|
|
type testEnv struct {
|
|
svr *grpc.Server
|
|
cc *grpc.ClientConn
|
|
}
|
|
|
|
func (e *testEnv) Close() {
|
|
if e.cc != nil {
|
|
e.cc.Close()
|
|
e.cc = nil
|
|
}
|
|
if e.svr != nil {
|
|
e.svr.GracefulStop()
|
|
e.svr = nil
|
|
}
|
|
}
|