istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/networking/grpcgen/grpcgen_test.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package grpcgen_test
    16  
    17  import (
    18  	"context"
    19  	"encoding/json"
    20  	"fmt"
    21  	"net"
    22  	"net/url"
    23  	"path"
    24  	"strconv"
    25  	"strings"
    26  	"testing"
    27  	"time"
    28  
    29  	listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
    30  	statefulsession "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/stateful_session/v3"
    31  	hcm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
    32  	cookiev3 "github.com/envoyproxy/go-control-plane/envoy/extensions/http/stateful_session/cookie/v3"
    33  	discovery "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
    34  	"google.golang.org/grpc"
    35  	"google.golang.org/grpc/codes"
    36  	"google.golang.org/grpc/credentials/insecure"
    37  	xdscreds "google.golang.org/grpc/credentials/xds"
    38  	"google.golang.org/grpc/metadata"
    39  	"google.golang.org/grpc/resolver"
    40  	"google.golang.org/grpc/serviceconfig"
    41  	"google.golang.org/grpc/status"
    42  	xdsgrpc "google.golang.org/grpc/xds" // To install the xds resolvers and balancers.
    43  	"google.golang.org/protobuf/proto"
    44  
    45  	networking "istio.io/api/networking/v1alpha3"
    46  	security "istio.io/api/security/v1beta1"
    47  	"istio.io/istio/pilot/pkg/features"
    48  	"istio.io/istio/pilot/pkg/model"
    49  	"istio.io/istio/pilot/pkg/networking/util"
    50  	"istio.io/istio/pilot/pkg/serviceregistry/memory"
    51  	v3 "istio.io/istio/pilot/pkg/xds/v3"
    52  	"istio.io/istio/pilot/test/xds"
    53  	"istio.io/istio/pkg/config"
    54  	"istio.io/istio/pkg/config/constants"
    55  	"istio.io/istio/pkg/config/host"
    56  	"istio.io/istio/pkg/config/protocol"
    57  	"istio.io/istio/pkg/config/schema/gvk"
    58  	"istio.io/istio/pkg/istio-agent/grpcxds"
    59  	"istio.io/istio/pkg/log"
    60  	"istio.io/istio/pkg/test"
    61  	"istio.io/istio/pkg/test/echo/common"
    62  	echoproto "istio.io/istio/pkg/test/echo/proto"
    63  	"istio.io/istio/pkg/test/echo/server/endpoint"
    64  	"istio.io/istio/pkg/test/env"
    65  )
    66  
    67  // Address of the test gRPC service, used in tests.
    68  // Avoid using "istiod" as it is implicitly considered clusterLocal
    69  var testSvcHost = "test.istio-system.svc.cluster.local"
    70  
    71  // Local integration tests for proxyless gRPC.
    72  // The tests will start an in-process Istiod, using the memory store, and use
    73  // proxyless grpc servers and clients to validate the config generation.
    74  // GRPC project has more extensive tests for each language, we mainly verify that Istiod
    75  // generates the expected XDS, and gRPC tests verify that the XDS is correctly interpreted.
    76  //
    77  // To debug, set GRPC_GO_LOG_SEVERITY_LEVEL=info;GRPC_GO_LOG_VERBOSITY_LEVEL=99 for
    78  // verbose logs from gRPC side.
    79  
    80  // GRPCBootstrap creates the bootstrap bytes dynamically.
    81  // This can be used with NewXDSResolverWithConfigForTesting, and used when creating clients.
    82  //
    83  // See pkg/istio-agent/testdata/grpc-bootstrap.json for a sample bootstrap as expected by Istio agent.
    84  func GRPCBootstrap(app, namespace, ip string, xdsPort int) []byte {
    85  	if ip == "" {
    86  		ip = "127.0.0.1"
    87  	}
    88  	if namespace == "" {
    89  		namespace = "default"
    90  	}
    91  	if app == "" {
    92  		app = "app"
    93  	}
    94  	nodeID := "sidecar~" + ip + "~" + app + "." + namespace + "~" + namespace + ".svc.cluster.local"
    95  	bootstrap, err := grpcxds.GenerateBootstrap(grpcxds.GenerateBootstrapOptions{
    96  		Node: &model.Node{
    97  			ID: nodeID,
    98  			Metadata: &model.BootstrapNodeMetadata{
    99  				NodeMetadata: model.NodeMetadata{
   100  					Namespace: namespace,
   101  					Generator: "grpc",
   102  					ClusterID: constants.DefaultClusterName,
   103  				},
   104  			},
   105  		},
   106  		DiscoveryAddress: fmt.Sprintf("127.0.0.1:%d", xdsPort),
   107  		CertDir:          path.Join(env.IstioSrc, "tests/testdata/certs/default"),
   108  	})
   109  	if err != nil {
   110  		return []byte{}
   111  	}
   112  	bootstrapBytes, err := json.Marshal(bootstrap)
   113  	if err != nil {
   114  		return []byte{}
   115  	}
   116  	return bootstrapBytes
   117  }
   118  
   119  // resolverForTest creates a resolver for xds:// names using dynamic bootstrap.
   120  func resolverForTest(t test.Failer, xdsPort int, ns string) resolver.Builder {
   121  	xdsresolver, err := xdsgrpc.NewXDSResolverWithConfigForTesting(
   122  		GRPCBootstrap("foo", ns, "10.0.0.1", xdsPort))
   123  	if err != nil {
   124  		t.Fatal(err)
   125  	}
   126  	return xdsresolver
   127  }
   128  
   129  func init() {
   130  	// Setup gRPC logging. Do it once in init to avoid races
   131  	o := log.DefaultOptions()
   132  	o.SetDefaultOutputLevel(log.GrpcScopeName, log.DebugLevel)
   133  	log.Configure(o)
   134  }
   135  
   136  func TestGRPC(t *testing.T) {
   137  	// Init Istiod in-process server.
   138  	ds := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{
   139  		ListenerBuilder: func() (net.Listener, error) {
   140  			return net.Listen("tcp", "127.0.0.1:0")
   141  		},
   142  	})
   143  	sd := ds.MemRegistry
   144  
   145  	lis, err := net.Listen("tcp", ":0")
   146  	if err != nil {
   147  		t.Fatalf("net.Listen failed: %v", err)
   148  	}
   149  	_, ports, _ := net.SplitHostPort(lis.Addr().String())
   150  	port, _ := strconv.Atoi(ports)
   151  
   152  	// Echo service
   153  	// initRBACTests(sd, store, "echo-rbac-plain", 14058, false)
   154  	initRBACTests(sd, ds.Store(), "echo-rbac-mtls", port, true)
   155  	initPersistent(sd)
   156  
   157  	_, xdsPorts, _ := net.SplitHostPort(ds.Listener.Addr().String())
   158  	xdsPort, _ := strconv.Atoi(xdsPorts)
   159  
   160  	addIstiod(sd, xdsPort)
   161  
   162  	// Client bootstrap - will show as "foo.clientns"
   163  	xdsresolver := resolverForTest(t, xdsPort, "clientns")
   164  
   165  	// Test the xdsresolver - query LDS and RDS for a specific service, wait for the update.
   166  	// Should be very fast (~20ms) and validate bootstrap and basic XDS connection.
   167  	// Unfortunately we have no way to look at the response except using the logs from XDS.
   168  	// This does not attempt to resolve CDS or EDS.
   169  	t.Run("gRPC-resolve", func(t *testing.T) {
   170  		rb := xdsresolver
   171  		stateCh := make(chan resolver.State, 1)
   172  		errorCh := make(chan error, 1)
   173  		_, err := rb.Build(resolver.Target{URL: url.URL{
   174  			Scheme: "xds",
   175  			Path:   "/" + net.JoinHostPort(testSvcHost, xdsPorts),
   176  		}},
   177  			&testClientConn{stateCh: stateCh, errorCh: errorCh}, resolver.BuildOptions{
   178  				Authority: testSvcHost,
   179  			})
   180  		if err != nil {
   181  			t.Fatal("Failed to resolve XDS ", err)
   182  		}
   183  		tm := time.After(10 * time.Second)
   184  		select {
   185  		case s := <-stateCh:
   186  			t.Log("Got state ", s)
   187  		case e := <-errorCh:
   188  			t.Error("Error in resolve", e)
   189  		case <-tm:
   190  			t.Error("Didn't resolve in time")
   191  		}
   192  	})
   193  
   194  	t.Run("gRPC-svc", func(t *testing.T) {
   195  		t.Run("gRPC-svc-tls", func(t *testing.T) {
   196  			// Replaces: insecure.NewCredentials
   197  			creds, err := xdscreds.NewServerCredentials(xdscreds.ServerOptions{FallbackCreds: insecure.NewCredentials()})
   198  			if err != nil {
   199  				t.Fatal(err)
   200  			}
   201  
   202  			grpcOptions := []grpc.ServerOption{
   203  				grpc.Creds(creds),
   204  			}
   205  
   206  			bootstrapB := GRPCBootstrap("echo-rbac-mtls", "test", "127.0.1.1", xdsPort)
   207  			grpcOptions = append(grpcOptions, xdsgrpc.BootstrapContentsForTesting(bootstrapB))
   208  
   209  			// Replaces: grpc NewServer
   210  			grpcServer, err := xdsgrpc.NewGRPCServer(grpcOptions...)
   211  			if err != nil {
   212  				t.Fatal(err)
   213  			}
   214  
   215  			testRBAC(t, grpcServer, xdsresolver, "echo-rbac-mtls", port, lis)
   216  		})
   217  	})
   218  
   219  	t.Run("persistent", func(t *testing.T) {
   220  		proxy := ds.SetupProxy(&model.Proxy{Metadata: &model.NodeMetadata{
   221  			Generator: "grpc",
   222  		}})
   223  		adscConn := ds.Connect(proxy, []string{}, []string{})
   224  
   225  		adscConn.Send(&discovery.DiscoveryRequest{
   226  			TypeUrl: v3.ListenerType,
   227  		})
   228  
   229  		msg, err := adscConn.WaitVersion(5*time.Second, v3.ListenerType, "")
   230  		if err != nil {
   231  			t.Fatal("Failed to receive lds", err)
   232  		}
   233  		// Extract the cookie name from 4 layers of marshaling...
   234  		hcm := &hcm.HttpConnectionManager{}
   235  		ss := &statefulsession.StatefulSession{}
   236  		sc := &cookiev3.CookieBasedSessionState{}
   237  		filterIndex := -1
   238  		for _, rsc := range msg.Resources {
   239  			valBytes := rsc.Value
   240  			ll := &listener.Listener{}
   241  			_ = proto.Unmarshal(valBytes, ll)
   242  			if strings.HasPrefix(ll.Name, "echo-persistent.test.svc.cluster.local:") {
   243  				proto.Unmarshal(ll.ApiListener.ApiListener.Value, hcm)
   244  				for index, f := range hcm.HttpFilters {
   245  					if f.Name == util.StatefulSessionFilter {
   246  						proto.Unmarshal(f.GetTypedConfig().Value, ss)
   247  						filterIndex = index
   248  						if ss.GetSessionState().Name == "envoy.http.stateful_session.cookie" {
   249  							proto.Unmarshal(ss.GetSessionState().TypedConfig.Value, sc)
   250  						}
   251  					}
   252  				}
   253  			}
   254  		}
   255  		if sc.Cookie == nil {
   256  			t.Fatal("Failed to find session cookie")
   257  		}
   258  		if filterIndex == (len(hcm.HttpFilters) - 1) {
   259  			t.Fatal("session-cookie-filter cannot be the last filter!")
   260  		}
   261  		if sc.Cookie.Name != "test-cookie" {
   262  			t.Fatal("Missing expected cookie name", sc.Cookie)
   263  		}
   264  		if sc.Cookie.Path != "/Service/Method" {
   265  			t.Fatal("Missing expected cookie path", sc.Cookie)
   266  		}
   267  		clusterName := "outbound|9999||echo-persistent.test.svc.cluster.local"
   268  		adscConn.Send(&discovery.DiscoveryRequest{
   269  			TypeUrl:       v3.EndpointType,
   270  			ResourceNames: []string{clusterName},
   271  		})
   272  		_, err = adscConn.Wait(5*time.Second, v3.EndpointType)
   273  		if err != nil {
   274  			t.Fatal("Failed to receive endpoint", err)
   275  		}
   276  		ep := adscConn.GetEndpoints()[clusterName]
   277  		if ep == nil {
   278  			t.Fatal("Endpoints not found for persistent session cluster")
   279  		}
   280  		if len(ep.GetEndpoints()) == 0 {
   281  			t.Fatal("No endpoint not found for persistent session cluster")
   282  		}
   283  		lbep1 := ep.GetEndpoints()[0]
   284  		if lbep1.LbEndpoints[0].HealthStatus.Number() != 3 {
   285  			t.Fatal("Draining status not included")
   286  		}
   287  	})
   288  
   289  	t.Run("gRPC-dial", func(t *testing.T) {
   290  		for _, host := range []string{
   291  			testSvcHost,
   292  			//"istiod.istio-system.svc",
   293  			//"istiod.istio-system",
   294  			//"istiod",
   295  		} {
   296  			t.Run(host, func(t *testing.T) {
   297  				ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
   298  				defer cancel()
   299  				conn, err := grpc.DialContext(ctx, "xds:///"+host+":"+xdsPorts, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock(),
   300  					grpc.WithResolvers(xdsresolver))
   301  				if err != nil {
   302  					t.Fatal("XDS gRPC", err)
   303  				}
   304  				defer conn.Close()
   305  				s, err := discovery.NewAggregatedDiscoveryServiceClient(conn).StreamAggregatedResources(ctx)
   306  				if err != nil {
   307  					t.Fatal(err)
   308  				}
   309  				_ = s.Send(&discovery.DiscoveryRequest{})
   310  			})
   311  		}
   312  	})
   313  }
   314  
   315  func addIstiod(sd *memory.ServiceDiscovery, xdsPort int) {
   316  	sd.AddService(&model.Service{
   317  		Attributes: model.ServiceAttributes{
   318  			Name:      "istiod",
   319  			Namespace: "istio-system",
   320  		},
   321  		Hostname:       host.Name(testSvcHost),
   322  		DefaultAddress: "127.0.1.12",
   323  		Ports: model.PortList{
   324  			{
   325  				Name:     "grpc-main",
   326  				Port:     xdsPort,
   327  				Protocol: protocol.GRPC, // SetEndpoints hardcodes this
   328  			},
   329  		},
   330  	})
   331  	sd.SetEndpoints(testSvcHost, "istio-system", []*model.IstioEndpoint{
   332  		{
   333  			Address:         "127.0.0.1",
   334  			EndpointPort:    uint32(xdsPort),
   335  			ServicePortName: "grpc-main",
   336  		},
   337  	})
   338  }
   339  
   340  // initPersistent creates echo-persistent.test:9999, type GRPC with one drained endpoint
   341  func initPersistent(sd *memory.ServiceDiscovery) {
   342  	ns := "test"
   343  	svcname := "echo-persistent"
   344  	hn := svcname + "." + ns + ".svc.cluster.local"
   345  	sd.AddService(&model.Service{
   346  		Attributes: model.ServiceAttributes{
   347  			Name:      svcname,
   348  			Namespace: ns,
   349  			Labels:    map[string]string{features.PersistentSessionLabel: "test-cookie:/Service/Method"},
   350  		},
   351  		Hostname:       host.Name(hn),
   352  		DefaultAddress: "127.0.5.2",
   353  		Ports: model.PortList{
   354  			{
   355  				Name:     "grpc-main",
   356  				Port:     9999,
   357  				Protocol: protocol.GRPC,
   358  			},
   359  		},
   360  	})
   361  	sd.SetEndpoints(hn, ns, []*model.IstioEndpoint{
   362  		{
   363  			Address:         "127.0.1.2",
   364  			EndpointPort:    uint32(9999),
   365  			ServicePortName: "grpc-main",
   366  			HealthStatus:    model.Draining,
   367  		},
   368  	})
   369  }
   370  
   371  // initRBACTests creates a service with RBAC configs, to be associated with the inbound listeners.
   372  func initRBACTests(sd *memory.ServiceDiscovery, store model.ConfigStore, svcname string, port int, mtls bool) {
   373  	ns := "test"
   374  	hn := svcname + "." + ns + ".svc.cluster.local"
   375  	// The 'memory' store GetProxyServiceTargets uses the IP address of the node and endpoints to
   376  	// identify the service. In k8s store, labels are matched instead.
   377  	// For server configs to work, the server XDS bootstrap must match the IP.
   378  	sd.AddService(&model.Service{
   379  		// Required: namespace (otherwise DR matching fails)
   380  		Attributes: model.ServiceAttributes{
   381  			Name:      svcname,
   382  			Namespace: ns,
   383  		},
   384  		Hostname:       host.Name(hn),
   385  		DefaultAddress: "127.0.5.1",
   386  		Ports: model.PortList{
   387  			{
   388  				Name:     "grpc-main",
   389  				Port:     port,
   390  				Protocol: protocol.GRPC,
   391  			},
   392  		},
   393  	})
   394  	// The address will be matched against the INSTANCE_IPS and id in the node id. If they match, the service is returned.
   395  	sd.SetEndpoints(hn, ns, []*model.IstioEndpoint{
   396  		{
   397  			Address:         "127.0.1.1",
   398  			EndpointPort:    uint32(port),
   399  			ServicePortName: "grpc-main",
   400  		},
   401  	})
   402  
   403  	store.Create(config.Config{
   404  		Meta: config.Meta{
   405  			GroupVersionKind: gvk.AuthorizationPolicy,
   406  			Name:             svcname,
   407  			Namespace:        ns,
   408  		},
   409  		Spec: &security.AuthorizationPolicy{
   410  			Rules: []*security.Rule{
   411  				{
   412  					When: []*security.Condition{
   413  						{
   414  							Key: "request.headers[echo]",
   415  							Values: []string{
   416  								"block",
   417  							},
   418  						},
   419  					},
   420  				},
   421  			},
   422  			Action: security.AuthorizationPolicy_DENY,
   423  		},
   424  	})
   425  
   426  	store.Create(config.Config{
   427  		Meta: config.Meta{
   428  			GroupVersionKind: gvk.AuthorizationPolicy,
   429  			Name:             svcname + "-allow",
   430  			Namespace:        ns,
   431  		},
   432  		Spec: &security.AuthorizationPolicy{
   433  			Rules: []*security.Rule{
   434  				{
   435  					When: []*security.Condition{
   436  						{
   437  							Key: "request.headers[echo]",
   438  							Values: []string{
   439  								"allow",
   440  							},
   441  						},
   442  					},
   443  				},
   444  			},
   445  			Action: security.AuthorizationPolicy_ALLOW,
   446  		},
   447  	})
   448  	if mtls {
   449  		// Client side.
   450  		_, _ = store.Create(config.Config{
   451  			Meta: config.Meta{
   452  				GroupVersionKind: gvk.DestinationRule,
   453  				Name:             svcname,
   454  				Namespace:        "test",
   455  			},
   456  			Spec: &networking.DestinationRule{
   457  				Host: svcname + ".test.svc.cluster.local",
   458  				TrafficPolicy: &networking.TrafficPolicy{Tls: &networking.ClientTLSSettings{
   459  					Mode: networking.ClientTLSSettings_ISTIO_MUTUAL,
   460  				}},
   461  			},
   462  		})
   463  
   464  		// Server side.
   465  		_, _ = store.Create(config.Config{
   466  			Meta: config.Meta{
   467  				GroupVersionKind: gvk.PeerAuthentication,
   468  				Name:             svcname,
   469  				Namespace:        "test",
   470  			},
   471  			Spec: &security.PeerAuthentication{
   472  				Mtls: &security.PeerAuthentication_MutualTLS{Mode: security.PeerAuthentication_MutualTLS_STRICT},
   473  			},
   474  		})
   475  
   476  		_, _ = store.Create(config.Config{
   477  			Meta: config.Meta{
   478  				GroupVersionKind: gvk.AuthorizationPolicy,
   479  				Name:             svcname,
   480  				Namespace:        "test",
   481  			},
   482  			Spec: &security.AuthorizationPolicy{
   483  				Rules: []*security.Rule{
   484  					{
   485  						From: []*security.Rule_From{
   486  							{
   487  								Source: &security.Source{
   488  									Principals: []string{"evie"},
   489  								},
   490  							},
   491  						},
   492  					},
   493  				},
   494  				Action: security.AuthorizationPolicy_DENY,
   495  			},
   496  		})
   497  	}
   498  }
   499  
   500  func testRBAC(t *testing.T, grpcServer *xdsgrpc.GRPCServer, xdsresolver resolver.Builder, svcname string, port int, lis net.Listener) {
   501  	echos := &endpoint.EchoGrpcHandler{Config: endpoint.Config{Port: &common.Port{Port: port}}}
   502  	echoproto.RegisterEchoTestServiceServer(grpcServer, echos)
   503  
   504  	go func() {
   505  		err := grpcServer.Serve(lis)
   506  		if err != nil {
   507  			log.Error(err)
   508  		}
   509  	}()
   510  	ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
   511  	defer cancel()
   512  
   513  	creds, _ := xdscreds.NewClientCredentials(xdscreds.ClientOptions{
   514  		FallbackCreds: insecure.NewCredentials(),
   515  	})
   516  
   517  	conn, err := grpc.DialContext(ctx, fmt.Sprintf("xds:///%s.test.svc.cluster.local:%d", svcname, port),
   518  		grpc.WithTransportCredentials(creds),
   519  		grpc.WithBlock(),
   520  		grpc.WithResolvers(xdsresolver))
   521  	if err != nil {
   522  		t.Fatal("XDS gRPC", err)
   523  	}
   524  	defer conn.Close()
   525  	echoc := echoproto.NewEchoTestServiceClient(conn)
   526  	md := metadata.New(map[string]string{"echo": "block"})
   527  	outctx := metadata.NewOutgoingContext(context.Background(), md)
   528  	_, err = echoc.Echo(outctx, &echoproto.EchoRequest{})
   529  	if err == nil {
   530  		t.Fatal("RBAC rule not enforced")
   531  	}
   532  	if status.Code(err) != codes.PermissionDenied {
   533  		t.Fatal("Unexpected error", err)
   534  	}
   535  	t.Log(err)
   536  }
   537  
   538  // From xds_resolver_test
   539  // testClientConn is a fake implementation of resolver.ClientConn. All is does
   540  // is to store the state received from the resolver locally and signal that
   541  // event through a channel.
   542  type testClientConn struct {
   543  	resolver.ClientConn
   544  	stateCh chan resolver.State
   545  	errorCh chan error
   546  }
   547  
   548  func (t *testClientConn) UpdateState(s resolver.State) error {
   549  	t.stateCh <- s
   550  	return nil
   551  }
   552  
   553  func (t *testClientConn) ReportError(err error) {
   554  	t.errorCh <- err
   555  }
   556  
   557  func (t *testClientConn) ParseServiceConfig(jsonSC string) *serviceconfig.ParseResult {
   558  	// Will be called with something like:
   559  	// {"loadBalancingConfig":[
   560  	//  {"xds_cluster_manager_experimental":{
   561  	//     "children":{
   562  	//        "cluster:outbound|14057||istiod.istio-system.svc.cluster.local":{
   563  	//           "childPolicy":[
   564  	//          {"cds_experimental":
   565  	//         		{"cluster":"outbound|14057||istiod.istio-system.svc.cluster.local"}}]}}}}]}
   566  	return &serviceconfig.ParseResult{}
   567  }