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 }