diff --git a/go/vt/grpcoptionaltls/conn_wrapper.go b/go/vt/grpcoptionaltls/conn_wrapper.go new file mode 100755 index 00000000000..5659a9170f3 --- /dev/null +++ b/go/vt/grpcoptionaltls/conn_wrapper.go @@ -0,0 +1,38 @@ +/* +Copyright 2019 The Vitess Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package grpcoptionaltls + +import ( + "bytes" + "io" + "net" +) + +// WrappedConn imitates MSG_PEEK behaviour +// Unlike net.Conn is not thread-safe for reading already peeked bytes +type WrappedConn struct { + net.Conn + rd io.Reader +} + +func NewWrappedConn(conn net.Conn, peeked []byte) net.Conn { + var rd = io.MultiReader(bytes.NewReader(peeked), conn) + return &WrappedConn{ + Conn: conn, + rd: rd, + } +} + +func (wc *WrappedConn) Read(b []byte) (n int, err error) { + return wc.rd.Read(b) +} diff --git a/go/vt/grpcoptionaltls/optionaltls.go b/go/vt/grpcoptionaltls/optionaltls.go new file mode 100755 index 00000000000..c18f80412a6 --- /dev/null +++ b/go/vt/grpcoptionaltls/optionaltls.go @@ -0,0 +1,58 @@ +/* +Copyright 2019 The Vitess Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package grpcoptionaltls + +import ( + "net" + + "google.golang.org/grpc/credentials" +) + +type optionalTLSCreds struct { + credentials.TransportCredentials +} + +func (c *optionalTLSCreds) Clone() credentials.TransportCredentials { + return New(c.TransportCredentials.Clone()) +} + +func (c *optionalTLSCreds) ServerHandshake(conn net.Conn) (net.Conn, credentials.AuthInfo, error) { + isTLS, bytes, err := DetectTLS(conn) + if err != nil { + conn.Close() + return nil, nil, err + } + + var wc net.Conn = NewWrappedConn(conn, bytes) + if isTLS { + return c.TransportCredentials.ServerHandshake(wc) + } + + var authInfo = info{ + CommonAuthInfo: credentials.CommonAuthInfo{SecurityLevel: credentials.NoSecurity}, + } + + return wc, authInfo, nil +} + +func New(tc credentials.TransportCredentials) credentials.TransportCredentials { + return &optionalTLSCreds{TransportCredentials: tc} +} + +type info struct { + credentials.CommonAuthInfo +} + +func (info) AuthType() string { + return "insecure" +} diff --git a/go/vt/grpcoptionaltls/server_test.go b/go/vt/grpcoptionaltls/server_test.go new file mode 100755 index 00000000000..a0f6e6c8ea0 --- /dev/null +++ b/go/vt/grpcoptionaltls/server_test.go @@ -0,0 +1,127 @@ +/* +Copyright 2019 The Vitess Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package grpcoptionaltls + +import ( + "context" + "crypto/tls" + "io/ioutil" + "net" + "os" + "testing" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + pb "google.golang.org/grpc/examples/helloworld/helloworld" + + "vitess.io/vitess/go/vt/tlstest" +) + +// server is used to implement helloworld.GreeterServer. +type server struct { + pb.UnimplementedGreeterServer +} + +// SayHello implements helloworld.GreeterServer +func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { + return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil +} + +func createUnstartedServer(creds credentials.TransportCredentials) *grpc.Server { + s := grpc.NewServer(grpc.Creds(creds)) + pb.RegisterGreeterServer(s, &server{}) + return s +} + +type testCredentials struct { + client credentials.TransportCredentials + server credentials.TransportCredentials +} + +func createCredentials() (*testCredentials, error) { + // Create a temporary directory. + certDir, err := ioutil.TempDir("", "optionaltls_grpc_test") + if err != nil { + return nil, err + } + defer os.RemoveAll(certDir) + + certs := tlstest.CreateClientServerCertPairs(certDir) + cert, err := tls.LoadX509KeyPair(certs.ServerCert, certs.ServerKey) + if err != nil { + return nil, err + } + + clientCredentials, err := credentials.NewClientTLSFromFile(certs.ServerCA, certs.ServerName) + if err != nil { + return nil, err + } + tc := &testCredentials{ + client: clientCredentials, + server: credentials.NewServerTLSFromCert(&cert), + } + return tc, nil +} + +func TestOptionalTLS(t *testing.T) { + testCtx, testCancel := context.WithCancel(context.Background()) + defer testCancel() + + tc, err := createCredentials() + if err != nil { + t.Fatalf("failed to create credentials %v", err) + } + + lis, err := net.Listen("tcp", "") + if err != nil { + t.Fatalf("failed to listen %v", err) + } + defer lis.Close() + addr := lis.Addr().String() + + srv := createUnstartedServer(New(tc.server)) + go func() { + srv.Serve(lis) + }() + defer srv.Stop() + + testFunc := func(t *testing.T, dialOpt grpc.DialOption) { + ctx, cancel := context.WithTimeout(testCtx, 5*time.Second) + defer cancel() + conn, err := grpc.DialContext(ctx, addr, dialOpt) + if err != nil { + t.Fatalf("failed to connect to the server %v", err) + } + defer conn.Close() + c := pb.NewGreeterClient(conn) + resp, err := c.SayHello(ctx, &pb.HelloRequest{Name: "Vittes"}) + if err != nil { + t.Fatalf("could not greet: %v", err) + } + if resp.Message != "Hello Vittes" { + t.Fatalf("unexpected reply %s", resp.Message) + } + } + + t.Run("Plain2TLS", func(t *testing.T) { + for i := 0; i < 5; i += 1 { + testFunc(t, grpc.WithInsecure()) + } + }) + t.Run("TLS2TLS", func(t *testing.T) { + for i := 0; i < 5; i += 1 { + testFunc(t, grpc.WithTransportCredentials(tc.client)) + } + }) +} diff --git a/go/vt/grpcoptionaltls/tls_detector.go b/go/vt/grpcoptionaltls/tls_detector.go new file mode 100755 index 00000000000..beff6bfd740 --- /dev/null +++ b/go/vt/grpcoptionaltls/tls_detector.go @@ -0,0 +1,50 @@ +/* +Copyright 2019 The Vitess Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package grpcoptionaltls + +import "io" + +const TLSPeekedBytes = 6 + +func looksLikeTLS(bytes []byte) bool { + if len(bytes) < TLSPeekedBytes { + return false + } + // TLS starts as + // 0: 0x16 - handshake protocol magic + // 1: 0x03 - SSL version major + // 2: 0x00 to 0x03 - SSL version minor (SSLv3 or TLS1.0 through TLS1.3) + // 3-4: length (2 bytes) + // 5: 0x01 - handshake type (ClientHello) + // 6-8: handshake len (3 bytes), equals value from offset 3-4 minus 4 + // HTTP2 initial frame bytes + // https://tools.ietf.org/html/rfc7540#section-3.4 + + // Definitely not TLS + if bytes[0] != 0x16 || bytes[1] != 0x03 || bytes[5] != 0x01 { + return false + } + return true +} + +// DetectTLS reads necessary number of bytes from io.Reader +// returns result, bytes read from Reader and error +// No matter if error happens or what flag value is +// returned bytes should be checked +func DetectTLS(r io.Reader) (bool, []byte, error) { + var bytes = make([]byte, TLSPeekedBytes) + if n, err := io.ReadFull(r, bytes); err != nil { + return false, bytes[:n], err + } + return looksLikeTLS(bytes), bytes, nil +} diff --git a/go/vt/servenv/grpc_server.go b/go/vt/servenv/grpc_server.go index 98268a50942..feae9a0e6ef 100644 --- a/go/vt/servenv/grpc_server.go +++ b/go/vt/servenv/grpc_server.go @@ -35,6 +35,7 @@ import ( "context" "vitess.io/vitess/go/vt/grpccommon" + "vitess.io/vitess/go/vt/grpcoptionaltls" "vitess.io/vitess/go/vt/log" "vitess.io/vitess/go/vt/vttls" ) @@ -64,6 +65,8 @@ var ( // GRPCCA is the CA to use if TLS is enabled GRPCCA = flag.String("grpc_ca", "", "server CA to use for gRPC connections, requires TLS, and enforces client certificate check") + GRPCEnableOptionalTLS = flag.Bool("grpc_enable_optional_tls", false, "enable optional TLS mode when a server accepts both TLS and plain-text connections on the same port") + // GRPCServerCA if specified will combine server cert and server CA GRPCServerCA = flag.String("grpc_server_ca", "", "path to server CA in PEM format, which will be combine with server cert, return full certificate chain to clients") @@ -135,6 +138,10 @@ func createGRPCServer() { // create the creds server options creds := credentials.NewTLS(config) + if *GRPCEnableOptionalTLS { + log.Warning("Optional TLS is active. Plain-text connections will be accepted") + creds = grpcoptionaltls.New(creds) + } opts = []grpc.ServerOption{grpc.Creds(creds)} } // Override the default max message size for both send and receive