istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/networking/core/sidecar_simulation_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 core_test 16 17 import ( 18 "encoding/json" 19 "fmt" 20 "reflect" 21 "sort" 22 "strings" 23 "testing" 24 25 cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" 26 endpoint "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3" 27 listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" 28 tls "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" 29 "k8s.io/apimachinery/pkg/types" 30 31 meshconfig "istio.io/api/mesh/v1alpha1" 32 networking "istio.io/api/networking/v1alpha3" 33 "istio.io/istio/pilot/pkg/config/kube/crd" 34 "istio.io/istio/pilot/pkg/features" 35 "istio.io/istio/pilot/pkg/model" 36 "istio.io/istio/pilot/pkg/networking/core" 37 "istio.io/istio/pilot/pkg/networking/util" 38 "istio.io/istio/pilot/pkg/simulation" 39 "istio.io/istio/pilot/test/xds" 40 "istio.io/istio/pilot/test/xdstest" 41 "istio.io/istio/pkg/config" 42 "istio.io/istio/pkg/config/host" 43 "istio.io/istio/pkg/config/mesh" 44 "istio.io/istio/pkg/config/protocol" 45 "istio.io/istio/pkg/config/schema/gvk" 46 "istio.io/istio/pkg/kube" 47 "istio.io/istio/pkg/test" 48 "istio.io/istio/pkg/test/util/tmpl" 49 "istio.io/istio/pkg/util/protomarshal" 50 ) 51 52 func flattenInstances(il ...[]*model.ServiceInstance) []*model.ServiceInstance { 53 ret := []*model.ServiceInstance{} 54 for _, i := range il { 55 ret = append(ret, i...) 56 } 57 return ret 58 } 59 60 func makeInstances(proxy *model.Proxy, svc *model.Service, servicePort int, targetPort int) []*model.ServiceInstance { 61 ret := []*model.ServiceInstance{} 62 for _, p := range svc.Ports { 63 if p.Port != servicePort { 64 continue 65 } 66 ret = append(ret, &model.ServiceInstance{ 67 Service: svc, 68 ServicePort: p, 69 Endpoint: &model.IstioEndpoint{ 70 Address: proxy.IPAddresses[0], 71 ServicePortName: p.Name, 72 EndpointPort: uint32(targetPort), 73 }, 74 }) 75 } 76 return ret 77 } 78 79 func TestInboundClusters(t *testing.T) { 80 proxy := &model.Proxy{ 81 IPAddresses: []string{"1.2.3.4"}, 82 Metadata: &model.NodeMetadata{}, 83 } 84 service := &model.Service{ 85 Hostname: host.Name("backend.default.svc.cluster.local"), 86 DefaultAddress: "1.1.1.1", 87 Ports: model.PortList{&model.Port{ 88 Name: "default", 89 Port: 80, 90 Protocol: protocol.HTTP, 91 }, &model.Port{ 92 Name: "other", 93 Port: 81, 94 Protocol: protocol.HTTP, 95 }}, 96 Resolution: model.ClientSideLB, 97 } 98 serviceAlt := &model.Service{ 99 Hostname: host.Name("backend-alt.default.svc.cluster.local"), 100 DefaultAddress: "1.1.1.2", 101 Ports: model.PortList{&model.Port{ 102 Name: "default", 103 Port: 80, 104 Protocol: protocol.HTTP, 105 }, &model.Port{ 106 Name: "other", 107 Port: 81, 108 Protocol: protocol.HTTP, 109 }}, 110 Resolution: model.ClientSideLB, 111 } 112 113 cases := []struct { 114 name string 115 configs []config.Config 116 services []*model.Service 117 instances []*model.ServiceInstance 118 // Assertions 119 clusters map[string][]string 120 telemetry map[string][]string 121 proxy *model.Proxy 122 disableInboundPassthrough bool 123 }{ 124 // Proxy 1.8.1+ tests 125 {name: "empty"}, 126 {name: "empty service", services: []*model.Service{service}}, 127 { 128 name: "single service, partial instance", 129 services: []*model.Service{service}, 130 instances: makeInstances(proxy, service, 80, 8080), 131 clusters: map[string][]string{ 132 "inbound|8080||": nil, 133 }, 134 telemetry: map[string][]string{ 135 "inbound|8080||": {string(service.Hostname)}, 136 }, 137 }, 138 { 139 name: "single service, multiple instance", 140 services: []*model.Service{service}, 141 instances: flattenInstances( 142 makeInstances(proxy, service, 80, 8080), 143 makeInstances(proxy, service, 81, 8081)), 144 clusters: map[string][]string{ 145 "inbound|8080||": nil, 146 "inbound|8081||": nil, 147 }, 148 telemetry: map[string][]string{ 149 "inbound|8080||": {string(service.Hostname)}, 150 "inbound|8081||": {string(service.Hostname)}, 151 }, 152 }, 153 { 154 name: "multiple services with same service port, different target", 155 services: []*model.Service{service, serviceAlt}, 156 instances: flattenInstances( 157 makeInstances(proxy, service, 80, 8080), 158 makeInstances(proxy, service, 81, 8081), 159 makeInstances(proxy, serviceAlt, 80, 8082), 160 makeInstances(proxy, serviceAlt, 81, 8083)), 161 clusters: map[string][]string{ 162 "inbound|8080||": nil, 163 "inbound|8081||": nil, 164 "inbound|8082||": nil, 165 "inbound|8083||": nil, 166 }, 167 telemetry: map[string][]string{ 168 "inbound|8080||": {string(service.Hostname)}, 169 "inbound|8081||": {string(service.Hostname)}, 170 "inbound|8082||": {string(serviceAlt.Hostname)}, 171 "inbound|8083||": {string(serviceAlt.Hostname)}, 172 }, 173 }, 174 { 175 name: "multiple services with same service port and target", 176 services: []*model.Service{service, serviceAlt}, 177 instances: flattenInstances( 178 makeInstances(proxy, service, 80, 8080), 179 makeInstances(proxy, service, 81, 8081), 180 makeInstances(proxy, serviceAlt, 80, 8080), 181 makeInstances(proxy, serviceAlt, 81, 8081)), 182 clusters: map[string][]string{ 183 "inbound|8080||": nil, 184 "inbound|8081||": nil, 185 }, 186 telemetry: map[string][]string{ 187 "inbound|8080||": {string(serviceAlt.Hostname), string(service.Hostname)}, 188 "inbound|8081||": {string(serviceAlt.Hostname), string(service.Hostname)}, 189 }, 190 }, 191 { 192 name: "ingress to same port", 193 configs: []config.Config{ 194 { 195 Meta: config.Meta{GroupVersionKind: gvk.Sidecar, Namespace: "default", Name: "sidecar"}, 196 Spec: &networking.Sidecar{Ingress: []*networking.IstioIngressListener{{ 197 Port: &networking.SidecarPort{ 198 Number: 80, 199 Protocol: "HTTP", 200 Name: "http", 201 }, 202 DefaultEndpoint: "127.0.0.1:80", 203 }}}, 204 }, 205 }, 206 clusters: map[string][]string{ 207 "inbound|80||": {"127.0.0.1:80"}, 208 }, 209 }, 210 { 211 name: "ingress to different port", 212 configs: []config.Config{ 213 { 214 Meta: config.Meta{GroupVersionKind: gvk.Sidecar, Namespace: "default", Name: "sidecar"}, 215 Spec: &networking.Sidecar{Ingress: []*networking.IstioIngressListener{{ 216 Port: &networking.SidecarPort{ 217 Number: 80, 218 Protocol: "HTTP", 219 Name: "http", 220 }, 221 DefaultEndpoint: "127.0.0.1:8080", 222 }}}, 223 }, 224 }, 225 clusters: map[string][]string{ 226 "inbound|80||": {"127.0.0.1:8080"}, 227 }, 228 }, 229 { 230 name: "ingress to instance IP", 231 configs: []config.Config{ 232 { 233 Meta: config.Meta{GroupVersionKind: gvk.Sidecar, Namespace: "default", Name: "sidecar"}, 234 Spec: &networking.Sidecar{Ingress: []*networking.IstioIngressListener{{ 235 Port: &networking.SidecarPort{ 236 Number: 80, 237 Protocol: "HTTP", 238 Name: "http", 239 }, 240 DefaultEndpoint: "0.0.0.0:8080", 241 }}}, 242 }, 243 }, 244 clusters: map[string][]string{ 245 "inbound|80||": {"1.2.3.4:8080"}, 246 }, 247 }, 248 { 249 name: "ingress without default endpoint", 250 configs: []config.Config{ 251 { 252 Meta: config.Meta{GroupVersionKind: gvk.Sidecar, Namespace: "default", Name: "sidecar"}, 253 Spec: &networking.Sidecar{Ingress: []*networking.IstioIngressListener{{ 254 Port: &networking.SidecarPort{ 255 Number: 80, 256 Protocol: "HTTP", 257 Name: "http", 258 }, 259 }}}, 260 }, 261 }, 262 clusters: map[string][]string{ 263 "inbound|80||": nil, 264 }, 265 }, 266 { 267 name: "ingress to socket", 268 configs: []config.Config{ 269 { 270 Meta: config.Meta{GroupVersionKind: gvk.Sidecar, Namespace: "default", Name: "sidecar"}, 271 Spec: &networking.Sidecar{Ingress: []*networking.IstioIngressListener{{ 272 Port: &networking.SidecarPort{ 273 Number: 80, 274 Protocol: "HTTP", 275 Name: "http", 276 }, 277 DefaultEndpoint: "unix:///socket", 278 }}}, 279 }, 280 }, 281 clusters: map[string][]string{ 282 "inbound|80||": {"/socket"}, 283 }, 284 }, 285 { 286 name: "multiple ingress", 287 configs: []config.Config{ 288 { 289 Meta: config.Meta{GroupVersionKind: gvk.Sidecar, Namespace: "default", Name: "sidecar"}, 290 Spec: &networking.Sidecar{Ingress: []*networking.IstioIngressListener{ 291 { 292 Port: &networking.SidecarPort{ 293 Number: 80, 294 Protocol: "HTTP", 295 Name: "http", 296 }, 297 DefaultEndpoint: "127.0.0.1:8080", 298 }, 299 { 300 Port: &networking.SidecarPort{ 301 Number: 81, 302 Protocol: "HTTP", 303 Name: "http", 304 }, 305 DefaultEndpoint: "127.0.0.1:8080", 306 }, 307 }}, 308 }, 309 }, 310 clusters: map[string][]string{ 311 "inbound|80||": {"127.0.0.1:8080"}, 312 "inbound|81||": {"127.0.0.1:8080"}, 313 }, 314 }, 315 // Disable inbound passthrough 316 { 317 name: "single service, partial instance", 318 services: []*model.Service{service}, 319 instances: makeInstances(proxy, service, 80, 8080), 320 clusters: map[string][]string{ 321 "inbound|8080||": {"127.0.0.1:8080"}, 322 }, 323 telemetry: map[string][]string{ 324 "inbound|8080||": {string(service.Hostname)}, 325 }, 326 disableInboundPassthrough: true, 327 }, 328 { 329 name: "single service, multiple instance", 330 services: []*model.Service{service}, 331 instances: flattenInstances( 332 makeInstances(proxy, service, 80, 8080), 333 makeInstances(proxy, service, 81, 8081)), 334 clusters: map[string][]string{ 335 "inbound|8080||": {"127.0.0.1:8080"}, 336 "inbound|8081||": {"127.0.0.1:8081"}, 337 }, 338 telemetry: map[string][]string{ 339 "inbound|8080||": {string(service.Hostname)}, 340 "inbound|8081||": {string(service.Hostname)}, 341 }, 342 disableInboundPassthrough: true, 343 }, 344 { 345 name: "multiple services with same service port, different target", 346 services: []*model.Service{service, serviceAlt}, 347 instances: flattenInstances( 348 makeInstances(proxy, service, 80, 8080), 349 makeInstances(proxy, service, 81, 8081), 350 makeInstances(proxy, serviceAlt, 80, 8082), 351 makeInstances(proxy, serviceAlt, 81, 8083)), 352 clusters: map[string][]string{ 353 "inbound|8080||": {"127.0.0.1:8080"}, 354 "inbound|8081||": {"127.0.0.1:8081"}, 355 "inbound|8082||": {"127.0.0.1:8082"}, 356 "inbound|8083||": {"127.0.0.1:8083"}, 357 }, 358 telemetry: map[string][]string{ 359 "inbound|8080||": {string(service.Hostname)}, 360 "inbound|8081||": {string(service.Hostname)}, 361 "inbound|8082||": {string(serviceAlt.Hostname)}, 362 "inbound|8083||": {string(serviceAlt.Hostname)}, 363 }, 364 disableInboundPassthrough: true, 365 }, 366 { 367 name: "multiple services with same service port and target", 368 services: []*model.Service{service, serviceAlt}, 369 instances: flattenInstances( 370 makeInstances(proxy, service, 80, 8080), 371 makeInstances(proxy, service, 81, 8081), 372 makeInstances(proxy, serviceAlt, 80, 8080), 373 makeInstances(proxy, serviceAlt, 81, 8081)), 374 clusters: map[string][]string{ 375 "inbound|8080||": {"127.0.0.1:8080"}, 376 "inbound|8081||": {"127.0.0.1:8081"}, 377 }, 378 telemetry: map[string][]string{ 379 "inbound|8080||": {string(serviceAlt.Hostname), string(service.Hostname)}, 380 "inbound|8081||": {string(serviceAlt.Hostname), string(service.Hostname)}, 381 }, 382 disableInboundPassthrough: true, 383 }, 384 } 385 for _, tt := range cases { 386 name := tt.name 387 if tt.proxy == nil { 388 tt.proxy = proxy 389 } else { 390 name += "-" + tt.proxy.Metadata.IstioVersion 391 } 392 393 t.Run(name, func(t *testing.T) { 394 s := core.NewConfigGenTest(t, core.TestOptions{ 395 Services: tt.services, 396 Instances: tt.instances, 397 Configs: tt.configs, 398 MeshConfig: func() *meshconfig.MeshConfig { 399 m := mesh.DefaultMeshConfig() 400 if tt.disableInboundPassthrough { 401 m.InboundTrafficPolicy.Mode = meshconfig.MeshConfig_InboundTrafficPolicy_LOCALHOST 402 } 403 return m 404 }(), 405 }) 406 sim := simulation.NewSimulationFromConfigGen(t, s, s.SetupProxy(tt.proxy)) 407 408 clusters := xdstest.FilterClusters(sim.Clusters, func(c *cluster.Cluster) bool { 409 return strings.HasPrefix(c.Name, "inbound") 410 }) 411 if len(s.PushContext().ProxyStatus) != 0 { 412 // TODO make this fatal, once inbound conflict is silenced 413 t.Logf("got unexpected error: %+v", s.PushContext().ProxyStatus) 414 } 415 cmap := xdstest.ExtractClusters(clusters) 416 got := xdstest.MapKeys(cmap) 417 418 // Check we have all expected clusters 419 if !reflect.DeepEqual(xdstest.MapKeys(tt.clusters), got) { 420 t.Errorf("expected clusters: %v, got: %v", xdstest.MapKeys(tt.clusters), got) 421 } 422 423 for cname, c := range cmap { 424 // Check the upstream endpoints match 425 got := xdstest.ExtractLoadAssignments([]*endpoint.ClusterLoadAssignment{c.GetLoadAssignment()})[cname] 426 if !reflect.DeepEqual(tt.clusters[cname], got) { 427 t.Errorf("%v: expected endpoints %v, got %v", cname, tt.clusters[cname], got) 428 } 429 gotTelemetry := extractClusterMetadataServices(t, c) 430 if !reflect.DeepEqual(tt.telemetry[cname], gotTelemetry) { 431 t.Errorf("%v: expected telemetry services %v, got %v", cname, tt.telemetry[cname], gotTelemetry) 432 } 433 434 // simulate an actual call, this ensures we are aligned with the inbound listener configuration 435 _, _, hostname, port := model.ParseSubsetKey(cname) 436 if tt.proxy.Metadata.IstioVersion != "" { 437 // This doesn't work with the legacy proxies which have issues (https://github.com/istio/istio/issues/29199) 438 for _, i := range tt.instances { 439 if len(hostname) > 0 && i.Service.Hostname != hostname { 440 continue 441 } 442 if i.ServicePort.Port == port { 443 port = int(i.Endpoint.EndpointPort) 444 } 445 } 446 } 447 sim.Run(simulation.Call{ 448 Port: port, 449 Protocol: simulation.HTTP, 450 Address: "1.2.3.4", 451 CallMode: simulation.CallModeInbound, 452 }).Matches(t, simulation.Result{ 453 ClusterMatched: cname, 454 }) 455 } 456 }) 457 } 458 } 459 460 type clusterServicesMetadata struct { 461 Services []struct { 462 Host string 463 Name string 464 Namespace string 465 } 466 } 467 468 func extractClusterMetadataServices(t test.Failer, c *cluster.Cluster) []string { 469 got := c.GetMetadata().GetFilterMetadata()[util.IstioMetadataKey] 470 if got == nil { 471 return nil 472 } 473 s, err := protomarshal.Marshal(got) 474 if err != nil { 475 t.Fatal(err) 476 } 477 meta := clusterServicesMetadata{} 478 if err := json.Unmarshal(s, &meta); err != nil { 479 t.Fatal(err) 480 } 481 res := []string{} 482 for _, m := range meta.Services { 483 res = append(res, m.Host) 484 } 485 return res 486 } 487 488 func mtlsMode(m string) string { 489 return fmt.Sprintf(`apiVersion: security.istio.io/v1beta1 490 kind: PeerAuthentication 491 metadata: 492 name: default 493 namespace: istio-system 494 spec: 495 mtls: 496 mode: %s 497 `, m) 498 } 499 500 func TestInbound(t *testing.T) { 501 svc := ` 502 apiVersion: networking.istio.io/v1alpha3 503 kind: ServiceEntry 504 metadata: 505 name: se 506 spec: 507 hosts: 508 - foo.bar 509 endpoints: 510 - address: 1.1.1.1 511 location: MESH_INTERNAL 512 resolution: STATIC 513 ports: 514 - name: tcp 515 number: 70 516 protocol: TCP 517 - name: http 518 number: 80 519 protocol: HTTP 520 - name: auto 521 number: 81 522 --- 523 ` 524 cases := []struct { 525 Name string 526 Call simulation.Call 527 Disabled simulation.Result 528 Permissive simulation.Result 529 Strict simulation.Result 530 }{ 531 { 532 Name: "tcp", 533 Call: simulation.Call{ 534 Port: 70, 535 Protocol: simulation.TCP, 536 CallMode: simulation.CallModeInbound, 537 }, 538 Disabled: simulation.Result{ 539 ClusterMatched: "inbound|70||", 540 }, 541 Permissive: simulation.Result{ 542 ClusterMatched: "inbound|70||", 543 }, 544 Strict: simulation.Result{ 545 // Plaintext to strict, should fail 546 Error: simulation.ErrNoFilterChain, 547 }, 548 }, 549 { 550 Name: "http to tcp", 551 Call: simulation.Call{ 552 Port: 70, 553 Protocol: simulation.HTTP, 554 CallMode: simulation.CallModeInbound, 555 }, 556 Disabled: simulation.Result{ 557 ClusterMatched: "inbound|70||", 558 }, 559 Permissive: simulation.Result{ 560 ClusterMatched: "inbound|70||", 561 }, 562 Strict: simulation.Result{ 563 // Plaintext to strict, should fail 564 Error: simulation.ErrNoFilterChain, 565 }, 566 }, 567 { 568 Name: "tls to tcp", 569 Call: simulation.Call{ 570 Port: 70, 571 Protocol: simulation.TCP, 572 TLS: simulation.TLS, 573 CallMode: simulation.CallModeInbound, 574 }, 575 Disabled: simulation.Result{ 576 ClusterMatched: "inbound|70||", 577 }, 578 Permissive: simulation.Result{ 579 ClusterMatched: "inbound|70||", 580 }, 581 Strict: simulation.Result{ 582 // TLS, but not mTLS 583 Error: simulation.ErrMTLSError, 584 }, 585 }, 586 { 587 Name: "https to tcp", 588 Call: simulation.Call{ 589 Port: 70, 590 Protocol: simulation.HTTP, 591 TLS: simulation.TLS, 592 CallMode: simulation.CallModeInbound, 593 }, 594 Disabled: simulation.Result{ 595 ClusterMatched: "inbound|70||", 596 }, 597 Permissive: simulation.Result{ 598 ClusterMatched: "inbound|70||", 599 }, 600 Strict: simulation.Result{ 601 // TLS, but not mTLS 602 Error: simulation.ErrMTLSError, 603 }, 604 }, 605 { 606 Name: "mtls tcp to tcp", 607 Call: simulation.Call{ 608 Port: 70, 609 Protocol: simulation.TCP, 610 TLS: simulation.MTLS, 611 CallMode: simulation.CallModeInbound, 612 }, 613 Disabled: simulation.Result{ 614 // This is probably a user error, but there is no reason we should block mTLS traffic 615 // we just will not terminate it 616 ClusterMatched: "inbound|70||", 617 }, 618 Permissive: simulation.Result{ 619 ClusterMatched: "inbound|70||", 620 }, 621 Strict: simulation.Result{ 622 ClusterMatched: "inbound|70||", 623 }, 624 }, 625 { 626 Name: "mtls http to tcp", 627 Call: simulation.Call{ 628 Port: 70, 629 Protocol: simulation.HTTP, 630 TLS: simulation.MTLS, 631 CallMode: simulation.CallModeInbound, 632 }, 633 Disabled: simulation.Result{ 634 // This is probably a user error, but there is no reason we should block mTLS traffic 635 // we just will not terminate it 636 ClusterMatched: "inbound|70||", 637 }, 638 Permissive: simulation.Result{ 639 ClusterMatched: "inbound|70||", 640 }, 641 Strict: simulation.Result{ 642 ClusterMatched: "inbound|70||", 643 }, 644 }, 645 { 646 Name: "http", 647 Call: simulation.Call{ 648 Port: 80, 649 Protocol: simulation.HTTP, 650 CallMode: simulation.CallModeInbound, 651 }, 652 Disabled: simulation.Result{ 653 VirtualHostMatched: "inbound|http|80", 654 ClusterMatched: "inbound|80||", 655 }, 656 Permissive: simulation.Result{ 657 VirtualHostMatched: "inbound|http|80", 658 ClusterMatched: "inbound|80||", 659 }, 660 Strict: simulation.Result{ 661 // Plaintext to strict, should fail 662 Error: simulation.ErrNoFilterChain, 663 }, 664 }, 665 { 666 Name: "tls to http", 667 Call: simulation.Call{ 668 Port: 80, 669 Protocol: simulation.TCP, 670 TLS: simulation.TLS, 671 CallMode: simulation.CallModeInbound, 672 }, 673 Disabled: simulation.Result{ 674 // TLS is not terminated, so we will attempt to decode as HTTP and fail 675 Error: simulation.ErrProtocolError, 676 }, 677 Permissive: simulation.Result{ 678 // This could also be a protocol error. In the current implementation, we choose not 679 // to create a match since if we did it would just be rejected in HCM; no match 680 // is more performant 681 Error: simulation.ErrNoFilterChain, 682 }, 683 Strict: simulation.Result{ 684 // TLS, but not mTLS 685 Error: simulation.ErrMTLSError, 686 }, 687 }, 688 { 689 Name: "https to http", 690 Call: simulation.Call{ 691 Port: 80, 692 Protocol: simulation.HTTP, 693 TLS: simulation.TLS, 694 CallMode: simulation.CallModeInbound, 695 }, 696 Disabled: simulation.Result{ 697 // TLS is not terminated, so we will attempt to decode as HTTP and fail 698 Error: simulation.ErrProtocolError, 699 }, 700 Permissive: simulation.Result{ 701 // This could also be a protocol error. In the current implementation, we choose not 702 // to create a match since if we did it would just be rejected in HCM; no match 703 // is more performant 704 Error: simulation.ErrNoFilterChain, 705 }, 706 Strict: simulation.Result{ 707 // TLS, but not mTLS 708 Error: simulation.ErrMTLSError, 709 }, 710 }, 711 { 712 Name: "mtls to http", 713 Call: simulation.Call{ 714 Port: 80, 715 Protocol: simulation.HTTP, 716 TLS: simulation.MTLS, 717 CallMode: simulation.CallModeInbound, 718 }, 719 Disabled: simulation.Result{ 720 // TLS is not terminated, so we will attempt to decode as HTTP and fail 721 Error: simulation.ErrProtocolError, 722 }, 723 Permissive: simulation.Result{ 724 VirtualHostMatched: "inbound|http|80", 725 ClusterMatched: "inbound|80||", 726 }, 727 Strict: simulation.Result{ 728 VirtualHostMatched: "inbound|http|80", 729 ClusterMatched: "inbound|80||", 730 }, 731 }, 732 { 733 Name: "tcp to http", 734 Call: simulation.Call{ 735 Port: 80, 736 Protocol: simulation.TCP, 737 CallMode: simulation.CallModeInbound, 738 }, 739 Disabled: simulation.Result{ 740 // Expected, the port only supports HTTP 741 Error: simulation.ErrProtocolError, 742 }, 743 Permissive: simulation.Result{ 744 // Expected, the port only supports HTTP 745 Error: simulation.ErrProtocolError, 746 }, 747 Strict: simulation.Result{ 748 // Plaintext to strict fails 749 Error: simulation.ErrNoFilterChain, 750 }, 751 }, 752 { 753 Name: "auto port http", 754 Call: simulation.Call{ 755 Port: 81, 756 Protocol: simulation.HTTP, 757 CallMode: simulation.CallModeInbound, 758 }, 759 Disabled: simulation.Result{ 760 VirtualHostMatched: "inbound|http|81", 761 ClusterMatched: "inbound|81||", 762 }, 763 Permissive: simulation.Result{ 764 VirtualHostMatched: "inbound|http|81", 765 ClusterMatched: "inbound|81||", 766 }, 767 Strict: simulation.Result{ 768 // Plaintext to strict fails 769 Error: simulation.ErrNoFilterChain, 770 }, 771 }, 772 { 773 Name: "auto port http2", 774 Call: simulation.Call{ 775 Port: 81, 776 Protocol: simulation.HTTP2, 777 CallMode: simulation.CallModeInbound, 778 }, 779 Disabled: simulation.Result{ 780 VirtualHostMatched: "inbound|http|81", 781 ClusterMatched: "inbound|81||", 782 }, 783 Permissive: simulation.Result{ 784 VirtualHostMatched: "inbound|http|81", 785 ClusterMatched: "inbound|81||", 786 }, 787 Strict: simulation.Result{ 788 // Plaintext to strict fails 789 Error: simulation.ErrNoFilterChain, 790 }, 791 }, 792 { 793 Name: "auto port tcp", 794 Call: simulation.Call{ 795 Port: 81, 796 Protocol: simulation.TCP, 797 CallMode: simulation.CallModeInbound, 798 }, 799 Disabled: simulation.Result{ 800 ListenerMatched: "virtualInbound", 801 FilterChainMatched: "0.0.0.0_81", 802 ClusterMatched: "inbound|81||", 803 StrictMatch: true, 804 }, 805 Permissive: simulation.Result{ 806 ListenerMatched: "virtualInbound", 807 FilterChainMatched: "0.0.0.0_81", 808 ClusterMatched: "inbound|81||", 809 StrictMatch: true, 810 }, 811 Strict: simulation.Result{ 812 // Plaintext to strict fails 813 Error: simulation.ErrNoFilterChain, 814 }, 815 }, 816 { 817 Name: "tls to auto port", 818 Call: simulation.Call{ 819 Port: 81, 820 Protocol: simulation.TCP, 821 TLS: simulation.TLS, 822 CallMode: simulation.CallModeInbound, 823 }, 824 Disabled: simulation.Result{ 825 // Should go through the TCP chains 826 ListenerMatched: "virtualInbound", 827 FilterChainMatched: "0.0.0.0_81", 828 ClusterMatched: "inbound|81||", 829 StrictMatch: true, 830 }, 831 Permissive: simulation.Result{ 832 // Should go through the TCP chains 833 ListenerMatched: "virtualInbound", 834 FilterChainMatched: "0.0.0.0_81", 835 ClusterMatched: "inbound|81||", 836 StrictMatch: true, 837 }, 838 Strict: simulation.Result{ 839 // Tls, but not mTLS 840 Error: simulation.ErrMTLSError, 841 }, 842 }, 843 { 844 Name: "https to auto port", 845 Call: simulation.Call{ 846 Port: 81, 847 Protocol: simulation.HTTP, 848 TLS: simulation.TLS, 849 CallMode: simulation.CallModeInbound, 850 }, 851 Disabled: simulation.Result{ 852 // Should go through the TCP chains 853 ListenerMatched: "virtualInbound", 854 FilterChainMatched: "0.0.0.0_81", 855 ClusterMatched: "inbound|81||", 856 StrictMatch: true, 857 }, 858 Permissive: simulation.Result{ 859 // Should go through the TCP chains 860 ListenerMatched: "virtualInbound", 861 FilterChainMatched: "0.0.0.0_81", 862 ClusterMatched: "inbound|81||", 863 StrictMatch: true, 864 }, 865 Strict: simulation.Result{ 866 // Tls, but not mTLS 867 Error: simulation.ErrMTLSError, 868 }, 869 }, 870 { 871 Name: "mtls tcp to auto port", 872 Call: simulation.Call{ 873 Port: 81, 874 Protocol: simulation.TCP, 875 TLS: simulation.MTLS, 876 CallMode: simulation.CallModeInbound, 877 }, 878 Disabled: simulation.Result{ 879 // This is probably a user error, but there is no reason we should block mTLS traffic 880 // we just will not terminate it 881 ClusterMatched: "inbound|81||", 882 }, 883 Permissive: simulation.Result{ 884 // Should go through the TCP chains 885 ListenerMatched: "virtualInbound", 886 FilterChainMatched: "0.0.0.0_81", 887 ClusterMatched: "inbound|81||", 888 StrictMatch: true, 889 }, 890 Strict: simulation.Result{ 891 // Should go through the TCP chains 892 ListenerMatched: "virtualInbound", 893 FilterChainMatched: "0.0.0.0_81", 894 ClusterMatched: "inbound|81||", 895 StrictMatch: true, 896 }, 897 }, 898 { 899 Name: "mtls http to auto port", 900 Call: simulation.Call{ 901 Port: 81, 902 Protocol: simulation.HTTP, 903 TLS: simulation.MTLS, 904 CallMode: simulation.CallModeInbound, 905 }, 906 Disabled: simulation.Result{ 907 // This is probably a user error, but there is no reason we should block mTLS traffic 908 // we just will not terminate it 909 ClusterMatched: "inbound|81||", 910 }, 911 Permissive: simulation.Result{ 912 // Should go through the HTTP chains 913 VirtualHostMatched: "inbound|http|81", 914 ClusterMatched: "inbound|81||", 915 }, 916 Strict: simulation.Result{ 917 // Should go through the HTTP chains 918 VirtualHostMatched: "inbound|http|81", 919 ClusterMatched: "inbound|81||", 920 }, 921 }, 922 { 923 Name: "passthrough http", 924 Call: simulation.Call{ 925 Address: "1.2.3.4", 926 Port: 82, 927 Protocol: simulation.HTTP, 928 CallMode: simulation.CallModeInbound, 929 }, 930 Disabled: simulation.Result{ 931 ClusterMatched: "InboundPassthroughClusterIpv4", 932 FilterChainMatched: "virtualInbound-catchall-http", 933 }, 934 Permissive: simulation.Result{ 935 ClusterMatched: "InboundPassthroughClusterIpv4", 936 FilterChainMatched: "virtualInbound-catchall-http", 937 }, 938 Strict: simulation.Result{ 939 // Plaintext to strict fails 940 Error: simulation.ErrNoFilterChain, 941 }, 942 }, 943 { 944 Name: "passthrough tcp", 945 Call: simulation.Call{ 946 Address: "1.2.3.4", 947 Port: 82, 948 Protocol: simulation.TCP, 949 CallMode: simulation.CallModeInbound, 950 }, 951 Disabled: simulation.Result{ 952 ClusterMatched: "InboundPassthroughClusterIpv4", 953 FilterChainMatched: "virtualInbound", 954 }, 955 Permissive: simulation.Result{ 956 ClusterMatched: "InboundPassthroughClusterIpv4", 957 FilterChainMatched: "virtualInbound", 958 }, 959 Strict: simulation.Result{ 960 // Plaintext to strict fails 961 Error: simulation.ErrNoFilterChain, 962 }, 963 }, 964 { 965 Name: "passthrough tls", 966 Call: simulation.Call{ 967 Address: "1.2.3.4", 968 Port: 82, 969 Protocol: simulation.TCP, 970 TLS: simulation.TLS, 971 CallMode: simulation.CallModeInbound, 972 }, 973 Disabled: simulation.Result{ 974 ClusterMatched: "InboundPassthroughClusterIpv4", 975 FilterChainMatched: "virtualInbound", 976 }, 977 Permissive: simulation.Result{ 978 ClusterMatched: "InboundPassthroughClusterIpv4", 979 }, 980 Strict: simulation.Result{ 981 // tls, but not mTLS 982 Error: simulation.ErrMTLSError, 983 }, 984 }, 985 { 986 Name: "passthrough https", 987 Call: simulation.Call{ 988 Address: "1.2.3.4", 989 Port: 82, 990 Protocol: simulation.HTTP, 991 TLS: simulation.TLS, 992 CallMode: simulation.CallModeInbound, 993 }, 994 Disabled: simulation.Result{ 995 ClusterMatched: "InboundPassthroughClusterIpv4", 996 }, 997 Permissive: simulation.Result{ 998 ClusterMatched: "InboundPassthroughClusterIpv4", 999 }, 1000 Strict: simulation.Result{ 1001 // tls, but not mTLS 1002 Error: simulation.ErrMTLSError, 1003 }, 1004 }, 1005 { 1006 Name: "passthrough mtls", 1007 Call: simulation.Call{ 1008 Address: "1.2.3.4", 1009 Port: 82, 1010 Protocol: simulation.HTTP, 1011 TLS: simulation.MTLS, 1012 CallMode: simulation.CallModeInbound, 1013 }, 1014 Disabled: simulation.Result{ 1015 ClusterMatched: "InboundPassthroughClusterIpv4", 1016 }, 1017 Permissive: simulation.Result{ 1018 ClusterMatched: "InboundPassthroughClusterIpv4", 1019 }, 1020 Strict: simulation.Result{ 1021 ClusterMatched: "InboundPassthroughClusterIpv4", 1022 }, 1023 }, 1024 } 1025 t.Run("Disable", func(t *testing.T) { 1026 calls := []simulation.Expect{} 1027 for _, c := range cases { 1028 calls = append(calls, simulation.Expect{ 1029 Name: c.Name, 1030 Call: c.Call, 1031 Result: c.Disabled, 1032 }) 1033 } 1034 runSimulationTest(t, nil, xds.FakeOptions{}, simulationTest{ 1035 config: svc + mtlsMode("DISABLE"), 1036 calls: calls, 1037 }) 1038 }) 1039 1040 t.Run("Permissive", func(t *testing.T) { 1041 calls := []simulation.Expect{} 1042 for _, c := range cases { 1043 calls = append(calls, simulation.Expect{ 1044 Name: c.Name, 1045 Call: c.Call, 1046 Result: c.Permissive, 1047 }) 1048 } 1049 runSimulationTest(t, nil, xds.FakeOptions{}, simulationTest{ 1050 config: svc + mtlsMode("PERMISSIVE"), 1051 calls: calls, 1052 }) 1053 }) 1054 1055 t.Run("Strict", func(t *testing.T) { 1056 calls := []simulation.Expect{} 1057 for _, c := range cases { 1058 calls = append(calls, simulation.Expect{ 1059 Name: c.Name, 1060 Call: c.Call, 1061 Result: c.Strict, 1062 }) 1063 } 1064 runSimulationTest(t, nil, xds.FakeOptions{}, simulationTest{ 1065 config: svc + mtlsMode("STRICT"), 1066 calls: calls, 1067 }) 1068 }) 1069 } 1070 1071 func TestHeadlessServices(t *testing.T) { 1072 ports := ` 1073 - name: http 1074 port: 80 1075 - name: auto 1076 port: 81 1077 - name: tcp 1078 port: 82 1079 - name: tls 1080 port: 83 1081 - name: https 1082 port: 84` 1083 1084 calls := []simulation.Expect{} 1085 for _, call := range []simulation.Call{ 1086 {Address: "1.2.3.4", Port: 80, Protocol: simulation.HTTP, HostHeader: "headless.default.svc.cluster.local"}, 1087 1088 // Auto port should support any protocol 1089 {Address: "1.2.3.4", Port: 81, Protocol: simulation.HTTP, HostHeader: "headless.default.svc.cluster.local"}, 1090 {Address: "1.2.3.4", Port: 81, Protocol: simulation.HTTP, TLS: simulation.TLS, HostHeader: "headless.default.svc.cluster.local"}, 1091 {Address: "1.2.3.4", Port: 81, Protocol: simulation.TCP, HostHeader: "headless.default.svc.cluster.local"}, 1092 1093 {Address: "1.2.3.4", Port: 82, Protocol: simulation.TCP, HostHeader: "headless.default.svc.cluster.local"}, 1094 1095 // Use short host name 1096 {Address: "1.2.3.4", Port: 83, Protocol: simulation.TCP, TLS: simulation.TLS, HostHeader: "headless.default"}, 1097 {Address: "1.2.3.4", Port: 84, Protocol: simulation.HTTP, TLS: simulation.TLS, HostHeader: "headless.default"}, 1098 } { 1099 calls = append(calls, simulation.Expect{ 1100 Name: fmt.Sprintf("%s-%d", call.Protocol, call.Port), 1101 Call: call, 1102 Result: simulation.Result{ 1103 ClusterMatched: fmt.Sprintf("outbound|%d||headless.default.svc.cluster.local", call.Port), 1104 }, 1105 }) 1106 } 1107 runSimulationTest(t, nil, xds.FakeOptions{}, simulationTest{ 1108 kubeConfig: `apiVersion: v1 1109 kind: Service 1110 metadata: 1111 name: headless 1112 namespace: default 1113 spec: 1114 clusterIP: None 1115 selector: 1116 app: headless 1117 ports:` + ports + ` 1118 --- 1119 apiVersion: discovery.k8s.io/v1 1120 kind: EndpointSlice 1121 metadata: 1122 name: headless 1123 namespace: default 1124 labels: 1125 kubernetes.io/service-name: headless 1126 endpoints: 1127 - addresses: 1128 - 1.2.3.4 1129 ports: 1130 ` + ports, 1131 calls: calls, 1132 }, 1133 ) 1134 } 1135 1136 func TestExternalNameServices(t *testing.T) { 1137 test.SetForTest(t, &features.EnableExternalNameAlias, true) 1138 ports := ` 1139 - name: http 1140 port: 80 1141 - name: auto 1142 port: 81 1143 - name: tcp 1144 port: 82 1145 - name: tls 1146 port: 83 1147 - name: https 1148 port: 84` 1149 1150 calls := []simulation.Expect{} 1151 for _, call := range []simulation.Call{ 1152 {Address: "1.2.3.4", Port: 80, Protocol: simulation.HTTP, HostHeader: "alias.default.svc.cluster.local"}, 1153 1154 // Auto port should support any protocol 1155 {Address: "1.2.3.4", Port: 81, Protocol: simulation.HTTP, HostHeader: "alias.default.svc.cluster.local"}, 1156 {Address: "1.2.3.4", Port: 81, Protocol: simulation.HTTP, TLS: simulation.TLS, HostHeader: "alias.default.svc.cluster.local"}, 1157 {Address: "1.2.3.4", Port: 81, Protocol: simulation.TCP}, 1158 1159 {Address: "1.2.3.4", Port: 82, Protocol: simulation.TCP}, 1160 1161 // Use short host name 1162 {Address: "1.2.3.4", Port: 83, Protocol: simulation.TCP, TLS: simulation.TLS, HostHeader: "alias.default"}, 1163 {Address: "1.2.3.4", Port: 84, Protocol: simulation.HTTP, TLS: simulation.TLS, HostHeader: "alias.default"}, 1164 } { 1165 calls = append(calls, simulation.Expect{ 1166 Name: fmt.Sprintf("%s-%d", call.Protocol, call.Port), 1167 Call: call, 1168 Result: simulation.Result{ 1169 ClusterMatched: fmt.Sprintf("outbound|%d||concrete.default.svc.cluster.local", call.Port), 1170 }, 1171 }) 1172 } 1173 service := `apiVersion: v1 1174 kind: Service 1175 metadata: 1176 name: alias 1177 namespace: default 1178 spec: 1179 type: ExternalName 1180 externalName: concrete.default.svc.cluster.local 1181 ` + `--- 1182 apiVersion: v1 1183 kind: Service 1184 metadata: 1185 name: concrete 1186 namespace: default 1187 spec: 1188 clusterIP: 1.2.3.4 1189 ports:` + ports 1190 runSimulationTest(t, nil, xds.FakeOptions{}, simulationTest{ 1191 kubeConfig: service, 1192 calls: calls, 1193 }) 1194 1195 // HTTP Routes 1196 runSimulationTest(t, nil, xds.FakeOptions{}, simulationTest{ 1197 config: `apiVersion: networking.istio.io/v1alpha3 1198 kind: VirtualService 1199 metadata: 1200 name: alias 1201 spec: 1202 hosts: 1203 - alias.default.svc.cluster.local 1204 http: 1205 - name: "route1" 1206 match: 1207 - uri: 1208 prefix: "/one" 1209 route: 1210 - destination: 1211 host: concrete.default.svc.cluster.local`, 1212 kubeConfig: service, 1213 calls: []simulation.Expect{ 1214 { 1215 // This work, Host is just an opaque hostname match 1216 Name: "HTTP virtual service applies to alias fqdn", 1217 Call: simulation.Call{Address: "1.2.3.4", Port: 80, Protocol: simulation.HTTP, HostHeader: "alias.default.svc.cluster.local", Path: "/one"}, 1218 Result: simulation.Result{ 1219 RouteMatched: "route1", 1220 ClusterMatched: "outbound|80||concrete.default.svc.cluster.local", 1221 }, 1222 }, 1223 { 1224 // Host is opaque, so no expansion 1225 Name: "HTTP virtual service does not apply to alias without exact match", 1226 Call: simulation.Call{Address: "1.2.3.4", Port: 80, Protocol: simulation.HTTP, HostHeader: "alias.default", Path: "/one"}, 1227 Result: simulation.Result{ 1228 RouteMatched: "default", 1229 ClusterMatched: "outbound|80||concrete.default.svc.cluster.local", 1230 }, 1231 }, 1232 { 1233 Name: "HTTP virtual service of alias does not apply to concrete", 1234 Call: simulation.Call{Address: "1.2.3.4", Port: 80, Protocol: simulation.HTTP, HostHeader: "concrete.default.svc.cluster.local", Path: "/one"}, 1235 Result: simulation.Result{ 1236 RouteMatched: "default", 1237 ClusterMatched: "outbound|80||concrete.default.svc.cluster.local", 1238 }, 1239 }, 1240 // Auto 1241 { 1242 // No opaque host match for auto 1243 Name: "Auto virtual service applies to alias fqdn", 1244 Call: simulation.Call{Address: "1.2.3.4", Port: 81, Protocol: simulation.HTTP, HostHeader: "alias.default.svc.cluster.local", Path: "/one"}, 1245 Result: simulation.Result{ 1246 RouteMatched: "default", 1247 ClusterMatched: "outbound|81||concrete.default.svc.cluster.local", 1248 }, 1249 }, 1250 { 1251 // Host is opaque, so no expansion 1252 Name: "Auto virtual service does not apply to alias without exact match", 1253 Call: simulation.Call{Address: "1.2.3.4", Port: 81, Protocol: simulation.HTTP, HostHeader: "alias.default", Path: "/one"}, 1254 Result: simulation.Result{ 1255 RouteMatched: "default", 1256 ClusterMatched: "outbound|81||concrete.default.svc.cluster.local", 1257 }, 1258 }, 1259 { 1260 Name: "Auto virtual service of alias does not apply to concrete", 1261 Call: simulation.Call{Address: "1.2.3.4", Port: 81, Protocol: simulation.HTTP, HostHeader: "concrete.default.svc.cluster.local", Path: "/one"}, 1262 Result: simulation.Result{ 1263 RouteMatched: "default", 1264 ClusterMatched: "outbound|81||concrete.default.svc.cluster.local", 1265 }, 1266 }, 1267 }, 1268 }) 1269 1270 // TCP Routes 1271 runSimulationTest(t, nil, xds.FakeOptions{}, simulationTest{ 1272 config: `apiVersion: networking.istio.io/v1alpha3 1273 kind: VirtualService 1274 metadata: 1275 name: alias 1276 spec: 1277 hosts: 1278 - alias.default.svc.cluster.local 1279 tcp: 1280 - name: "route1" 1281 route: 1282 - destination: 1283 host: concrete.default.svc.cluster.local 1284 port: 1285 number: 80`, 1286 kubeConfig: service, 1287 calls: []simulation.Expect{ 1288 { 1289 Name: "TCP virtual services do not apply", 1290 Call: simulation.Call{Address: "1.2.3.4", Port: 82, Protocol: simulation.TCP, Path: "/one"}, 1291 Result: simulation.Result{ 1292 ClusterMatched: "outbound|82||concrete.default.svc.cluster.local", 1293 }, 1294 }, 1295 }, 1296 }) 1297 } 1298 1299 func TestExternalNameServicesWithoutAliases(t *testing.T) { 1300 test.SetForTest(t, &features.EnableExternalNameAlias, false) 1301 ports := ` 1302 - name: http 1303 port: 80 1304 - name: auto 1305 port: 81 1306 - name: tcp 1307 port: 82 1308 - name: tls 1309 port: 83 1310 - name: https 1311 port: 84` 1312 1313 type tc struct { 1314 call simulation.Call 1315 expected string 1316 } 1317 calls := []simulation.Expect{} 1318 for _, call := range []tc{ 1319 {call: simulation.Call{Address: "1.2.3.4", Port: 80, Protocol: simulation.HTTP, HostHeader: "alias.default.svc.cluster.local"}, expected: "alias"}, 1320 1321 // Auto port should support any protocol 1322 {call: simulation.Call{Address: "1.2.3.4", Port: 81, Protocol: simulation.HTTP, HostHeader: "alias.default.svc.cluster.local"}, expected: "concrete"}, 1323 {call: simulation.Call{Address: "1.1.1.1", Port: 81, Protocol: simulation.HTTP, HostHeader: "alias.default.svc.cluster.local"}, expected: "alias"}, 1324 { 1325 call: simulation.Call{Address: "1.2.3.4", Port: 81, Protocol: simulation.HTTP, TLS: simulation.TLS, HostHeader: "alias.default.svc.cluster.local"}, 1326 expected: "concrete", 1327 }, 1328 {call: simulation.Call{Address: "1.2.3.4", Port: 81, Protocol: simulation.TCP}, expected: "concrete"}, 1329 1330 {call: simulation.Call{Address: "1.2.3.4", Port: 82, Protocol: simulation.TCP}, expected: "concrete"}, 1331 1332 // Use short host name 1333 {call: simulation.Call{Address: "1.2.3.4", Port: 83, Protocol: simulation.TCP, TLS: simulation.TLS, HostHeader: "alias.default"}, expected: "concrete"}, 1334 {call: simulation.Call{Address: "1.2.3.4", Port: 84, Protocol: simulation.HTTP, TLS: simulation.TLS, HostHeader: "alias.default"}, expected: "concrete"}, 1335 } { 1336 calls = append(calls, simulation.Expect{ 1337 Name: fmt.Sprintf("%s-%d", call.call.Protocol, call.call.Port), 1338 Call: call.call, 1339 Result: simulation.Result{ 1340 ClusterMatched: fmt.Sprintf("outbound|%d||%s.default.svc.cluster.local", call.call.Port, call.expected), 1341 }, 1342 }) 1343 } 1344 service := `apiVersion: v1 1345 kind: Service 1346 metadata: 1347 name: alias 1348 namespace: default 1349 spec: 1350 type: ExternalName 1351 externalName: concrete.default.svc.cluster.local 1352 ports:` + ports + ` 1353 --- 1354 apiVersion: v1 1355 kind: Service 1356 metadata: 1357 name: concrete 1358 namespace: default 1359 spec: 1360 clusterIP: 1.2.3.4 1361 ports:` + ports 1362 runSimulationTest(t, nil, xds.FakeOptions{}, simulationTest{ 1363 kubeConfig: service, 1364 calls: calls, 1365 }) 1366 1367 // HTTP Routes 1368 runSimulationTest(t, nil, xds.FakeOptions{}, simulationTest{ 1369 config: `apiVersion: networking.istio.io/v1alpha3 1370 kind: VirtualService 1371 metadata: 1372 name: alias 1373 spec: 1374 hosts: 1375 - alias.default.svc.cluster.local 1376 http: 1377 - name: "route1" 1378 match: 1379 - uri: 1380 prefix: "/one" 1381 route: 1382 - destination: 1383 host: concrete.default.svc.cluster.local`, 1384 kubeConfig: service, 1385 calls: []simulation.Expect{ 1386 { 1387 // This work, Host is just an opaque hostname match 1388 Name: "HTTP virtual service applies to alias fqdn", 1389 Call: simulation.Call{Address: "1.2.3.4", Port: 80, Protocol: simulation.HTTP, HostHeader: "alias.default.svc.cluster.local", Path: "/one"}, 1390 Result: simulation.Result{ 1391 RouteMatched: "route1", 1392 ClusterMatched: "outbound|80||concrete.default.svc.cluster.local", 1393 }, 1394 }, 1395 { 1396 // Host is expanded 1397 Name: "HTTP virtual service does apply to alias without exact match", 1398 Call: simulation.Call{Address: "1.2.3.4", Port: 80, Protocol: simulation.HTTP, HostHeader: "alias.default", Path: "/one"}, 1399 Result: simulation.Result{ 1400 RouteMatched: "route1", 1401 ClusterMatched: "outbound|80||concrete.default.svc.cluster.local", 1402 }, 1403 }, 1404 { 1405 Name: "HTTP virtual service of alias does not apply to concrete", 1406 Call: simulation.Call{Address: "1.2.3.4", Port: 80, Protocol: simulation.HTTP, HostHeader: "concrete.default.svc.cluster.local", Path: "/one"}, 1407 Result: simulation.Result{ 1408 RouteMatched: "default", 1409 ClusterMatched: "outbound|80||concrete.default.svc.cluster.local", 1410 }, 1411 }, 1412 // Auto 1413 { 1414 // No opaque host match for auto 1415 Name: "Auto virtual service applies to alias fqdn", 1416 Call: simulation.Call{Address: "1.2.3.4", Port: 81, Protocol: simulation.HTTP, HostHeader: "alias.default.svc.cluster.local", Path: "/one"}, 1417 Result: simulation.Result{ 1418 RouteMatched: "default", 1419 ClusterMatched: "outbound|81||concrete.default.svc.cluster.local", 1420 }, 1421 }, 1422 { 1423 // Host is opaque, so no expansion 1424 Name: "Auto virtual service does not apply to alias without exact match", 1425 Call: simulation.Call{Address: "1.2.3.4", Port: 81, Protocol: simulation.HTTP, HostHeader: "alias.default", Path: "/one"}, 1426 Result: simulation.Result{ 1427 RouteMatched: "default", 1428 ClusterMatched: "outbound|81||concrete.default.svc.cluster.local", 1429 }, 1430 }, 1431 { 1432 Name: "Auto virtual service of alias does not apply to concrete", 1433 Call: simulation.Call{Address: "1.2.3.4", Port: 81, Protocol: simulation.HTTP, HostHeader: "concrete.default.svc.cluster.local", Path: "/one"}, 1434 Result: simulation.Result{ 1435 RouteMatched: "default", 1436 ClusterMatched: "outbound|81||concrete.default.svc.cluster.local", 1437 }, 1438 }, 1439 }, 1440 }) 1441 1442 // TCP Routes 1443 runSimulationTest(t, nil, xds.FakeOptions{}, simulationTest{ 1444 config: `apiVersion: networking.istio.io/v1alpha3 1445 kind: VirtualService 1446 metadata: 1447 name: alias 1448 spec: 1449 hosts: 1450 - alias.default.svc.cluster.local 1451 tcp: 1452 - name: "route1" 1453 route: 1454 - destination: 1455 host: concrete.default.svc.cluster.local 1456 port: 1457 number: 80`, 1458 kubeConfig: service, 1459 calls: []simulation.Expect{ 1460 { 1461 Name: "TCP virtual services do not apply", 1462 Call: simulation.Call{Address: "1.2.3.4", Port: 82, Protocol: simulation.TCP, Path: "/one"}, 1463 Result: simulation.Result{ 1464 ClusterMatched: "outbound|82||concrete.default.svc.cluster.local", 1465 }, 1466 }, 1467 }, 1468 }) 1469 1470 // HTTP Routes to alias 1471 runSimulationTest(t, nil, xds.FakeOptions{}, simulationTest{ 1472 config: `apiVersion: networking.istio.io/v1alpha3 1473 kind: VirtualService 1474 metadata: 1475 name: alias 1476 spec: 1477 hosts: 1478 - example.com 1479 http: 1480 - name: "route1" 1481 match: 1482 - uri: 1483 prefix: "/one" 1484 route: 1485 - destination: 1486 host: alias.default.svc.cluster.local`, 1487 kubeConfig: service, 1488 calls: []simulation.Expect{ 1489 { 1490 // This work, Host is just an opaque hostname match 1491 Name: "HTTP route to alias", 1492 Call: simulation.Call{Port: 80, Protocol: simulation.HTTP, HostHeader: "example.com", Path: "/one"}, 1493 Result: simulation.Result{ 1494 RouteMatched: "route1", 1495 ClusterMatched: "outbound|80||alias.default.svc.cluster.local", 1496 }, 1497 }, 1498 }, 1499 }) 1500 } 1501 1502 func TestPassthroughTraffic(t *testing.T) { 1503 calls := map[string]simulation.Call{} 1504 for port := 80; port < 87; port++ { 1505 for _, call := range []simulation.Call{ 1506 {Port: port, Protocol: simulation.HTTP, TLS: simulation.Plaintext, HostHeader: "foo"}, 1507 {Port: port, Protocol: simulation.HTTP, TLS: simulation.TLS, HostHeader: "foo"}, 1508 {Port: port, Protocol: simulation.HTTP, TLS: simulation.TLS, HostHeader: "foo", Alpn: "http/1.1"}, 1509 {Port: port, Protocol: simulation.TCP, TLS: simulation.Plaintext, HostHeader: "foo"}, 1510 {Port: port, Protocol: simulation.HTTP2, TLS: simulation.TLS, HostHeader: "foo"}, 1511 } { 1512 suffix := "" 1513 if call.Alpn != "" { 1514 suffix = "-" + call.Alpn 1515 } 1516 calls[fmt.Sprintf("%v-%v-%v%v", call.Protocol, call.TLS, port, suffix)] = call 1517 } 1518 } 1519 ports := ` 1520 ports: 1521 - name: http 1522 number: 80 1523 protocol: HTTP 1524 - name: auto 1525 number: 81 1526 - name: tcp 1527 number: 82 1528 protocol: TCP 1529 - name: tls 1530 number: 83 1531 protocol: TLS 1532 - name: https 1533 number: 84 1534 protocol: HTTPS 1535 - name: grpc 1536 number: 85 1537 protocol: GRPC 1538 - name: h2 1539 number: 86 1540 protocol: HTTP2` 1541 1542 isHTTPPort := func(p int) bool { 1543 switch p { 1544 case 80, 85, 86: 1545 return true 1546 default: 1547 return false 1548 } 1549 } 1550 isAutoPort := func(p int) bool { 1551 switch p { 1552 case 81: 1553 return true 1554 default: 1555 return false 1556 } 1557 } 1558 for _, tp := range []meshconfig.MeshConfig_OutboundTrafficPolicy_Mode{ 1559 meshconfig.MeshConfig_OutboundTrafficPolicy_REGISTRY_ONLY, 1560 meshconfig.MeshConfig_OutboundTrafficPolicy_ALLOW_ANY, 1561 } { 1562 t.Run(tp.String(), func(t *testing.T) { 1563 o := xds.FakeOptions{ 1564 MeshConfig: func() *meshconfig.MeshConfig { 1565 m := mesh.DefaultMeshConfig() 1566 m.OutboundTrafficPolicy.Mode = tp 1567 return m 1568 }(), 1569 } 1570 expectedCluster := map[meshconfig.MeshConfig_OutboundTrafficPolicy_Mode]string{ 1571 meshconfig.MeshConfig_OutboundTrafficPolicy_REGISTRY_ONLY: util.BlackHoleCluster, 1572 meshconfig.MeshConfig_OutboundTrafficPolicy_ALLOW_ANY: util.PassthroughCluster, 1573 }[tp] 1574 t.Run("with VIP", func(t *testing.T) { 1575 testCalls := []simulation.Expect{} 1576 for name, call := range calls { 1577 e := simulation.Expect{ 1578 Name: name, 1579 Call: call, 1580 Result: simulation.Result{ 1581 ClusterMatched: expectedCluster, 1582 }, 1583 } 1584 // For blackhole, we will 502 where possible instead of blackhole cluster 1585 // This only works for HTTP on HTTP 1586 if expectedCluster == util.BlackHoleCluster && call.IsHTTP() && isHTTPPort(call.Port) { 1587 e.Result.ClusterMatched = "" 1588 e.Result.VirtualHostMatched = util.BlackHole 1589 } 1590 testCalls = append(testCalls, e) 1591 } 1592 sort.Slice(testCalls, func(i, j int) bool { 1593 return testCalls[i].Name < testCalls[j].Name 1594 }) 1595 runSimulationTest(t, nil, o, 1596 simulationTest{ 1597 config: ` 1598 apiVersion: networking.istio.io/v1alpha3 1599 kind: ServiceEntry 1600 metadata: 1601 name: se 1602 spec: 1603 hosts: 1604 - istio.io 1605 addresses: [1.2.3.4] 1606 location: MESH_EXTERNAL 1607 resolution: DNS` + ports, 1608 calls: testCalls, 1609 }) 1610 }) 1611 t.Run("without VIP", func(t *testing.T) { 1612 testCalls := []simulation.Expect{} 1613 for name, call := range calls { 1614 e := simulation.Expect{ 1615 Name: name, 1616 Call: call, 1617 Result: simulation.Result{ 1618 ClusterMatched: expectedCluster, 1619 }, 1620 } 1621 // For blackhole, we will 502 where possible instead of blackhole cluster 1622 // This only works for HTTP on HTTP 1623 if expectedCluster == util.BlackHoleCluster && call.IsHTTP() && (isHTTPPort(call.Port) || isAutoPort(call.Port)) { 1624 e.Result.ClusterMatched = "" 1625 e.Result.VirtualHostMatched = util.BlackHole 1626 } 1627 // TCP without a VIP will capture everything. 1628 // Auto without a VIP is similar, but HTTP happens to work because routing is done on header 1629 if call.Port == 82 || (call.Port == 81 && !call.IsHTTP()) { 1630 e.Result.Error = nil 1631 e.Result.ClusterMatched = "" 1632 } 1633 testCalls = append(testCalls, e) 1634 } 1635 sort.Slice(testCalls, func(i, j int) bool { 1636 return testCalls[i].Name < testCalls[j].Name 1637 }) 1638 runSimulationTest(t, nil, o, 1639 simulationTest{ 1640 config: ` 1641 apiVersion: networking.istio.io/v1alpha3 1642 kind: ServiceEntry 1643 metadata: 1644 name: se 1645 spec: 1646 hosts: 1647 - istio.io 1648 location: MESH_EXTERNAL 1649 resolution: DNS` + ports, 1650 calls: testCalls, 1651 }) 1652 }) 1653 }) 1654 } 1655 } 1656 1657 func TestLoop(t *testing.T) { 1658 runSimulationTest(t, nil, xds.FakeOptions{}, simulationTest{ 1659 calls: []simulation.Expect{ 1660 { 1661 Name: "direct request to outbound port", 1662 Call: simulation.Call{ 1663 Port: 15001, 1664 Protocol: simulation.TCP, 1665 }, 1666 Result: simulation.Result{ 1667 // This request should be blocked 1668 ClusterMatched: "BlackHoleCluster", 1669 }, 1670 }, 1671 { 1672 Name: "direct request to inbound port", 1673 Call: simulation.Call{ 1674 Port: 15006, 1675 Protocol: simulation.TCP, 1676 }, 1677 Result: simulation.Result{ 1678 // This request should be blocked 1679 ClusterMatched: "BlackHoleCluster", 1680 }, 1681 }, 1682 }, 1683 }) 1684 } 1685 1686 func TestInboundSidecarTLSModes(t *testing.T) { 1687 peerAuthConfig := func(m string) string { 1688 return fmt.Sprintf(`apiVersion: security.istio.io/v1beta1 1689 kind: PeerAuthentication 1690 metadata: 1691 name: peer-auth 1692 namespace: default 1693 spec: 1694 selector: 1695 matchLabels: 1696 app: foo 1697 mtls: 1698 mode: STRICT 1699 portLevelMtls: 1700 9080: 1701 mode: %s 1702 --- 1703 `, m) 1704 } 1705 sidecarSimple := func(protocol string) string { 1706 return fmt.Sprintf(` 1707 apiVersion: networking.istio.io/v1alpha3 1708 kind: Sidecar 1709 metadata: 1710 labels: 1711 app: foo 1712 name: sidecar 1713 namespace: default 1714 spec: 1715 ingress: 1716 - defaultEndpoint: 0.0.0.0:9080 1717 port: 1718 name: tls 1719 number: 9080 1720 protocol: %s 1721 tls: 1722 mode: SIMPLE 1723 privateKey: "httpbinkey.pem" 1724 serverCertificate: "httpbin.pem" 1725 workloadSelector: 1726 labels: 1727 app: foo 1728 --- 1729 `, protocol) 1730 } 1731 sidecarMutual := func(protocol string) string { 1732 return fmt.Sprintf(` 1733 apiVersion: networking.istio.io/v1alpha3 1734 kind: Sidecar 1735 metadata: 1736 labels: 1737 app: foo 1738 name: sidecar 1739 namespace: default 1740 spec: 1741 ingress: 1742 - defaultEndpoint: 0.0.0.0:9080 1743 port: 1744 name: tls 1745 number: 9080 1746 protocol: %s 1747 tls: 1748 mode: MUTUAL 1749 privateKey: "httpbinkey.pem" 1750 serverCertificate: "httpbin.pem" 1751 caCertificates: "rootCA.pem" 1752 workloadSelector: 1753 labels: 1754 app: foo 1755 --- 1756 `, protocol) 1757 } 1758 expectedTLSContext := func(filterChain *listener.FilterChain) error { 1759 tlsContext := &tls.DownstreamTlsContext{} 1760 ts := filterChain.GetTransportSocket().GetTypedConfig() 1761 if ts == nil { 1762 return fmt.Errorf("expected transport socket for chain %v", filterChain.GetName()) 1763 } 1764 if err := ts.UnmarshalTo(tlsContext); err != nil { 1765 return err 1766 } 1767 commonTLSContext := tlsContext.CommonTlsContext 1768 if len(commonTLSContext.TlsCertificateSdsSecretConfigs) == 0 { 1769 return fmt.Errorf("expected tls certificates") 1770 } 1771 if commonTLSContext.TlsCertificateSdsSecretConfigs[0].Name != "file-cert:httpbin.pem~httpbinkey.pem" { 1772 return fmt.Errorf("expected certificate httpbin.pem, actual %s", commonTLSContext.TlsCertificates[0].CertificateChain.String()) 1773 } 1774 if tlsContext.RequireClientCertificate.Value { 1775 return fmt.Errorf("expected RequireClientCertificate to be false") 1776 } 1777 return nil 1778 } 1779 1780 mkCall := func(port int, protocol simulation.Protocol, 1781 tls simulation.TLSMode, validations []simulation.CustomFilterChainValidation, 1782 mTLSSecretConfigName string, 1783 ) simulation.Call { 1784 return simulation.Call{ 1785 Protocol: protocol, 1786 Port: port, 1787 CallMode: simulation.CallModeInbound, 1788 TLS: tls, 1789 CustomListenerValidations: validations, 1790 MtlsSecretConfigName: mTLSSecretConfigName, 1791 } 1792 } 1793 cases := []struct { 1794 name string 1795 config string 1796 calls []simulation.Expect 1797 }{ 1798 { 1799 name: "sidecar http over TLS simple mode with peer auth on port disabled", 1800 config: peerAuthConfig("DISABLE") + sidecarSimple("HTTPS"), 1801 calls: []simulation.Expect{ 1802 { 1803 Name: "http over tls", 1804 Call: mkCall(9080, simulation.HTTP, simulation.TLS, []simulation.CustomFilterChainValidation{expectedTLSContext}, ""), 1805 Result: simulation.Result{ 1806 FilterChainMatched: "1.1.1.1_9080", 1807 ClusterMatched: "inbound|9080||", 1808 VirtualHostMatched: "inbound|http|9080", 1809 RouteMatched: "default", 1810 ListenerMatched: "virtualInbound", 1811 }, 1812 }, 1813 { 1814 Name: "plaintext", 1815 Call: mkCall(9080, simulation.HTTP, simulation.Plaintext, nil, ""), 1816 Result: simulation.Result{ 1817 Error: simulation.ErrNoFilterChain, 1818 }, 1819 }, 1820 { 1821 Name: "http over mTLS", 1822 Call: mkCall(9080, simulation.HTTP, simulation.MTLS, nil, "file-cert:httpbin.pem~httpbinkey.pem"), 1823 Result: simulation.Result{ 1824 Error: simulation.ErrMTLSError, 1825 }, 1826 }, 1827 }, 1828 }, 1829 { 1830 name: "sidecar TCP over TLS simple mode with peer auth on port disabled", 1831 config: peerAuthConfig("DISABLE") + sidecarSimple("TLS"), 1832 calls: []simulation.Expect{ 1833 { 1834 Name: "tcp over tls", 1835 Call: mkCall(9080, simulation.TCP, simulation.TLS, []simulation.CustomFilterChainValidation{expectedTLSContext}, ""), 1836 Result: simulation.Result{ 1837 FilterChainMatched: "1.1.1.1_9080", 1838 ClusterMatched: "inbound|9080||", 1839 ListenerMatched: "virtualInbound", 1840 }, 1841 }, 1842 { 1843 Name: "plaintext", 1844 Call: mkCall(9080, simulation.TCP, simulation.Plaintext, nil, ""), 1845 Result: simulation.Result{ 1846 Error: simulation.ErrNoFilterChain, 1847 }, 1848 }, 1849 { 1850 Name: "tcp over mTLS", 1851 Call: mkCall(9080, simulation.TCP, simulation.MTLS, nil, "file-cert:httpbin.pem~httpbinkey.pem"), 1852 Result: simulation.Result{ 1853 Error: simulation.ErrMTLSError, 1854 }, 1855 }, 1856 }, 1857 }, 1858 { 1859 name: "sidecar http over mTLS mutual mode with peer auth on port disabled", 1860 config: peerAuthConfig("DISABLE") + sidecarMutual("HTTPS"), 1861 calls: []simulation.Expect{ 1862 { 1863 Name: "http over mtls", 1864 Call: mkCall(9080, simulation.HTTP, simulation.MTLS, nil, "file-cert:httpbin.pem~httpbinkey.pem"), 1865 Result: simulation.Result{ 1866 FilterChainMatched: "1.1.1.1_9080", 1867 ClusterMatched: "inbound|9080||", 1868 ListenerMatched: "virtualInbound", 1869 }, 1870 }, 1871 { 1872 Name: "plaintext", 1873 Call: mkCall(9080, simulation.HTTP, simulation.Plaintext, nil, ""), 1874 Result: simulation.Result{ 1875 Error: simulation.ErrNoFilterChain, 1876 }, 1877 }, 1878 { 1879 Name: "http over tls", 1880 Call: mkCall(9080, simulation.HTTP, simulation.TLS, nil, "file-cert:httpbin.pem~httpbinkey.pem"), 1881 Result: simulation.Result{ 1882 Error: simulation.ErrMTLSError, 1883 }, 1884 }, 1885 }, 1886 }, 1887 { 1888 name: "sidecar tcp over mTLS mutual mode with peer auth on port disabled", 1889 config: peerAuthConfig("DISABLE") + sidecarMutual("TLS"), 1890 calls: []simulation.Expect{ 1891 { 1892 Name: "tcp over mtls", 1893 Call: mkCall(9080, simulation.TCP, simulation.MTLS, nil, "file-cert:httpbin.pem~httpbinkey.pem"), 1894 Result: simulation.Result{ 1895 FilterChainMatched: "1.1.1.1_9080", 1896 ClusterMatched: "inbound|9080||", 1897 ListenerMatched: "virtualInbound", 1898 }, 1899 }, 1900 { 1901 Name: "plaintext", 1902 Call: mkCall(9080, simulation.TCP, simulation.Plaintext, nil, ""), 1903 Result: simulation.Result{ 1904 Error: simulation.ErrNoFilterChain, 1905 }, 1906 }, 1907 { 1908 Name: "http over tls", 1909 Call: mkCall(9080, simulation.TCP, simulation.TLS, nil, "file-cert:httpbin.pem~httpbinkey.pem"), 1910 Result: simulation.Result{ 1911 Error: simulation.ErrMTLSError, 1912 }, 1913 }, 1914 }, 1915 }, 1916 { 1917 name: "sidecar http over TLS SIMPLE mode with peer auth on port STRICT", 1918 config: peerAuthConfig("STRICT") + sidecarMutual("TLS"), 1919 calls: []simulation.Expect{ 1920 { 1921 Name: "http over tls", 1922 Call: mkCall(9080, simulation.HTTP, simulation.TLS, nil, ""), 1923 Result: simulation.Result{ 1924 Error: simulation.ErrMTLSError, 1925 }, 1926 }, 1927 { 1928 Name: "plaintext", 1929 Call: mkCall(9080, simulation.HTTP, simulation.Plaintext, nil, ""), 1930 Result: simulation.Result{ 1931 Error: simulation.ErrNoFilterChain, 1932 }, 1933 }, 1934 { 1935 Name: "http over mtls", 1936 Call: mkCall(9080, simulation.HTTP, simulation.MTLS, nil, ""), 1937 Result: simulation.Result{ 1938 FilterChainMatched: "1.1.1.1_9080", 1939 ClusterMatched: "inbound|9080||", 1940 ListenerMatched: "virtualInbound", 1941 }, 1942 }, 1943 }, 1944 }, 1945 } 1946 proxy := &model.Proxy{ 1947 Labels: map[string]string{"app": "foo"}, 1948 Metadata: &model.NodeMetadata{Labels: map[string]string{"app": "foo"}}, 1949 } 1950 test.SetForTest(t, &features.EnableTLSOnSidecarIngress, true) 1951 for _, tt := range cases { 1952 runSimulationTest(t, proxy, xds.FakeOptions{}, simulationTest{ 1953 name: tt.name, 1954 config: tt.config, 1955 calls: tt.calls, 1956 }) 1957 } 1958 } 1959 1960 const ( 1961 TimeOlder = "2019-01-01T00:00:00Z" 1962 TimeBase = "2020-01-01T00:00:00Z" 1963 TimeNewer = "2021-01-01T00:00:00Z" 1964 ) 1965 1966 type Configer interface { 1967 Config(t *testing.T, variant string) string 1968 } 1969 1970 type vsArgs struct { 1971 Namespace string 1972 Match string 1973 Matches []string 1974 GwMatches []types.NamespacedName 1975 Dest string 1976 Port int 1977 PortMatch int 1978 Time string 1979 } 1980 1981 func (args vsArgs) Config(t *testing.T, variant string) string { 1982 if args.Time == "" { 1983 args.Time = TimeBase 1984 } 1985 1986 if args.Matches == nil { 1987 args.Matches = []string{args.Match} 1988 } 1989 if variant == "httproute" { 1990 if args.GwMatches == nil { 1991 args.GwMatches = make([]types.NamespacedName, 0, len(args.Matches)) 1992 for _, m := range args.Matches { 1993 spl := strings.Split(m, ".") 1994 if len(spl) != 5 { 1995 t.Skipf("unsupported match: %v", spl) 1996 } 1997 if spl[0] == "*" { 1998 t.Skipf("unsupported match: %v", spl) 1999 } 2000 args.GwMatches = append(args.GwMatches, types.NamespacedName{ 2001 Namespace: spl[1], 2002 Name: spl[0], 2003 }) 2004 } 2005 } 2006 } 2007 switch variant { 2008 case "httproute": 2009 return tmpl.MustEvaluate(`apiVersion: gateway.networking.k8s.io/v1beta1 2010 kind: HTTPRoute 2011 metadata: 2012 name: "{{.Namespace}}{{.Match | replace "*" "wild"}}{{.Dest}}" 2013 namespace: {{.Namespace}} 2014 creationTimestamp: "{{.Time}}" 2015 spec: 2016 parentRefs: 2017 {{- range $val := .GwMatches }} 2018 - group: "" 2019 kind: Service 2020 name: "{{$val.Name}}" 2021 namespace: "{{$val.Namespace}}" 2022 {{ with $.PortMatch }} 2023 port: {{.}} 2024 {{ end }} 2025 {{ end }} 2026 rules: 2027 - backendRefs: 2028 - kind: Hostname 2029 group: networking.istio.io 2030 name: {{.Dest}} 2031 port: {{.Port | default 80}} 2032 `, args) 2033 case "virtualservice": 2034 return tmpl.MustEvaluate(`apiVersion: networking.istio.io/v1alpha3 2035 kind: VirtualService 2036 metadata: 2037 name: "{{.Namespace}}{{.Match | replace "*" "wild"}}{{.Dest}}" 2038 namespace: {{.Namespace}} 2039 creationTimestamp: "{{.Time}}" 2040 spec: 2041 hosts: 2042 {{- range $val := .Matches }} 2043 - "{{$val}}" 2044 {{ end }} 2045 http: 2046 - route: 2047 - destination: 2048 host: {{.Dest}} 2049 {{ with .Port }} 2050 port: 2051 number: {{.}} 2052 {{ end }} 2053 {{ with .PortMatch }} 2054 match: 2055 - port: {{.}} 2056 {{ end }} 2057 `, args) 2058 default: 2059 panic(variant + " unknown") 2060 } 2061 } 2062 2063 type scArgs struct { 2064 Namespace string 2065 Egress []string 2066 } 2067 2068 func (args scArgs) Config(t *testing.T, variant string) string { 2069 return tmpl.MustEvaluate(`apiVersion: networking.istio.io/v1alpha3 2070 kind: Sidecar 2071 metadata: 2072 name: "{{.Namespace}}" 2073 namespace: "{{.Namespace}}" 2074 spec: 2075 egress: 2076 - hosts: 2077 {{- range $val := .Egress }} 2078 - "{{$val}}" 2079 {{- end }} 2080 `, args) 2081 } 2082 2083 func TestSidecarRoutes(t *testing.T) { 2084 knownServices := ` 2085 apiVersion: v1 2086 kind: Service 2087 metadata: 2088 name: known 2089 namespace: default 2090 spec: 2091 clusterIP: 2.0.0.0 2092 ports: 2093 - port: 80 2094 name: http 2095 - port: 8080 2096 name: http 2097 --- 2098 apiVersion: v1 2099 kind: Service 2100 metadata: 2101 name: alt-known 2102 namespace: default 2103 spec: 2104 clusterIP: 2.0.0.1 2105 ports: 2106 - port: 80 2107 name: http 2108 - port: 8080 2109 name: http 2110 --- 2111 apiVersion: v1 2112 kind: Service 2113 metadata: 2114 name: not-default 2115 namespace: not-default 2116 spec: 2117 clusterIP: 2.0.0.2 2118 ports: 2119 - port: 80 2120 name: http 2121 - port: 8080 2122 name: http 2123 ` 2124 proxy := func(ns string) *model.Proxy { 2125 return &model.Proxy{ConfigNamespace: ns} 2126 } 2127 cases := []struct { 2128 name string 2129 cfg []Configer 2130 proxy *model.Proxy 2131 routeName string 2132 expected map[string][]string 2133 expectedGateway map[string][]string 2134 oldestWins bool 2135 }{ 2136 // Port 80 has special cases as there is defaulting logic around this port 2137 { 2138 name: "simple port 80", 2139 cfg: []Configer{vsArgs{ 2140 Namespace: "default", 2141 Match: "known.default.svc.cluster.local", 2142 Dest: "alt-known.default.svc.cluster.local", 2143 }}, 2144 proxy: proxy("default"), 2145 routeName: "80", 2146 expected: map[string][]string{ 2147 "known.default.svc.cluster.local": {"outbound|80||alt-known.default.svc.cluster.local"}, 2148 }, 2149 }, 2150 { 2151 name: "simple port 8080", 2152 cfg: []Configer{vsArgs{ 2153 Namespace: "default", 2154 Match: "known.default.svc.cluster.local", 2155 Dest: "alt-known.default.svc.cluster.local", 2156 }}, 2157 proxy: proxy("default"), 2158 routeName: "8080", 2159 expected: map[string][]string{ 2160 "known.default.svc.cluster.local": {"outbound|8080||alt-known.default.svc.cluster.local"}, 2161 }, 2162 expectedGateway: map[string][]string{ 2163 "known.default.svc.cluster.local": {"outbound|80||alt-known.default.svc.cluster.local"}, 2164 }, 2165 }, 2166 { 2167 name: "unknown port 80", 2168 cfg: []Configer{vsArgs{ 2169 Namespace: "default", 2170 Match: "foo.default.svc.cluster.local", 2171 Dest: "foo.default.svc.cluster.local", 2172 }}, 2173 proxy: proxy("default"), 2174 routeName: "80", 2175 expected: map[string][]string{ 2176 "foo.default.svc.cluster.local": {"outbound|80||foo.default.svc.cluster.local"}, 2177 }, 2178 expectedGateway: map[string][]string{ 2179 "foo.default.svc.cluster.local": nil, 2180 }, 2181 }, 2182 { 2183 name: "unknown port 8080", 2184 cfg: []Configer{vsArgs{ 2185 Namespace: "default", 2186 Match: "foo.default.svc.cluster.local", 2187 Dest: "foo.default.svc.cluster.local", 2188 }}, 2189 proxy: proxy("default"), 2190 routeName: "8080", 2191 // For unknown services, we only will add a route to the port 80 2192 expected: map[string][]string{ 2193 "foo.default.svc.cluster.local": nil, 2194 }, 2195 }, 2196 { 2197 name: "unknown port 8080 match 8080", 2198 cfg: []Configer{vsArgs{ 2199 Namespace: "default", 2200 Match: "foo.default.svc.cluster.local", 2201 Dest: "foo.default.svc.cluster.local", 2202 PortMatch: 8080, 2203 }}, 2204 proxy: proxy("default"), 2205 routeName: "8080", 2206 // For unknown services, we only will add a route to the port 80 2207 expected: map[string][]string{ 2208 "foo.default.svc.cluster.local": nil, 2209 }, 2210 }, 2211 { 2212 name: "unknown port 8080 dest 8080 ", 2213 cfg: []Configer{vsArgs{ 2214 Namespace: "default", 2215 Match: "foo.default.svc.cluster.local", 2216 Dest: "foo.default.svc.cluster.local", 2217 Port: 8080, 2218 }}, 2219 proxy: proxy("default"), 2220 routeName: "8080", 2221 // For unknown services, we only will add a route to the port 80 2222 expected: map[string][]string{ 2223 "foo.default.svc.cluster.local": nil, 2224 }, 2225 }, 2226 { 2227 name: "producer rule port 80", 2228 cfg: []Configer{vsArgs{ 2229 Namespace: "default", 2230 Match: "known.default.svc.cluster.local", 2231 Dest: "alt-known.default.svc.cluster.local", 2232 }}, 2233 proxy: proxy("not-default"), 2234 routeName: "80", 2235 expected: map[string][]string{ 2236 "known.default.svc.cluster.local": {"outbound|80||alt-known.default.svc.cluster.local"}, 2237 }, 2238 }, 2239 { 2240 name: "producer rule port 8080", 2241 cfg: []Configer{vsArgs{ 2242 Namespace: "default", 2243 Match: "known.default.svc.cluster.local", 2244 Dest: "alt-known.default.svc.cluster.local", 2245 }}, 2246 proxy: proxy("not-default"), 2247 routeName: "8080", 2248 expected: map[string][]string{ 2249 "known.default.svc.cluster.local": {"outbound|8080||alt-known.default.svc.cluster.local"}, 2250 }, 2251 expectedGateway: map[string][]string{ // No implicit port matching for gateway 2252 "known.default.svc.cluster.local": {"outbound|80||alt-known.default.svc.cluster.local"}, 2253 }, 2254 }, 2255 { 2256 name: "consumer rule port 80", 2257 cfg: []Configer{vsArgs{ 2258 Namespace: "not-default", 2259 Match: "known.default.svc.cluster.local", 2260 Dest: "alt-known.default.svc.cluster.local", 2261 }}, 2262 proxy: proxy("not-default"), 2263 routeName: "80", 2264 expected: map[string][]string{ 2265 "known.default.svc.cluster.local": {"outbound|80||alt-known.default.svc.cluster.local"}, 2266 }, 2267 }, 2268 { 2269 name: "consumer rule port 8080", 2270 cfg: []Configer{vsArgs{ 2271 Namespace: "not-default", 2272 Match: "known.default.svc.cluster.local", 2273 Dest: "alt-known.default.svc.cluster.local", 2274 }}, 2275 proxy: proxy("not-default"), 2276 routeName: "8080", 2277 expected: map[string][]string{ 2278 "known.default.svc.cluster.local": {"outbound|8080||alt-known.default.svc.cluster.local"}, 2279 }, 2280 expectedGateway: map[string][]string{ // No implicit port matching for gateway 2281 "known.default.svc.cluster.local": {"outbound|80||alt-known.default.svc.cluster.local"}, 2282 }, 2283 }, 2284 { 2285 name: "arbitrary rule port 80", 2286 cfg: []Configer{vsArgs{ 2287 Namespace: "arbitrary", 2288 Match: "known.default.svc.cluster.local", 2289 Dest: "alt-known.default.svc.cluster.local", 2290 }}, 2291 proxy: proxy("not-default"), 2292 routeName: "80", 2293 expected: map[string][]string{ 2294 "known.default.svc.cluster.local": {"outbound|80||alt-known.default.svc.cluster.local"}, 2295 }, 2296 expectedGateway: map[string][]string{ 2297 "known.default.svc.cluster.local": {"outbound|80||known.default.svc.cluster.local"}, 2298 }, 2299 }, 2300 { 2301 name: "arbitrary rule port 8080", 2302 cfg: []Configer{vsArgs{ 2303 Namespace: "arbitrary", 2304 Match: "known.default.svc.cluster.local", 2305 Dest: "alt-known.default.svc.cluster.local", 2306 }}, 2307 proxy: proxy("not-default"), 2308 routeName: "8080", 2309 expected: map[string][]string{ 2310 "known.default.svc.cluster.local": {"outbound|8080||alt-known.default.svc.cluster.local"}, 2311 }, 2312 expectedGateway: map[string][]string{ 2313 "known.default.svc.cluster.local": {"outbound|8080||known.default.svc.cluster.local"}, 2314 }, 2315 }, 2316 { 2317 name: "multiple rules 80", 2318 cfg: []Configer{ 2319 vsArgs{ 2320 Namespace: "arbitrary", 2321 Match: "known.default.svc.cluster.local", 2322 Dest: "arbitrary.example.com", 2323 Time: TimeOlder, 2324 }, 2325 vsArgs{ 2326 Namespace: "default", 2327 Match: "known.default.svc.cluster.local", 2328 Dest: "default.example.com", 2329 Time: TimeBase, 2330 }, 2331 vsArgs{ 2332 Namespace: "not-default", 2333 Match: "known.default.svc.cluster.local", 2334 Dest: "not-default.example.com", 2335 Time: TimeNewer, 2336 }, 2337 }, 2338 proxy: proxy("not-default"), 2339 routeName: "80", 2340 expected: map[string][]string{ 2341 // Oldest wins 2342 "known.default.svc.cluster.local": {"outbound|80||arbitrary.example.com"}, 2343 }, 2344 expectedGateway: map[string][]string{ 2345 "known.default.svc.cluster.local": {"outbound|80||not-default.example.com"}, 2346 }, 2347 }, 2348 { 2349 name: "multiple rules 8080", 2350 cfg: []Configer{ 2351 vsArgs{ 2352 Namespace: "arbitrary", 2353 Match: "known.default.svc.cluster.local", 2354 Dest: "arbitrary.example.com", 2355 Time: TimeOlder, 2356 }, 2357 vsArgs{ 2358 Namespace: "default", 2359 Match: "known.default.svc.cluster.local", 2360 Dest: "default.example.com", 2361 Time: TimeBase, 2362 }, 2363 vsArgs{ 2364 Namespace: "not-default", 2365 Match: "known.default.svc.cluster.local", 2366 Dest: "not-default.example.com", 2367 Time: TimeNewer, 2368 }, 2369 }, 2370 proxy: proxy("not-default"), 2371 routeName: "8080", 2372 expected: map[string][]string{ 2373 // Oldest wins 2374 "known.default.svc.cluster.local": {"outbound|8080||arbitrary.example.com"}, 2375 }, 2376 expectedGateway: map[string][]string{ 2377 "known.default.svc.cluster.local": {"outbound|80||not-default.example.com"}, 2378 }, 2379 }, 2380 { 2381 name: "wildcard random", 2382 cfg: []Configer{vsArgs{ 2383 Namespace: "default", 2384 Match: "*.unknown.example.com", 2385 Dest: "arbitrary.example.com", 2386 }}, 2387 proxy: proxy("default"), 2388 routeName: "80", 2389 expected: map[string][]string{ 2390 // match no VS, get default config 2391 "alt-known.default.svc.cluster.local": {"outbound|80||alt-known.default.svc.cluster.local"}, 2392 "known.default.svc.cluster.local": {"outbound|80||known.default.svc.cluster.local"}, 2393 // Wildcard doesn't match any known services, insert it as-is 2394 "*.unknown.example.com": {"outbound|80||arbitrary.example.com"}, 2395 }, 2396 }, 2397 { 2398 name: "wildcard match with sidecar", 2399 cfg: []Configer{ 2400 vsArgs{ 2401 Namespace: "default", 2402 Match: "*.cluster.local", 2403 Dest: "arbitrary.example.com", 2404 }, 2405 scArgs{ 2406 Namespace: "default", 2407 Egress: []string{"*/*.cluster.local"}, 2408 }, 2409 }, 2410 proxy: proxy("default"), 2411 routeName: "80", 2412 expected: map[string][]string{ 2413 "alt-known.default.svc.cluster.local": {"outbound|80||arbitrary.example.com"}, 2414 "known.default.svc.cluster.local": {"outbound|80||arbitrary.example.com"}, 2415 // Matched an exact service, so we have no route for the wildcard 2416 "*.cluster.local": nil, 2417 }, 2418 expectedGateway: map[string][]string{ 2419 // Exact service matches do not get the wildcard applied 2420 "alt-known.default.svc.cluster.local": {"outbound|80||alt-known.default.svc.cluster.local"}, 2421 "known.default.svc.cluster.local": {"outbound|80||known.default.svc.cluster.local"}, 2422 // The wildcard 2423 "*.cluster.local": {"outbound|80||arbitrary.example.com"}, 2424 }, 2425 }, 2426 { 2427 name: "wildcard first then explicit (oldest wins feature flag)", 2428 cfg: []Configer{ 2429 vsArgs{ 2430 Namespace: "default", 2431 Match: "*.cluster.local", 2432 Dest: "wild.example.com", 2433 Time: TimeOlder, 2434 }, 2435 vsArgs{ 2436 Namespace: "default", 2437 Match: "known.default.svc.cluster.local", 2438 Dest: "explicit.example.com", 2439 Time: TimeNewer, 2440 }, 2441 }, 2442 proxy: proxy("default"), 2443 routeName: "80", 2444 expected: map[string][]string{ 2445 "alt-known.default.svc.cluster.local": {"outbound|80||wild.example.com"}, 2446 "known.default.svc.cluster.local": {"outbound|80||wild.example.com"}, // oldest wins 2447 // Matched an exact service, so we have no route for the wildcard 2448 "*.cluster.local": nil, 2449 }, 2450 expectedGateway: map[string][]string{ 2451 // No overrides, use default 2452 "alt-known.default.svc.cluster.local": {"outbound|80||alt-known.default.svc.cluster.local"}, 2453 // Explicit has precedence 2454 "known.default.svc.cluster.local": {"outbound|80||explicit.example.com"}, 2455 // Last is our wildcard 2456 "*.cluster.local": {"outbound|80||wild.example.com"}, 2457 }, 2458 oldestWins: true, 2459 }, 2460 { 2461 name: "wildcard first then explicit", 2462 cfg: []Configer{ 2463 vsArgs{ 2464 Namespace: "default", 2465 Match: "*.cluster.local", 2466 Dest: "wild.example.com", 2467 Time: TimeOlder, 2468 }, 2469 vsArgs{ 2470 Namespace: "default", 2471 Match: "known.default.svc.cluster.local", 2472 Dest: "explicit.example.com", 2473 Time: TimeNewer, 2474 }, 2475 }, 2476 proxy: proxy("default"), 2477 routeName: "80", 2478 expected: map[string][]string{ 2479 "alt-known.default.svc.cluster.local": {"outbound|80||wild.example.com"}, 2480 "known.default.svc.cluster.local": {"outbound|80||explicit.example.com"}, 2481 // Matched an exact service, so we have no route for the wildcard 2482 "*.cluster.local": nil, 2483 }, 2484 expectedGateway: map[string][]string{ 2485 // No overrides, use default 2486 "alt-known.default.svc.cluster.local": {"outbound|80||alt-known.default.svc.cluster.local"}, 2487 // Explicit has precedence 2488 "known.default.svc.cluster.local": {"outbound|80||explicit.example.com"}, 2489 // Last is our wildcard 2490 "*.cluster.local": {"outbound|80||wild.example.com"}, 2491 }, 2492 }, 2493 { 2494 name: "explicit first then wildcard", 2495 cfg: []Configer{ 2496 vsArgs{ 2497 Namespace: "default", 2498 Match: "*.cluster.local", 2499 Dest: "wild.example.com", 2500 Time: TimeNewer, 2501 }, 2502 vsArgs{ 2503 Namespace: "default", 2504 Match: "known.default.svc.cluster.local", 2505 Dest: "explicit.example.com", 2506 Time: TimeOlder, 2507 }, 2508 }, 2509 proxy: proxy("default"), 2510 routeName: "80", 2511 expected: map[string][]string{ 2512 "alt-known.default.svc.cluster.local": {"outbound|80||wild.example.com"}, 2513 "known.default.svc.cluster.local": {"outbound|80||explicit.example.com"}, 2514 // Matched an exact service, so we have no route for the wildcard 2515 "*.cluster.local": nil, 2516 }, 2517 expectedGateway: map[string][]string{ 2518 // No overrides, use default 2519 "alt-known.default.svc.cluster.local": {"outbound|80||alt-known.default.svc.cluster.local"}, 2520 // Explicit has precedence 2521 "known.default.svc.cluster.local": {"outbound|80||explicit.example.com"}, 2522 // Last is our wildcard 2523 "*.cluster.local": {"outbound|80||wild.example.com"}, 2524 }, 2525 }, 2526 { 2527 name: "wildcard and explicit with sidecar (oldest wins feature flag)", 2528 cfg: []Configer{ 2529 vsArgs{ 2530 Namespace: "default", 2531 Match: "*.cluster.local", 2532 Dest: "wild.example.com", 2533 Time: TimeOlder, 2534 }, 2535 vsArgs{ 2536 Namespace: "default", 2537 Match: "known.default.svc.cluster.local", 2538 Dest: "explicit.example.com", 2539 Time: TimeNewer, 2540 }, 2541 scArgs{ 2542 Namespace: "default", 2543 Egress: []string{"default/known.default.svc.cluster.local", "default/alt-known.default.svc.cluster.local"}, 2544 }, 2545 }, 2546 proxy: proxy("default"), 2547 routeName: "80", 2548 expected: map[string][]string{ 2549 // Even though we did not import `*.cluster.local`, the VS attaches 2550 "alt-known.default.svc.cluster.local": {"outbound|80||wild.example.com"}, 2551 // Oldest wins 2552 "known.default.svc.cluster.local": {"outbound|80||wild.example.com"}, 2553 // Matched an exact service, so we have no route for the wildcard 2554 "*.cluster.local": nil, 2555 }, 2556 expectedGateway: map[string][]string{ 2557 // No rule imported 2558 "alt-known.default.svc.cluster.local": {"outbound|80||alt-known.default.svc.cluster.local"}, 2559 // Imported rule 2560 "known.default.svc.cluster.local": {"outbound|80||explicit.example.com"}, 2561 // Not imported 2562 "*.cluster.local": nil, 2563 }, 2564 oldestWins: true, 2565 }, 2566 { 2567 name: "wildcard and explicit with sidecar", 2568 cfg: []Configer{ 2569 vsArgs{ 2570 Namespace: "default", 2571 Match: "*.cluster.local", 2572 Dest: "wild.example.com", 2573 Time: TimeOlder, 2574 }, 2575 vsArgs{ 2576 Namespace: "default", 2577 Match: "known.default.svc.cluster.local", 2578 Dest: "explicit.example.com", 2579 Time: TimeNewer, 2580 }, 2581 scArgs{ 2582 Namespace: "default", 2583 Egress: []string{"default/known.default.svc.cluster.local", "default/alt-known.default.svc.cluster.local"}, 2584 }, 2585 }, 2586 proxy: proxy("default"), 2587 routeName: "80", 2588 expected: map[string][]string{ 2589 // Even though we did not import `*.cluster.local`, the VS attaches 2590 "alt-known.default.svc.cluster.local": {"outbound|80||wild.example.com"}, 2591 // Most exact match wins 2592 "known.default.svc.cluster.local": {"outbound|80||explicit.example.com"}, 2593 // Matched an exact service, so we have no route for the wildcard 2594 "*.cluster.local": nil, 2595 }, 2596 expectedGateway: map[string][]string{ 2597 // No rule imported 2598 "alt-known.default.svc.cluster.local": {"outbound|80||alt-known.default.svc.cluster.local"}, 2599 // Imported rule 2600 "known.default.svc.cluster.local": {"outbound|80||explicit.example.com"}, 2601 // Not imported 2602 "*.cluster.local": nil, 2603 }, 2604 }, 2605 { 2606 name: "explicit first then wildcard with sidecar cross namespace", 2607 cfg: []Configer{ 2608 vsArgs{ 2609 Namespace: "not-default", 2610 Match: "*.cluster.local", 2611 Dest: "wild.example.com", 2612 Time: TimeOlder, 2613 }, 2614 vsArgs{ 2615 Namespace: "default", 2616 Match: "known.default.svc.cluster.local", 2617 Dest: "explicit.example.com", 2618 Time: TimeNewer, 2619 }, 2620 scArgs{ 2621 Namespace: "default", 2622 Egress: []string{"default/known.default.svc.cluster.local", "default/alt-known.default.svc.cluster.local"}, 2623 }, 2624 }, 2625 proxy: proxy("default"), 2626 routeName: "80", 2627 expected: map[string][]string{ 2628 // Similar to above, but now the older wildcard VS is in a complete different namespace which we don't import 2629 "alt-known.default.svc.cluster.local": {"outbound|80||alt-known.default.svc.cluster.local"}, 2630 "known.default.svc.cluster.local": {"outbound|80||explicit.example.com"}, 2631 // Matched an exact service, so we have no route for the wildcard 2632 "*.cluster.local": nil, 2633 }, 2634 }, 2635 { 2636 name: "wildcard and explicit cross namespace (oldest wins feature flag)", 2637 cfg: []Configer{ 2638 vsArgs{ 2639 Namespace: "not-default", 2640 Match: "*.cluster.local", 2641 Dest: "wild.example.com", 2642 Time: TimeOlder, 2643 }, 2644 vsArgs{ 2645 Namespace: "default", 2646 Match: "known.default.svc.cluster.local", 2647 Dest: "explicit.example.com", 2648 Time: TimeNewer, 2649 }, 2650 }, 2651 proxy: proxy("default"), 2652 routeName: "80", 2653 expected: map[string][]string{ 2654 // Wildcard is older, so it wins, even though it is cross namespace 2655 "alt-known.default.svc.cluster.local": {"outbound|80||wild.example.com"}, 2656 "known.default.svc.cluster.local": {"outbound|80||wild.example.com"}, 2657 // Matched an exact service, so we have no route for the wildcard 2658 "*.cluster.local": nil, 2659 }, 2660 expectedGateway: map[string][]string{ 2661 // Exact match wins 2662 "alt-known.default.svc.cluster.local": {"outbound|80||alt-known.default.svc.cluster.local"}, 2663 "known.default.svc.cluster.local": {"outbound|80||explicit.example.com"}, 2664 // Wildcard last 2665 "*.cluster.local": {"outbound|80||wild.example.com"}, 2666 }, 2667 oldestWins: true, 2668 }, 2669 { 2670 name: "wildcard and explicit cross namespace", 2671 cfg: []Configer{ 2672 vsArgs{ 2673 Namespace: "not-default", 2674 Match: "*.cluster.local", 2675 Dest: "wild.example.com", 2676 Time: TimeOlder, 2677 }, 2678 vsArgs{ 2679 Namespace: "default", 2680 Match: "known.default.svc.cluster.local", 2681 Dest: "explicit.example.com", 2682 Time: TimeNewer, 2683 }, 2684 }, 2685 proxy: proxy("default"), 2686 routeName: "80", 2687 expected: map[string][]string{ 2688 // Exact match wins 2689 "alt-known.default.svc.cluster.local": {"outbound|80||wild.example.com"}, 2690 "known.default.svc.cluster.local": {"outbound|80||explicit.example.com"}, 2691 // Matched an exact service, so we have no route for the wildcard 2692 "*.cluster.local": nil, 2693 }, 2694 expectedGateway: map[string][]string{ 2695 // Exact match wins 2696 "alt-known.default.svc.cluster.local": {"outbound|80||alt-known.default.svc.cluster.local"}, 2697 "known.default.svc.cluster.local": {"outbound|80||explicit.example.com"}, 2698 // Wildcard last 2699 "*.cluster.local": {"outbound|80||wild.example.com"}, 2700 }, 2701 }, 2702 { 2703 name: "wildcard and explicit unknown", 2704 cfg: []Configer{ 2705 vsArgs{ 2706 Namespace: "default", 2707 Match: "*.tld", 2708 Dest: "wild.example.com", 2709 Time: TimeOlder, 2710 }, 2711 vsArgs{ 2712 Namespace: "default", 2713 Match: "example.tld", 2714 Dest: "explicit.example.com", 2715 Time: TimeNewer, 2716 }, 2717 }, 2718 proxy: proxy("default"), 2719 routeName: "80", 2720 expected: map[string][]string{ 2721 // wildcard does not match 2722 "known.default.svc.cluster.local": {"outbound|80||known.default.svc.cluster.local"}, 2723 // Even though its less exact, this wildcard wins 2724 "*.tld": {"outbound|80||wild.example.com"}, 2725 "*.example.tld": nil, 2726 }, 2727 }, 2728 { 2729 name: "explicit match with wildcard sidecar", 2730 cfg: []Configer{ 2731 vsArgs{ 2732 Namespace: "default", 2733 Match: "arbitrary.svc.cluster.local", 2734 Dest: "arbitrary.svc.cluster.local", 2735 }, 2736 scArgs{ 2737 Namespace: "default", 2738 Egress: []string{"*/*.cluster.local"}, 2739 }, 2740 }, 2741 proxy: proxy("default"), 2742 routeName: "80", 2743 expected: map[string][]string{ 2744 "arbitrary.svc.cluster.local": {"outbound|80||arbitrary.svc.cluster.local"}, 2745 }, 2746 }, 2747 { 2748 name: "wildcard match with explicit sidecar", 2749 cfg: []Configer{ 2750 vsArgs{ 2751 Namespace: "default", 2752 Match: "*.cluster.local", 2753 Dest: "arbitrary.example.com", 2754 }, 2755 scArgs{ 2756 Namespace: "default", 2757 Egress: []string{"*/known.default.svc.cluster.local"}, 2758 }, 2759 }, 2760 proxy: proxy("default"), 2761 routeName: "80", 2762 expected: map[string][]string{ 2763 "known.default.svc.cluster.local": {"outbound|80||arbitrary.example.com"}, 2764 "*.cluster.local": nil, 2765 }, 2766 expectedGateway: map[string][]string{ 2767 "known.default.svc.cluster.local": {"outbound|80||known.default.svc.cluster.local"}, 2768 "*.cluster.local": nil, 2769 }, 2770 }, 2771 { 2772 name: "non-service wildcard match with explicit sidecar", 2773 cfg: []Configer{ 2774 vsArgs{ 2775 Namespace: "default", 2776 Match: "*.example.org", 2777 Dest: "arbitrary.example.com", 2778 }, 2779 scArgs{ 2780 Namespace: "default", 2781 Egress: []string{"*/explicit.example.org", "*/alt-known.default.svc.cluster.local"}, 2782 }, 2783 }, 2784 proxy: proxy("default"), 2785 routeName: "80", 2786 expected: map[string][]string{ 2787 "known.default.svc.cluster.local": nil, // Not imported 2788 "alt-known.default.svc.cluster.local": {"outbound|80||alt-known.default.svc.cluster.local"}, // No change 2789 "*.example.org": {"outbound|80||arbitrary.example.com"}, 2790 }, 2791 expectedGateway: map[string][]string{ 2792 "known.default.svc.cluster.local": nil, // Not imported 2793 "alt-known.default.svc.cluster.local": {"outbound|80||alt-known.default.svc.cluster.local"}, // No change 2794 "*.example.org": nil, // Not imported 2795 }, 2796 }, 2797 { 2798 name: "sidecar filter", 2799 cfg: []Configer{ 2800 vsArgs{ 2801 Namespace: "not-default", 2802 Match: "*.default.svc.cluster.local", 2803 Dest: "arbitrary.example.com", 2804 }, 2805 vsArgs{ 2806 Namespace: "default", 2807 Match: "explicit.default.svc.cluster.local", 2808 Dest: "explicit.default.svc.cluster.local", 2809 }, 2810 scArgs{ 2811 Namespace: "not-default", 2812 Egress: []string{"not-default/*.default.svc.cluster.local", "not-default/not-default.not-default.svc.cluster.local"}, 2813 }, 2814 }, 2815 proxy: proxy("not-default"), 2816 routeName: "80", 2817 expected: map[string][]string{ 2818 // even though there is an *.svc.cluster.local, since we do not import it we should create a wildcard matcher 2819 "*.default.svc.cluster.local": {"outbound|80||arbitrary.example.com"}, 2820 // We did not import this, shouldn't show up 2821 "explicit.default.svc.cluster.local": nil, 2822 "not-default.not-default.svc.cluster.local": {"outbound|80||not-default.not-default.svc.cluster.local"}, 2823 }, 2824 }, 2825 { 2826 name: "same namespace conflict", 2827 cfg: []Configer{ 2828 vsArgs{ 2829 Namespace: "default", 2830 Match: "known.default.svc.cluster.local", 2831 Dest: "old.example.com", 2832 Time: TimeOlder, 2833 }, 2834 vsArgs{ 2835 Namespace: "default", 2836 Match: "known.default.svc.cluster.local", 2837 Dest: "new.example.com", 2838 Time: TimeNewer, 2839 }, 2840 }, 2841 proxy: proxy("default"), 2842 routeName: "80", 2843 expected: map[string][]string{ 2844 "known.default.svc.cluster.local": {"outbound|80||old.example.com"}, // oldest wins 2845 }, 2846 }, 2847 { 2848 name: "cross namespace conflict", 2849 cfg: []Configer{ 2850 vsArgs{ 2851 Namespace: "not-default", 2852 Match: "known.default.svc.cluster.local", 2853 Dest: "producer.example.com", 2854 Time: TimeOlder, 2855 }, 2856 vsArgs{ 2857 Namespace: "default", 2858 Match: "known.default.svc.cluster.local", 2859 Dest: "consumer.example.com", 2860 Time: TimeNewer, 2861 }, 2862 }, 2863 proxy: proxy("default"), 2864 routeName: "80", 2865 expected: map[string][]string{ 2866 // oldest wins 2867 "known.default.svc.cluster.local": {"outbound|80||producer.example.com"}, 2868 }, 2869 expectedGateway: map[string][]string{ 2870 // consumer wins 2871 "known.default.svc.cluster.local": {"outbound|80||consumer.example.com"}, 2872 }, 2873 }, 2874 { 2875 name: "import only a unknown service route", 2876 cfg: []Configer{ 2877 vsArgs{ 2878 Namespace: "default", 2879 Match: "foo.default.svc.cluster.local", 2880 Dest: "example.com", 2881 }, 2882 scArgs{ 2883 Namespace: "default", 2884 Egress: []string{"*/foo.default.svc.cluster.local"}, 2885 }, 2886 }, 2887 proxy: proxy("default"), 2888 routeName: "80", 2889 expected: nil, // We do not even get a route as there is no service on the port 2890 }, 2891 { 2892 // https://github.com/istio/istio/issues/37087 2893 name: "multi-host import single", 2894 cfg: []Configer{ 2895 vsArgs{ 2896 Namespace: "default", 2897 Matches: []string{"known.default.svc.cluster.local", "alt-known.default.svc.cluster.local"}, 2898 Dest: "example.com", 2899 }, 2900 scArgs{ 2901 Namespace: "default", 2902 Egress: []string{"*/known.default.svc.cluster.local"}, 2903 }, 2904 }, 2905 proxy: proxy("default"), 2906 routeName: "80", 2907 expected: map[string][]string{ 2908 // imported 2909 "known.default.svc.cluster.local": {"outbound|80||example.com"}, 2910 // Not imported but we include it anyway 2911 "alt-known.default.svc.cluster.local": {"outbound|80||example.com"}, 2912 }, 2913 expectedGateway: map[string][]string{ 2914 // imported 2915 "known.default.svc.cluster.local": {"outbound|80||example.com"}, 2916 // Not imported 2917 "alt-known.default.svc.cluster.local": nil, 2918 }, 2919 }, 2920 } 2921 for _, variant := range []string{"httproute", "virtualservice"} { 2922 t.Run(variant, func(t *testing.T) { 2923 for _, tt := range cases { 2924 tt := tt 2925 t.Run(tt.name, func(t *testing.T) { 2926 if tt.oldestWins { 2927 test.SetForTest(t, &features.PersistOldestWinsHeuristicForVirtualServiceHostMatching, true) 2928 } else { 2929 t.Parallel() // feature flags and parallel tests don't mix 2930 } 2931 cfg := knownServices 2932 for _, tc := range tt.cfg { 2933 cfg = cfg + "\n---\n" + tc.Config(t, variant) 2934 } 2935 istio, k, err := crd.ParseInputs(cfg) 2936 if err != nil { 2937 t.Fatal(err) 2938 } 2939 kubeo, err := kube.SlowConvertKindsToRuntimeObjects(k) 2940 if err != nil { 2941 t.Fatal(err) 2942 } 2943 s := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{ 2944 Configs: istio, 2945 KubernetesObjects: kubeo, 2946 }) 2947 sim := simulation.NewSimulation(t, s, s.SetupProxy(tt.proxy)) 2948 xdstest.ValidateListeners(t, sim.Listeners) 2949 xdstest.ValidateRouteConfigurations(t, sim.Routes) 2950 r := xdstest.ExtractRouteConfigurations(sim.Routes) 2951 vh := r[tt.routeName] 2952 exp := tt.expected 2953 if variant == "httproute" && tt.expectedGateway != nil { 2954 exp = tt.expectedGateway 2955 } 2956 if vh == nil && exp != nil { 2957 t.Fatalf("route %q not found, have %v", tt.routeName, xdstest.MapKeys(r)) 2958 } 2959 gotHosts := xdstest.ExtractVirtualHosts(vh) 2960 for wk, wv := range exp { 2961 got := gotHosts[wk] 2962 if !reflect.DeepEqual(wv, got) { 2963 t.Errorf("%q: wanted %v, got %v (had %v)", wk, wv, got, xdstest.MapKeys(gotHosts)) 2964 } 2965 } 2966 }) 2967 } 2968 }) 2969 } 2970 }