| // Copyright Istio Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package v1alpha3_test |
| |
| import ( |
| "encoding/json" |
| "fmt" |
| "reflect" |
| "sort" |
| "strings" |
| "testing" |
| ) |
| |
| import ( |
| cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" |
| endpoint "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3" |
| listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" |
| tls "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" |
| meshconfig "istio.io/api/mesh/v1alpha1" |
| networking "istio.io/api/networking/v1alpha3" |
| ) |
| |
| import ( |
| "github.com/apache/dubbo-go-pixiu/pilot/pkg/features" |
| "github.com/apache/dubbo-go-pixiu/pilot/pkg/model" |
| "github.com/apache/dubbo-go-pixiu/pilot/pkg/networking/core/v1alpha3" |
| "github.com/apache/dubbo-go-pixiu/pilot/pkg/networking/util" |
| "github.com/apache/dubbo-go-pixiu/pilot/pkg/simulation" |
| "github.com/apache/dubbo-go-pixiu/pilot/pkg/xds" |
| "github.com/apache/dubbo-go-pixiu/pilot/test/xdstest" |
| "github.com/apache/dubbo-go-pixiu/pkg/config" |
| "github.com/apache/dubbo-go-pixiu/pkg/config/host" |
| "github.com/apache/dubbo-go-pixiu/pkg/config/mesh" |
| "github.com/apache/dubbo-go-pixiu/pkg/config/protocol" |
| "github.com/apache/dubbo-go-pixiu/pkg/config/schema/gvk" |
| "github.com/apache/dubbo-go-pixiu/pkg/test" |
| "github.com/apache/dubbo-go-pixiu/pkg/test/util/tmpl" |
| "github.com/apache/dubbo-go-pixiu/pkg/util/protomarshal" |
| ) |
| |
| func flattenInstances(il ...[]*model.ServiceInstance) []*model.ServiceInstance { |
| ret := []*model.ServiceInstance{} |
| for _, i := range il { |
| ret = append(ret, i...) |
| } |
| return ret |
| } |
| |
| func makeInstances(proxy *model.Proxy, svc *model.Service, servicePort int, targetPort int) []*model.ServiceInstance { |
| ret := []*model.ServiceInstance{} |
| for _, p := range svc.Ports { |
| if p.Port != servicePort { |
| continue |
| } |
| ret = append(ret, &model.ServiceInstance{ |
| Service: svc, |
| ServicePort: p, |
| Endpoint: &model.IstioEndpoint{ |
| Address: proxy.IPAddresses[0], |
| ServicePortName: p.Name, |
| EndpointPort: uint32(targetPort), |
| }, |
| }) |
| } |
| return ret |
| } |
| |
| func TestInboundClusters(t *testing.T) { |
| proxy := &model.Proxy{ |
| IPAddresses: []string{"1.2.3.4"}, |
| Metadata: &model.NodeMetadata{}, |
| } |
| service := &model.Service{ |
| Hostname: host.Name("backend.default.svc.cluster.local"), |
| DefaultAddress: "1.1.1.1", |
| Ports: model.PortList{&model.Port{ |
| Name: "default", |
| Port: 80, |
| Protocol: protocol.HTTP, |
| }, &model.Port{ |
| Name: "other", |
| Port: 81, |
| Protocol: protocol.HTTP, |
| }}, |
| Resolution: model.ClientSideLB, |
| } |
| serviceAlt := &model.Service{ |
| Hostname: host.Name("backend-alt.default.svc.cluster.local"), |
| DefaultAddress: "1.1.1.2", |
| Ports: model.PortList{&model.Port{ |
| Name: "default", |
| Port: 80, |
| Protocol: protocol.HTTP, |
| }, &model.Port{ |
| Name: "other", |
| Port: 81, |
| Protocol: protocol.HTTP, |
| }}, |
| Resolution: model.ClientSideLB, |
| } |
| |
| cases := []struct { |
| name string |
| configs []config.Config |
| services []*model.Service |
| instances []*model.ServiceInstance |
| // Assertions |
| clusters map[string][]string |
| telemetry map[string][]string |
| proxy *model.Proxy |
| disableInboundPassthrough bool |
| }{ |
| // Proxy 1.8.1+ tests |
| {name: "empty"}, |
| {name: "empty service", services: []*model.Service{service}}, |
| { |
| name: "single service, partial instance", |
| services: []*model.Service{service}, |
| instances: makeInstances(proxy, service, 80, 8080), |
| clusters: map[string][]string{ |
| "inbound|8080||": nil, |
| }, |
| telemetry: map[string][]string{ |
| "inbound|8080||": {string(service.Hostname)}, |
| }, |
| }, |
| { |
| name: "single service, multiple instance", |
| services: []*model.Service{service}, |
| instances: flattenInstances( |
| makeInstances(proxy, service, 80, 8080), |
| makeInstances(proxy, service, 81, 8081)), |
| clusters: map[string][]string{ |
| "inbound|8080||": nil, |
| "inbound|8081||": nil, |
| }, |
| telemetry: map[string][]string{ |
| "inbound|8080||": {string(service.Hostname)}, |
| "inbound|8081||": {string(service.Hostname)}, |
| }, |
| }, |
| { |
| name: "multiple services with same service port, different target", |
| services: []*model.Service{service, serviceAlt}, |
| instances: flattenInstances( |
| makeInstances(proxy, service, 80, 8080), |
| makeInstances(proxy, service, 81, 8081), |
| makeInstances(proxy, serviceAlt, 80, 8082), |
| makeInstances(proxy, serviceAlt, 81, 8083)), |
| clusters: map[string][]string{ |
| "inbound|8080||": nil, |
| "inbound|8081||": nil, |
| "inbound|8082||": nil, |
| "inbound|8083||": nil, |
| }, |
| telemetry: map[string][]string{ |
| "inbound|8080||": {string(service.Hostname)}, |
| "inbound|8081||": {string(service.Hostname)}, |
| "inbound|8082||": {string(serviceAlt.Hostname)}, |
| "inbound|8083||": {string(serviceAlt.Hostname)}, |
| }, |
| }, |
| { |
| name: "multiple services with same service port and target", |
| services: []*model.Service{service, serviceAlt}, |
| instances: flattenInstances( |
| makeInstances(proxy, service, 80, 8080), |
| makeInstances(proxy, service, 81, 8081), |
| makeInstances(proxy, serviceAlt, 80, 8080), |
| makeInstances(proxy, serviceAlt, 81, 8081)), |
| clusters: map[string][]string{ |
| "inbound|8080||": nil, |
| "inbound|8081||": nil, |
| }, |
| telemetry: map[string][]string{ |
| "inbound|8080||": {string(serviceAlt.Hostname), string(service.Hostname)}, |
| "inbound|8081||": {string(serviceAlt.Hostname), string(service.Hostname)}, |
| }, |
| }, |
| { |
| name: "ingress to same port", |
| configs: []config.Config{ |
| { |
| Meta: config.Meta{GroupVersionKind: gvk.Sidecar, Namespace: "default", Name: "sidecar"}, |
| Spec: &networking.Sidecar{Ingress: []*networking.IstioIngressListener{{ |
| Port: &networking.Port{ |
| Number: 80, |
| Protocol: "HTTP", |
| Name: "http", |
| }, |
| DefaultEndpoint: "127.0.0.1:80", |
| }}}, |
| }, |
| }, |
| clusters: map[string][]string{ |
| "inbound|80||": {"127.0.0.1:80"}, |
| }, |
| }, |
| { |
| name: "ingress to different port", |
| configs: []config.Config{ |
| { |
| Meta: config.Meta{GroupVersionKind: gvk.Sidecar, Namespace: "default", Name: "sidecar"}, |
| Spec: &networking.Sidecar{Ingress: []*networking.IstioIngressListener{{ |
| Port: &networking.Port{ |
| Number: 80, |
| Protocol: "HTTP", |
| Name: "http", |
| }, |
| DefaultEndpoint: "127.0.0.1:8080", |
| }}}, |
| }, |
| }, |
| clusters: map[string][]string{ |
| "inbound|80||": {"127.0.0.1:8080"}, |
| }, |
| }, |
| { |
| name: "ingress to instance IP", |
| configs: []config.Config{ |
| { |
| Meta: config.Meta{GroupVersionKind: gvk.Sidecar, Namespace: "default", Name: "sidecar"}, |
| Spec: &networking.Sidecar{Ingress: []*networking.IstioIngressListener{{ |
| Port: &networking.Port{ |
| Number: 80, |
| Protocol: "HTTP", |
| Name: "http", |
| }, |
| DefaultEndpoint: "0.0.0.0:8080", |
| }}}, |
| }, |
| }, |
| clusters: map[string][]string{ |
| "inbound|80||": {"1.2.3.4:8080"}, |
| }, |
| }, |
| { |
| name: "ingress without default endpoint", |
| configs: []config.Config{ |
| { |
| Meta: config.Meta{GroupVersionKind: gvk.Sidecar, Namespace: "default", Name: "sidecar"}, |
| Spec: &networking.Sidecar{Ingress: []*networking.IstioIngressListener{{ |
| Port: &networking.Port{ |
| Number: 80, |
| Protocol: "HTTP", |
| Name: "http", |
| }, |
| }}}, |
| }, |
| }, |
| clusters: map[string][]string{ |
| "inbound|80||": nil, |
| }, |
| }, |
| { |
| name: "ingress to socket", |
| configs: []config.Config{ |
| { |
| Meta: config.Meta{GroupVersionKind: gvk.Sidecar, Namespace: "default", Name: "sidecar"}, |
| Spec: &networking.Sidecar{Ingress: []*networking.IstioIngressListener{{ |
| Port: &networking.Port{ |
| Number: 80, |
| Protocol: "HTTP", |
| Name: "http", |
| }, |
| DefaultEndpoint: "unix:///socket", |
| }}}, |
| }, |
| }, |
| clusters: map[string][]string{ |
| "inbound|80||": {"/socket"}, |
| }, |
| }, |
| { |
| name: "multiple ingress", |
| configs: []config.Config{ |
| { |
| Meta: config.Meta{GroupVersionKind: gvk.Sidecar, Namespace: "default", Name: "sidecar"}, |
| Spec: &networking.Sidecar{Ingress: []*networking.IstioIngressListener{ |
| { |
| Port: &networking.Port{ |
| Number: 80, |
| Protocol: "HTTP", |
| Name: "http", |
| }, |
| DefaultEndpoint: "127.0.0.1:8080", |
| }, |
| { |
| Port: &networking.Port{ |
| Number: 81, |
| Protocol: "HTTP", |
| Name: "http", |
| }, |
| DefaultEndpoint: "127.0.0.1:8080", |
| }, |
| }}, |
| }, |
| }, |
| clusters: map[string][]string{ |
| "inbound|80||": {"127.0.0.1:8080"}, |
| "inbound|81||": {"127.0.0.1:8080"}, |
| }, |
| }, |
| |
| // Disable inbound passthrough |
| { |
| name: "single service, partial instance", |
| services: []*model.Service{service}, |
| instances: makeInstances(proxy, service, 80, 8080), |
| clusters: map[string][]string{ |
| "inbound|8080||": {"127.0.0.1:8080"}, |
| }, |
| telemetry: map[string][]string{ |
| "inbound|8080||": {string(service.Hostname)}, |
| }, |
| disableInboundPassthrough: true, |
| }, |
| { |
| name: "single service, multiple instance", |
| services: []*model.Service{service}, |
| instances: flattenInstances( |
| makeInstances(proxy, service, 80, 8080), |
| makeInstances(proxy, service, 81, 8081)), |
| clusters: map[string][]string{ |
| "inbound|8080||": {"127.0.0.1:8080"}, |
| "inbound|8081||": {"127.0.0.1:8081"}, |
| }, |
| telemetry: map[string][]string{ |
| "inbound|8080||": {string(service.Hostname)}, |
| "inbound|8081||": {string(service.Hostname)}, |
| }, |
| disableInboundPassthrough: true, |
| }, |
| { |
| name: "multiple services with same service port, different target", |
| services: []*model.Service{service, serviceAlt}, |
| instances: flattenInstances( |
| makeInstances(proxy, service, 80, 8080), |
| makeInstances(proxy, service, 81, 8081), |
| makeInstances(proxy, serviceAlt, 80, 8082), |
| makeInstances(proxy, serviceAlt, 81, 8083)), |
| clusters: map[string][]string{ |
| "inbound|8080||": {"127.0.0.1:8080"}, |
| "inbound|8081||": {"127.0.0.1:8081"}, |
| "inbound|8082||": {"127.0.0.1:8082"}, |
| "inbound|8083||": {"127.0.0.1:8083"}, |
| }, |
| telemetry: map[string][]string{ |
| "inbound|8080||": {string(service.Hostname)}, |
| "inbound|8081||": {string(service.Hostname)}, |
| "inbound|8082||": {string(serviceAlt.Hostname)}, |
| "inbound|8083||": {string(serviceAlt.Hostname)}, |
| }, |
| disableInboundPassthrough: true, |
| }, |
| { |
| name: "multiple services with same service port and target", |
| services: []*model.Service{service, serviceAlt}, |
| instances: flattenInstances( |
| makeInstances(proxy, service, 80, 8080), |
| makeInstances(proxy, service, 81, 8081), |
| makeInstances(proxy, serviceAlt, 80, 8080), |
| makeInstances(proxy, serviceAlt, 81, 8081)), |
| clusters: map[string][]string{ |
| "inbound|8080||": {"127.0.0.1:8080"}, |
| "inbound|8081||": {"127.0.0.1:8081"}, |
| }, |
| telemetry: map[string][]string{ |
| "inbound|8080||": {string(serviceAlt.Hostname), string(service.Hostname)}, |
| "inbound|8081||": {string(serviceAlt.Hostname), string(service.Hostname)}, |
| }, |
| disableInboundPassthrough: true, |
| }, |
| } |
| for _, tt := range cases { |
| name := tt.name |
| if tt.proxy == nil { |
| tt.proxy = proxy |
| } else { |
| name += "-" + tt.proxy.Metadata.IstioVersion |
| } |
| |
| if tt.disableInboundPassthrough { |
| name += "-disableinbound" |
| } |
| t.Run(name, func(t *testing.T) { |
| test.SetBoolForTest(t, &features.EnableInboundPassthrough, !tt.disableInboundPassthrough) |
| s := v1alpha3.NewConfigGenTest(t, v1alpha3.TestOptions{ |
| Services: tt.services, |
| Instances: tt.instances, |
| Configs: tt.configs, |
| }) |
| sim := simulation.NewSimulationFromConfigGen(t, s, s.SetupProxy(tt.proxy)) |
| |
| clusters := xdstest.FilterClusters(sim.Clusters, func(c *cluster.Cluster) bool { |
| return strings.HasPrefix(c.Name, "inbound") |
| }) |
| if len(s.PushContext().ProxyStatus) != 0 { |
| // TODO make this fatal, once inbound conflict is silenced |
| t.Logf("got unexpected error: %+v", s.PushContext().ProxyStatus) |
| } |
| cmap := xdstest.ExtractClusters(clusters) |
| got := xdstest.MapKeys(cmap) |
| |
| // Check we have all expected clusters |
| if !reflect.DeepEqual(xdstest.MapKeys(tt.clusters), got) { |
| t.Errorf("expected clusters: %v, got: %v", xdstest.MapKeys(tt.clusters), got) |
| } |
| |
| for cname, c := range cmap { |
| // Check the upstream endpoints match |
| got := xdstest.ExtractLoadAssignments([]*endpoint.ClusterLoadAssignment{c.GetLoadAssignment()})[cname] |
| if !reflect.DeepEqual(tt.clusters[cname], got) { |
| t.Errorf("%v: expected endpoints %v, got %v", cname, tt.clusters[cname], got) |
| } |
| gotTelemetry := extractClusterMetadataServices(t, c) |
| if !reflect.DeepEqual(tt.telemetry[cname], gotTelemetry) { |
| t.Errorf("%v: expected telemetry services %v, got %v", cname, tt.telemetry[cname], gotTelemetry) |
| } |
| |
| // simulate an actual call, this ensures we are aligned with the inbound listener configuration |
| _, _, hostname, port := model.ParseSubsetKey(cname) |
| if tt.proxy.Metadata.IstioVersion != "" { |
| // This doesn't work with the legacy proxies which have issues (https://github.com/istio/istio/issues/29199) |
| for _, i := range tt.instances { |
| if len(hostname) > 0 && i.Service.Hostname != hostname { |
| continue |
| } |
| if i.ServicePort.Port == port { |
| port = int(i.Endpoint.EndpointPort) |
| } |
| } |
| } |
| sim.Run(simulation.Call{ |
| Port: port, |
| Protocol: simulation.HTTP, |
| Address: "1.2.3.4", |
| CallMode: simulation.CallModeInbound, |
| }).Matches(t, simulation.Result{ |
| ClusterMatched: cname, |
| }) |
| } |
| }) |
| } |
| } |
| |
| type clusterServicesMetadata struct { |
| Services []struct { |
| Host string |
| Name string |
| Namespace string |
| } |
| } |
| |
| func extractClusterMetadataServices(t test.Failer, c *cluster.Cluster) []string { |
| got := c.GetMetadata().GetFilterMetadata()[util.IstioMetadataKey] |
| if got == nil { |
| return nil |
| } |
| s, err := protomarshal.Marshal(got) |
| if err != nil { |
| t.Fatal(err) |
| } |
| meta := clusterServicesMetadata{} |
| if err := json.Unmarshal(s, &meta); err != nil { |
| t.Fatal(err) |
| } |
| res := []string{} |
| for _, m := range meta.Services { |
| res = append(res, m.Host) |
| } |
| return res |
| } |
| |
| func mtlsMode(m string) string { |
| return fmt.Sprintf(`apiVersion: security.istio.io/v1beta1 |
| kind: PeerAuthentication |
| metadata: |
| name: default |
| namespace: dubbo-system |
| spec: |
| mtls: |
| mode: %s |
| `, m) |
| } |
| |
| func TestInbound(t *testing.T) { |
| svc := ` |
| apiVersion: networking.istio.io/v1alpha3 |
| kind: ServiceEntry |
| metadata: |
| name: se |
| spec: |
| hosts: |
| - foo.bar |
| endpoints: |
| - address: 1.1.1.1 |
| location: MESH_INTERNAL |
| resolution: STATIC |
| ports: |
| - name: tcp |
| number: 70 |
| protocol: TCP |
| - name: http |
| number: 80 |
| protocol: HTTP |
| - name: auto |
| number: 81 |
| --- |
| ` |
| cases := []struct { |
| Name string |
| Call simulation.Call |
| Disabled simulation.Result |
| Permissive simulation.Result |
| Strict simulation.Result |
| }{ |
| { |
| Name: "tcp", |
| Call: simulation.Call{ |
| Port: 70, |
| Protocol: simulation.TCP, |
| CallMode: simulation.CallModeInbound, |
| }, |
| Disabled: simulation.Result{ |
| ClusterMatched: "inbound|70||", |
| }, |
| Permissive: simulation.Result{ |
| ClusterMatched: "inbound|70||", |
| }, |
| Strict: simulation.Result{ |
| // Plaintext to strict, should fail |
| Error: simulation.ErrNoFilterChain, |
| }, |
| }, |
| { |
| Name: "http to tcp", |
| Call: simulation.Call{ |
| Port: 70, |
| Protocol: simulation.HTTP, |
| CallMode: simulation.CallModeInbound, |
| }, |
| Disabled: simulation.Result{ |
| ClusterMatched: "inbound|70||", |
| }, |
| Permissive: simulation.Result{ |
| ClusterMatched: "inbound|70||", |
| }, |
| Strict: simulation.Result{ |
| // Plaintext to strict, should fail |
| Error: simulation.ErrNoFilterChain, |
| }, |
| }, |
| { |
| Name: "tls to tcp", |
| Call: simulation.Call{ |
| Port: 70, |
| Protocol: simulation.TCP, |
| TLS: simulation.TLS, |
| CallMode: simulation.CallModeInbound, |
| }, |
| Disabled: simulation.Result{ |
| ClusterMatched: "inbound|70||", |
| }, |
| Permissive: simulation.Result{ |
| ClusterMatched: "inbound|70||", |
| }, |
| Strict: simulation.Result{ |
| // TLS, but not mTLS |
| Error: simulation.ErrMTLSError, |
| }, |
| }, |
| { |
| Name: "https to tcp", |
| Call: simulation.Call{ |
| Port: 70, |
| Protocol: simulation.HTTP, |
| TLS: simulation.TLS, |
| CallMode: simulation.CallModeInbound, |
| }, |
| Disabled: simulation.Result{ |
| ClusterMatched: "inbound|70||", |
| }, |
| Permissive: simulation.Result{ |
| ClusterMatched: "inbound|70||", |
| }, |
| Strict: simulation.Result{ |
| // TLS, but not mTLS |
| Error: simulation.ErrMTLSError, |
| }, |
| }, |
| { |
| Name: "mtls tcp to tcp", |
| Call: simulation.Call{ |
| Port: 70, |
| Protocol: simulation.TCP, |
| TLS: simulation.MTLS, |
| CallMode: simulation.CallModeInbound, |
| }, |
| Disabled: simulation.Result{ |
| // This is probably a user error, but there is no reason we should block mTLS traffic |
| // we just will not terminate it |
| ClusterMatched: "inbound|70||", |
| }, |
| Permissive: simulation.Result{ |
| ClusterMatched: "inbound|70||", |
| }, |
| Strict: simulation.Result{ |
| ClusterMatched: "inbound|70||", |
| }, |
| }, |
| { |
| Name: "mtls http to tcp", |
| Call: simulation.Call{ |
| Port: 70, |
| Protocol: simulation.HTTP, |
| TLS: simulation.MTLS, |
| CallMode: simulation.CallModeInbound, |
| }, |
| Disabled: simulation.Result{ |
| // This is probably a user error, but there is no reason we should block mTLS traffic |
| // we just will not terminate it |
| ClusterMatched: "inbound|70||", |
| }, |
| Permissive: simulation.Result{ |
| ClusterMatched: "inbound|70||", |
| }, |
| Strict: simulation.Result{ |
| ClusterMatched: "inbound|70||", |
| }, |
| }, |
| { |
| Name: "http", |
| Call: simulation.Call{ |
| Port: 80, |
| Protocol: simulation.HTTP, |
| CallMode: simulation.CallModeInbound, |
| }, |
| Disabled: simulation.Result{ |
| VirtualHostMatched: "inbound|http|80", |
| ClusterMatched: "inbound|80||", |
| }, |
| Permissive: simulation.Result{ |
| VirtualHostMatched: "inbound|http|80", |
| ClusterMatched: "inbound|80||", |
| }, |
| Strict: simulation.Result{ |
| // Plaintext to strict, should fail |
| Error: simulation.ErrNoFilterChain, |
| }, |
| }, |
| { |
| Name: "tls to http", |
| Call: simulation.Call{ |
| Port: 80, |
| Protocol: simulation.TCP, |
| TLS: simulation.TLS, |
| CallMode: simulation.CallModeInbound, |
| }, |
| Disabled: simulation.Result{ |
| // TLS is not terminated, so we will attempt to decode as HTTP and fail |
| Error: simulation.ErrProtocolError, |
| }, |
| Permissive: simulation.Result{ |
| // This could also be a protocol error. In the current implementation, we choose not |
| // to create a match since if we did it would just be rejected in HCM; no match |
| // is more performant |
| Error: simulation.ErrNoFilterChain, |
| }, |
| Strict: simulation.Result{ |
| // TLS, but not mTLS |
| Error: simulation.ErrMTLSError, |
| }, |
| }, |
| { |
| Name: "https to http", |
| Call: simulation.Call{ |
| Port: 80, |
| Protocol: simulation.HTTP, |
| TLS: simulation.TLS, |
| CallMode: simulation.CallModeInbound, |
| }, |
| Disabled: simulation.Result{ |
| // TLS is not terminated, so we will attempt to decode as HTTP and fail |
| Error: simulation.ErrProtocolError, |
| }, |
| Permissive: simulation.Result{ |
| // This could also be a protocol error. In the current implementation, we choose not |
| // to create a match since if we did it would just be rejected in HCM; no match |
| // is more performant |
| Error: simulation.ErrNoFilterChain, |
| }, |
| Strict: simulation.Result{ |
| // TLS, but not mTLS |
| Error: simulation.ErrMTLSError, |
| }, |
| }, |
| { |
| Name: "mtls to http", |
| Call: simulation.Call{ |
| Port: 80, |
| Protocol: simulation.HTTP, |
| TLS: simulation.MTLS, |
| CallMode: simulation.CallModeInbound, |
| }, |
| Disabled: simulation.Result{ |
| // TLS is not terminated, so we will attempt to decode as HTTP and fail |
| Error: simulation.ErrProtocolError, |
| }, |
| Permissive: simulation.Result{ |
| VirtualHostMatched: "inbound|http|80", |
| ClusterMatched: "inbound|80||", |
| }, |
| Strict: simulation.Result{ |
| VirtualHostMatched: "inbound|http|80", |
| ClusterMatched: "inbound|80||", |
| }, |
| }, |
| { |
| Name: "tcp to http", |
| Call: simulation.Call{ |
| Port: 80, |
| Protocol: simulation.TCP, |
| CallMode: simulation.CallModeInbound, |
| }, |
| Disabled: simulation.Result{ |
| // Expected, the port only supports HTTP |
| Error: simulation.ErrProtocolError, |
| }, |
| Permissive: simulation.Result{ |
| // Expected, the port only supports HTTP |
| Error: simulation.ErrProtocolError, |
| }, |
| Strict: simulation.Result{ |
| // Plaintext to strict fails |
| Error: simulation.ErrNoFilterChain, |
| }, |
| }, |
| { |
| Name: "auto port http", |
| Call: simulation.Call{ |
| Port: 81, |
| Protocol: simulation.HTTP, |
| CallMode: simulation.CallModeInbound, |
| }, |
| Disabled: simulation.Result{ |
| VirtualHostMatched: "inbound|http|81", |
| ClusterMatched: "inbound|81||", |
| }, |
| Permissive: simulation.Result{ |
| VirtualHostMatched: "inbound|http|81", |
| ClusterMatched: "inbound|81||", |
| }, |
| Strict: simulation.Result{ |
| // Plaintext to strict fails |
| Error: simulation.ErrNoFilterChain, |
| }, |
| }, |
| { |
| Name: "auto port http2", |
| Call: simulation.Call{ |
| Port: 81, |
| Protocol: simulation.HTTP2, |
| CallMode: simulation.CallModeInbound, |
| }, |
| Disabled: simulation.Result{ |
| VirtualHostMatched: "inbound|http|81", |
| ClusterMatched: "inbound|81||", |
| }, |
| Permissive: simulation.Result{ |
| VirtualHostMatched: "inbound|http|81", |
| ClusterMatched: "inbound|81||", |
| }, |
| Strict: simulation.Result{ |
| // Plaintext to strict fails |
| Error: simulation.ErrNoFilterChain, |
| }, |
| }, |
| { |
| Name: "auto port tcp", |
| Call: simulation.Call{ |
| Port: 81, |
| Protocol: simulation.TCP, |
| CallMode: simulation.CallModeInbound, |
| }, |
| Disabled: simulation.Result{ |
| ListenerMatched: "virtualInbound", |
| FilterChainMatched: "0.0.0.0_81", |
| ClusterMatched: "inbound|81||", |
| StrictMatch: true, |
| }, |
| Permissive: simulation.Result{ |
| ListenerMatched: "virtualInbound", |
| FilterChainMatched: "0.0.0.0_81", |
| ClusterMatched: "inbound|81||", |
| StrictMatch: true, |
| }, |
| Strict: simulation.Result{ |
| // Plaintext to strict fails |
| Error: simulation.ErrNoFilterChain, |
| }, |
| }, |
| { |
| Name: "tls to auto port", |
| Call: simulation.Call{ |
| Port: 81, |
| Protocol: simulation.TCP, |
| TLS: simulation.TLS, |
| CallMode: simulation.CallModeInbound, |
| }, |
| Disabled: simulation.Result{ |
| // Should go through the TCP chains |
| ListenerMatched: "virtualInbound", |
| FilterChainMatched: "0.0.0.0_81", |
| ClusterMatched: "inbound|81||", |
| StrictMatch: true, |
| }, |
| Permissive: simulation.Result{ |
| // Should go through the TCP chains |
| ListenerMatched: "virtualInbound", |
| FilterChainMatched: "0.0.0.0_81", |
| ClusterMatched: "inbound|81||", |
| StrictMatch: true, |
| }, |
| Strict: simulation.Result{ |
| // Tls, but not mTLS |
| Error: simulation.ErrMTLSError, |
| }, |
| }, |
| { |
| Name: "https to auto port", |
| Call: simulation.Call{ |
| Port: 81, |
| Protocol: simulation.HTTP, |
| TLS: simulation.TLS, |
| CallMode: simulation.CallModeInbound, |
| }, |
| Disabled: simulation.Result{ |
| // Should go through the TCP chains |
| ListenerMatched: "virtualInbound", |
| FilterChainMatched: "0.0.0.0_81", |
| ClusterMatched: "inbound|81||", |
| StrictMatch: true, |
| }, |
| Permissive: simulation.Result{ |
| // Should go through the TCP chains |
| ListenerMatched: "virtualInbound", |
| FilterChainMatched: "0.0.0.0_81", |
| ClusterMatched: "inbound|81||", |
| StrictMatch: true, |
| }, |
| Strict: simulation.Result{ |
| // Tls, but not mTLS |
| Error: simulation.ErrMTLSError, |
| }, |
| }, |
| { |
| Name: "mtls tcp to auto port", |
| Call: simulation.Call{ |
| Port: 81, |
| Protocol: simulation.TCP, |
| TLS: simulation.MTLS, |
| CallMode: simulation.CallModeInbound, |
| }, |
| Disabled: simulation.Result{ |
| // This is probably a user error, but there is no reason we should block mTLS traffic |
| // we just will not terminate it |
| ClusterMatched: "inbound|81||", |
| }, |
| Permissive: simulation.Result{ |
| // Should go through the TCP chains |
| ListenerMatched: "virtualInbound", |
| FilterChainMatched: "0.0.0.0_81", |
| ClusterMatched: "inbound|81||", |
| StrictMatch: true, |
| }, |
| Strict: simulation.Result{ |
| // Should go through the TCP chains |
| ListenerMatched: "virtualInbound", |
| FilterChainMatched: "0.0.0.0_81", |
| ClusterMatched: "inbound|81||", |
| StrictMatch: true, |
| }, |
| }, |
| { |
| Name: "mtls http to auto port", |
| Call: simulation.Call{ |
| Port: 81, |
| Protocol: simulation.HTTP, |
| TLS: simulation.MTLS, |
| CallMode: simulation.CallModeInbound, |
| }, |
| Disabled: simulation.Result{ |
| // This is probably a user error, but there is no reason we should block mTLS traffic |
| // we just will not terminate it |
| ClusterMatched: "inbound|81||", |
| }, |
| Permissive: simulation.Result{ |
| // Should go through the HTTP chains |
| VirtualHostMatched: "inbound|http|81", |
| ClusterMatched: "inbound|81||", |
| }, |
| Strict: simulation.Result{ |
| // Should go through the HTTP chains |
| VirtualHostMatched: "inbound|http|81", |
| ClusterMatched: "inbound|81||", |
| }, |
| }, |
| { |
| Name: "passthrough http", |
| Call: simulation.Call{ |
| Address: "1.2.3.4", |
| Port: 82, |
| Protocol: simulation.HTTP, |
| CallMode: simulation.CallModeInbound, |
| }, |
| Disabled: simulation.Result{ |
| ClusterMatched: "InboundPassthroughClusterIpv4", |
| FilterChainMatched: "virtualInbound-catchall-http", |
| }, |
| Permissive: simulation.Result{ |
| ClusterMatched: "InboundPassthroughClusterIpv4", |
| FilterChainMatched: "virtualInbound-catchall-http", |
| }, |
| Strict: simulation.Result{ |
| // Plaintext to strict fails |
| Error: simulation.ErrNoFilterChain, |
| }, |
| }, |
| { |
| Name: "passthrough tcp", |
| Call: simulation.Call{ |
| Address: "1.2.3.4", |
| Port: 82, |
| Protocol: simulation.TCP, |
| CallMode: simulation.CallModeInbound, |
| }, |
| Disabled: simulation.Result{ |
| ClusterMatched: "InboundPassthroughClusterIpv4", |
| FilterChainMatched: "virtualInbound", |
| }, |
| Permissive: simulation.Result{ |
| ClusterMatched: "InboundPassthroughClusterIpv4", |
| FilterChainMatched: "virtualInbound", |
| }, |
| Strict: simulation.Result{ |
| // Plaintext to strict fails |
| Error: simulation.ErrNoFilterChain, |
| }, |
| }, |
| { |
| Name: "passthrough tls", |
| Call: simulation.Call{ |
| Address: "1.2.3.4", |
| Port: 82, |
| Protocol: simulation.TCP, |
| TLS: simulation.TLS, |
| CallMode: simulation.CallModeInbound, |
| }, |
| Disabled: simulation.Result{ |
| ClusterMatched: "InboundPassthroughClusterIpv4", |
| FilterChainMatched: "virtualInbound", |
| }, |
| Permissive: simulation.Result{ |
| ClusterMatched: "InboundPassthroughClusterIpv4", |
| }, |
| Strict: simulation.Result{ |
| // tls, but not mTLS |
| Error: simulation.ErrMTLSError, |
| }, |
| }, |
| { |
| Name: "passthrough https", |
| Call: simulation.Call{ |
| Address: "1.2.3.4", |
| Port: 82, |
| Protocol: simulation.HTTP, |
| TLS: simulation.TLS, |
| CallMode: simulation.CallModeInbound, |
| }, |
| Disabled: simulation.Result{ |
| ClusterMatched: "InboundPassthroughClusterIpv4", |
| }, |
| Permissive: simulation.Result{ |
| ClusterMatched: "InboundPassthroughClusterIpv4", |
| }, |
| Strict: simulation.Result{ |
| // tls, but not mTLS |
| Error: simulation.ErrMTLSError, |
| }, |
| }, |
| { |
| Name: "passthrough mtls", |
| Call: simulation.Call{ |
| Address: "1.2.3.4", |
| Port: 82, |
| Protocol: simulation.HTTP, |
| TLS: simulation.MTLS, |
| CallMode: simulation.CallModeInbound, |
| }, |
| Disabled: simulation.Result{ |
| ClusterMatched: "InboundPassthroughClusterIpv4", |
| }, |
| Permissive: simulation.Result{ |
| ClusterMatched: "InboundPassthroughClusterIpv4", |
| }, |
| Strict: simulation.Result{ |
| ClusterMatched: "InboundPassthroughClusterIpv4", |
| }, |
| }, |
| } |
| t.Run("Disable", func(t *testing.T) { |
| calls := []simulation.Expect{} |
| for _, c := range cases { |
| calls = append(calls, simulation.Expect{ |
| Name: c.Name, |
| Call: c.Call, |
| Result: c.Disabled, |
| }) |
| } |
| runSimulationTest(t, nil, xds.FakeOptions{}, simulationTest{ |
| config: svc + mtlsMode("DISABLE"), |
| calls: calls, |
| }) |
| }) |
| |
| t.Run("Permissive", func(t *testing.T) { |
| calls := []simulation.Expect{} |
| for _, c := range cases { |
| calls = append(calls, simulation.Expect{ |
| Name: c.Name, |
| Call: c.Call, |
| Result: c.Permissive, |
| }) |
| } |
| runSimulationTest(t, nil, xds.FakeOptions{}, simulationTest{ |
| config: svc + mtlsMode("PERMISSIVE"), |
| calls: calls, |
| }) |
| }) |
| |
| t.Run("Strict", func(t *testing.T) { |
| calls := []simulation.Expect{} |
| for _, c := range cases { |
| calls = append(calls, simulation.Expect{ |
| Name: c.Name, |
| Call: c.Call, |
| Result: c.Strict, |
| }) |
| } |
| runSimulationTest(t, nil, xds.FakeOptions{}, simulationTest{ |
| config: svc + mtlsMode("STRICT"), |
| calls: calls, |
| }) |
| }) |
| } |
| |
| func TestHeadlessServices(t *testing.T) { |
| ports := ` |
| - name: http |
| port: 80 |
| - name: auto |
| port: 81 |
| - name: tcp |
| port: 82 |
| - name: tls |
| port: 83 |
| - name: https |
| port: 84` |
| |
| calls := []simulation.Expect{} |
| for _, call := range []simulation.Call{ |
| {Address: "1.2.3.4", Port: 80, Protocol: simulation.HTTP, HostHeader: "headless.default.svc.cluster.local"}, |
| |
| // Auto port should support any protocol |
| {Address: "1.2.3.4", Port: 81, Protocol: simulation.HTTP, HostHeader: "headless.default.svc.cluster.local"}, |
| {Address: "1.2.3.4", Port: 81, Protocol: simulation.HTTP, TLS: simulation.TLS, HostHeader: "headless.default.svc.cluster.local"}, |
| {Address: "1.2.3.4", Port: 81, Protocol: simulation.TCP, HostHeader: "headless.default.svc.cluster.local"}, |
| |
| {Address: "1.2.3.4", Port: 82, Protocol: simulation.TCP, HostHeader: "headless.default.svc.cluster.local"}, |
| |
| // Use short host name |
| {Address: "1.2.3.4", Port: 83, Protocol: simulation.TCP, TLS: simulation.TLS, HostHeader: "headless.default"}, |
| {Address: "1.2.3.4", Port: 84, Protocol: simulation.HTTP, TLS: simulation.TLS, HostHeader: "headless.default"}, |
| } { |
| calls = append(calls, simulation.Expect{ |
| Name: fmt.Sprintf("%s-%d", call.Protocol, call.Port), |
| Call: call, |
| Result: simulation.Result{ |
| ClusterMatched: fmt.Sprintf("outbound|%d||headless.default.svc.cluster.local", call.Port), |
| }, |
| }) |
| } |
| runSimulationTest(t, nil, xds.FakeOptions{}, simulationTest{ |
| kubeConfig: `apiVersion: v1 |
| kind: Service |
| metadata: |
| name: headless |
| namespace: default |
| spec: |
| clusterIP: None |
| selector: |
| app: headless |
| ports:` + ports + ` |
| --- |
| apiVersion: v1 |
| kind: Endpoints |
| metadata: |
| name: headless |
| namespace: default |
| subsets: |
| - addresses: |
| - ip: 1.2.3.4 |
| ports: |
| ` + ports, |
| calls: calls, |
| }, |
| ) |
| } |
| |
| func TestPassthroughTraffic(t *testing.T) { |
| calls := map[string]simulation.Call{} |
| for port := 80; port < 87; port++ { |
| for _, call := range []simulation.Call{ |
| {Port: port, Protocol: simulation.HTTP, TLS: simulation.Plaintext, HostHeader: "foo"}, |
| {Port: port, Protocol: simulation.HTTP, TLS: simulation.TLS, HostHeader: "foo"}, |
| {Port: port, Protocol: simulation.HTTP, TLS: simulation.TLS, HostHeader: "foo", Alpn: "http/1.1"}, |
| {Port: port, Protocol: simulation.TCP, TLS: simulation.Plaintext, HostHeader: "foo"}, |
| {Port: port, Protocol: simulation.HTTP2, TLS: simulation.TLS, HostHeader: "foo"}, |
| } { |
| suffix := "" |
| if call.Alpn != "" { |
| suffix = "-" + call.Alpn |
| } |
| calls[fmt.Sprintf("%v-%v-%v%v", call.Protocol, call.TLS, port, suffix)] = call |
| } |
| } |
| ports := ` |
| ports: |
| - name: http |
| number: 80 |
| protocol: HTTP |
| - name: auto |
| number: 81 |
| - name: tcp |
| number: 82 |
| protocol: TCP |
| - name: tls |
| number: 83 |
| protocol: TLS |
| - name: https |
| number: 84 |
| protocol: HTTPS |
| - name: grpc |
| number: 85 |
| protocol: GRPC |
| - name: h2 |
| number: 86 |
| protocol: HTTP2` |
| |
| isHTTPPort := func(p int) bool { |
| switch p { |
| case 80, 85, 86: |
| return true |
| default: |
| return false |
| } |
| } |
| isAutoPort := func(p int) bool { |
| switch p { |
| case 81: |
| return true |
| default: |
| return false |
| } |
| } |
| for _, tp := range []meshconfig.MeshConfig_OutboundTrafficPolicy_Mode{ |
| meshconfig.MeshConfig_OutboundTrafficPolicy_REGISTRY_ONLY, |
| meshconfig.MeshConfig_OutboundTrafficPolicy_ALLOW_ANY, |
| } { |
| t.Run(tp.String(), func(t *testing.T) { |
| o := xds.FakeOptions{ |
| MeshConfig: func() *meshconfig.MeshConfig { |
| m := mesh.DefaultMeshConfig() |
| m.OutboundTrafficPolicy.Mode = tp |
| return m |
| }(), |
| } |
| expectedCluster := map[meshconfig.MeshConfig_OutboundTrafficPolicy_Mode]string{ |
| meshconfig.MeshConfig_OutboundTrafficPolicy_REGISTRY_ONLY: util.BlackHoleCluster, |
| meshconfig.MeshConfig_OutboundTrafficPolicy_ALLOW_ANY: util.PassthroughCluster, |
| }[tp] |
| t.Run("with VIP", func(t *testing.T) { |
| testCalls := []simulation.Expect{} |
| for name, call := range calls { |
| e := simulation.Expect{ |
| Name: name, |
| Call: call, |
| Result: simulation.Result{ |
| ClusterMatched: expectedCluster, |
| }, |
| } |
| // For blackhole, we will 502 where possible instead of blackhole cluster |
| // This only works for HTTP on HTTP |
| if expectedCluster == util.BlackHoleCluster && call.IsHTTP() && isHTTPPort(call.Port) { |
| e.Result.ClusterMatched = "" |
| e.Result.VirtualHostMatched = util.BlackHole |
| } |
| testCalls = append(testCalls, e) |
| } |
| sort.Slice(testCalls, func(i, j int) bool { |
| return testCalls[i].Name < testCalls[j].Name |
| }) |
| runSimulationTest(t, nil, o, |
| simulationTest{ |
| config: ` |
| apiVersion: networking.istio.io/v1alpha3 |
| kind: ServiceEntry |
| metadata: |
| name: se |
| spec: |
| hosts: |
| - istio.io |
| addresses: [1.2.3.4] |
| location: MESH_EXTERNAL |
| resolution: DNS` + ports, |
| calls: testCalls, |
| }) |
| }) |
| t.Run("without VIP", func(t *testing.T) { |
| testCalls := []simulation.Expect{} |
| for name, call := range calls { |
| e := simulation.Expect{ |
| Name: name, |
| Call: call, |
| Result: simulation.Result{ |
| ClusterMatched: expectedCluster, |
| }, |
| } |
| // For blackhole, we will 502 where possible instead of blackhole cluster |
| // This only works for HTTP on HTTP |
| if expectedCluster == util.BlackHoleCluster && call.IsHTTP() && (isHTTPPort(call.Port) || isAutoPort(call.Port)) { |
| e.Result.ClusterMatched = "" |
| e.Result.VirtualHostMatched = util.BlackHole |
| } |
| // TCP without a VIP will capture everything. |
| // Auto without a VIP is similar, but HTTP happens to work because routing is done on header |
| if call.Port == 82 || (call.Port == 81 && !call.IsHTTP()) { |
| e.Result.Error = nil |
| e.Result.ClusterMatched = "" |
| } |
| testCalls = append(testCalls, e) |
| } |
| sort.Slice(testCalls, func(i, j int) bool { |
| return testCalls[i].Name < testCalls[j].Name |
| }) |
| runSimulationTest(t, nil, o, |
| simulationTest{ |
| config: ` |
| apiVersion: networking.istio.io/v1alpha3 |
| kind: ServiceEntry |
| metadata: |
| name: se |
| spec: |
| hosts: |
| - istio.io |
| location: MESH_EXTERNAL |
| resolution: DNS` + ports, |
| calls: testCalls, |
| }) |
| }) |
| }) |
| } |
| } |
| |
| func TestLoop(t *testing.T) { |
| runSimulationTest(t, nil, xds.FakeOptions{}, simulationTest{ |
| calls: []simulation.Expect{ |
| { |
| Name: "direct request to outbound port", |
| Call: simulation.Call{ |
| Port: 15001, |
| Protocol: simulation.TCP, |
| }, |
| Result: simulation.Result{ |
| // This request should be blocked |
| ClusterMatched: "BlackHoleCluster", |
| }, |
| }, |
| { |
| Name: "direct request to inbound port", |
| Call: simulation.Call{ |
| Port: 15006, |
| Protocol: simulation.TCP, |
| }, |
| Result: simulation.Result{ |
| // This request should be blocked |
| ClusterMatched: "BlackHoleCluster", |
| }, |
| }, |
| }, |
| }) |
| } |
| |
| func TestInboundSidecarTLSModes(t *testing.T) { |
| peerAuthConfig := func(m string) string { |
| return fmt.Sprintf(`apiVersion: security.istio.io/v1beta1 |
| kind: PeerAuthentication |
| metadata: |
| name: peer-auth |
| namespace: default |
| spec: |
| selector: |
| matchLabels: |
| app: foo |
| mtls: |
| mode: STRICT |
| portLevelMtls: |
| 9080: |
| mode: %s |
| --- |
| `, m) |
| } |
| sidecarSimple := func(protocol string) string { |
| return fmt.Sprintf(` |
| apiVersion: networking.istio.io/v1alpha3 |
| kind: Sidecar |
| metadata: |
| labels: |
| app: foo |
| name: sidecar |
| namespace: default |
| spec: |
| ingress: |
| - defaultEndpoint: 0.0.0.0:9080 |
| port: |
| name: tls |
| number: 9080 |
| protocol: %s |
| tls: |
| mode: SIMPLE |
| privateKey: "httpbinkey.pem" |
| serverCertificate: "httpbin.pem" |
| workloadSelector: |
| labels: |
| app: foo |
| --- |
| `, protocol) |
| } |
| sidecarMutual := func(protocol string) string { |
| return fmt.Sprintf(` |
| apiVersion: networking.istio.io/v1alpha3 |
| kind: Sidecar |
| metadata: |
| labels: |
| app: foo |
| name: sidecar |
| namespace: default |
| spec: |
| ingress: |
| - defaultEndpoint: 0.0.0.0:9080 |
| port: |
| name: tls |
| number: 9080 |
| protocol: %s |
| tls: |
| mode: MUTUAL |
| privateKey: "httpbinkey.pem" |
| serverCertificate: "httpbin.pem" |
| caCertificates: "rootCA.pem" |
| workloadSelector: |
| labels: |
| app: foo |
| --- |
| `, protocol) |
| } |
| expectedTLSContext := func(filterChain *listener.FilterChain) error { |
| tlsContext := &tls.DownstreamTlsContext{} |
| ts := filterChain.GetTransportSocket().GetTypedConfig() |
| if ts == nil { |
| return fmt.Errorf("expected transport socket for chain %v", filterChain.GetName()) |
| } |
| if err := ts.UnmarshalTo(tlsContext); err != nil { |
| return err |
| } |
| commonTLSContext := tlsContext.CommonTlsContext |
| if len(commonTLSContext.TlsCertificateSdsSecretConfigs) == 0 { |
| return fmt.Errorf("expected tls certificates") |
| } |
| if commonTLSContext.TlsCertificateSdsSecretConfigs[0].Name != "file-cert:httpbin.pem~httpbinkey.pem" { |
| return fmt.Errorf("expected certificate httpbin.pem, actual %s", commonTLSContext.TlsCertificates[0].CertificateChain.String()) |
| } |
| if tlsContext.RequireClientCertificate.Value { |
| return fmt.Errorf("expected RequireClientCertificate to be false") |
| } |
| return nil |
| } |
| |
| mkCall := func(port int, protocol simulation.Protocol, |
| tls simulation.TLSMode, validations []simulation.CustomFilterChainValidation, |
| mTLSSecretConfigName string, |
| ) simulation.Call { |
| return simulation.Call{ |
| Protocol: protocol, |
| Port: port, |
| CallMode: simulation.CallModeInbound, |
| TLS: tls, |
| CustomListenerValidations: validations, |
| MtlsSecretConfigName: mTLSSecretConfigName, |
| } |
| } |
| cases := []struct { |
| name string |
| config string |
| calls []simulation.Expect |
| }{ |
| { |
| name: "sidecar http over TLS simple mode with peer auth on port disabled", |
| config: peerAuthConfig("DISABLE") + sidecarSimple("HTTPS"), |
| calls: []simulation.Expect{ |
| { |
| Name: "http over tls", |
| Call: mkCall(9080, simulation.HTTP, simulation.TLS, []simulation.CustomFilterChainValidation{expectedTLSContext}, ""), |
| Result: simulation.Result{ |
| FilterChainMatched: "1.1.1.1_9080", |
| ClusterMatched: "inbound|9080||", |
| VirtualHostMatched: "inbound|http|9080", |
| RouteMatched: "default", |
| ListenerMatched: "virtualInbound", |
| }, |
| }, |
| { |
| Name: "plaintext", |
| Call: mkCall(9080, simulation.HTTP, simulation.Plaintext, nil, ""), |
| Result: simulation.Result{ |
| Error: simulation.ErrNoFilterChain, |
| }, |
| }, |
| { |
| Name: "http over mTLS", |
| Call: mkCall(9080, simulation.HTTP, simulation.MTLS, nil, "file-cert:httpbin.pem~httpbinkey.pem"), |
| Result: simulation.Result{ |
| Error: simulation.ErrMTLSError, |
| }, |
| }, |
| }, |
| }, |
| { |
| name: "sidecar TCP over TLS simple mode with peer auth on port disabled", |
| config: peerAuthConfig("DISABLE") + sidecarSimple("TLS"), |
| calls: []simulation.Expect{ |
| { |
| Name: "tcp over tls", |
| Call: mkCall(9080, simulation.TCP, simulation.TLS, []simulation.CustomFilterChainValidation{expectedTLSContext}, ""), |
| Result: simulation.Result{ |
| FilterChainMatched: "1.1.1.1_9080", |
| ClusterMatched: "inbound|9080||", |
| ListenerMatched: "virtualInbound", |
| }, |
| }, |
| { |
| Name: "plaintext", |
| Call: mkCall(9080, simulation.TCP, simulation.Plaintext, nil, ""), |
| Result: simulation.Result{ |
| Error: simulation.ErrNoFilterChain, |
| }, |
| }, |
| { |
| Name: "tcp over mTLS", |
| Call: mkCall(9080, simulation.TCP, simulation.MTLS, nil, "file-cert:httpbin.pem~httpbinkey.pem"), |
| Result: simulation.Result{ |
| Error: simulation.ErrMTLSError, |
| }, |
| }, |
| }, |
| }, |
| { |
| name: "sidecar http over mTLS mutual mode with peer auth on port disabled", |
| config: peerAuthConfig("DISABLE") + sidecarMutual("HTTPS"), |
| calls: []simulation.Expect{ |
| { |
| Name: "http over mtls", |
| Call: mkCall(9080, simulation.HTTP, simulation.MTLS, nil, "file-cert:httpbin.pem~httpbinkey.pem"), |
| Result: simulation.Result{ |
| FilterChainMatched: "1.1.1.1_9080", |
| ClusterMatched: "inbound|9080||", |
| ListenerMatched: "virtualInbound", |
| }, |
| }, |
| { |
| Name: "plaintext", |
| Call: mkCall(9080, simulation.HTTP, simulation.Plaintext, nil, ""), |
| Result: simulation.Result{ |
| Error: simulation.ErrNoFilterChain, |
| }, |
| }, |
| { |
| Name: "http over tls", |
| Call: mkCall(9080, simulation.HTTP, simulation.TLS, nil, "file-cert:httpbin.pem~httpbinkey.pem"), |
| Result: simulation.Result{ |
| Error: simulation.ErrMTLSError, |
| }, |
| }, |
| }, |
| }, |
| { |
| name: "sidecar tcp over mTLS mutual mode with peer auth on port disabled", |
| config: peerAuthConfig("DISABLE") + sidecarMutual("TLS"), |
| calls: []simulation.Expect{ |
| { |
| Name: "tcp over mtls", |
| Call: mkCall(9080, simulation.TCP, simulation.MTLS, nil, "file-cert:httpbin.pem~httpbinkey.pem"), |
| Result: simulation.Result{ |
| FilterChainMatched: "1.1.1.1_9080", |
| ClusterMatched: "inbound|9080||", |
| ListenerMatched: "virtualInbound", |
| }, |
| }, |
| { |
| Name: "plaintext", |
| Call: mkCall(9080, simulation.TCP, simulation.Plaintext, nil, ""), |
| Result: simulation.Result{ |
| Error: simulation.ErrNoFilterChain, |
| }, |
| }, |
| { |
| Name: "http over tls", |
| Call: mkCall(9080, simulation.TCP, simulation.TLS, nil, "file-cert:httpbin.pem~httpbinkey.pem"), |
| Result: simulation.Result{ |
| Error: simulation.ErrMTLSError, |
| }, |
| }, |
| }, |
| }, |
| { |
| name: "sidecar http over TLS SIMPLE mode with peer auth on port STRICT", |
| config: peerAuthConfig("STRICT") + sidecarMutual("TLS"), |
| calls: []simulation.Expect{ |
| { |
| Name: "http over tls", |
| Call: mkCall(9080, simulation.HTTP, simulation.TLS, nil, ""), |
| Result: simulation.Result{ |
| Error: simulation.ErrMTLSError, |
| }, |
| }, |
| { |
| Name: "plaintext", |
| Call: mkCall(9080, simulation.HTTP, simulation.Plaintext, nil, ""), |
| Result: simulation.Result{ |
| Error: simulation.ErrNoFilterChain, |
| }, |
| }, |
| { |
| Name: "http over mtls", |
| Call: mkCall(9080, simulation.HTTP, simulation.MTLS, nil, ""), |
| Result: simulation.Result{ |
| FilterChainMatched: "1.1.1.1_9080", |
| ClusterMatched: "inbound|9080||", |
| ListenerMatched: "virtualInbound", |
| }, |
| }, |
| }, |
| }, |
| } |
| proxy := &model.Proxy{Metadata: &model.NodeMetadata{Labels: map[string]string{"app": "foo"}}} |
| test.SetBoolForTest(t, &features.EnableTLSOnSidecarIngress, true) |
| for _, tt := range cases { |
| runSimulationTest(t, proxy, xds.FakeOptions{}, simulationTest{ |
| name: tt.name, |
| config: tt.config, |
| calls: tt.calls, |
| }) |
| } |
| } |
| |
| const ( |
| TimeOlder = "2019-01-01T00:00:00Z" |
| TimeBase = "2020-01-01T00:00:00Z" |
| TimeNewer = "2021-01-01T00:00:00Z" |
| ) |
| |
| type Configer interface { |
| Config(variant string) string |
| } |
| |
| type vsArgs struct { |
| Namespace string |
| Match string |
| Matches []string |
| Dest string |
| Port int |
| PortMatch int |
| Time string |
| } |
| |
| func (args vsArgs) Config(variant string) string { |
| if args.Time == "" { |
| args.Time = TimeBase |
| } |
| |
| if args.PortMatch != 0 { |
| // TODO(v0.4.2) test port match |
| variant = "virtualservice" |
| } |
| if args.Matches == nil { |
| args.Matches = []string{args.Match} |
| } |
| switch variant { |
| case "httproute": |
| return tmpl.MustEvaluate(`apiVersion: gateway.networking.k8s.io/v1alpha2 |
| kind: HTTPRoute |
| metadata: |
| name: "{{.Namespace}}{{.Match | replace "*" "wild"}}{{.Dest}}" |
| namespace: {{.Namespace}} |
| creationTimestamp: "{{.Time}}" |
| spec: |
| parentRefs: |
| - kind: Mesh |
| name: istio |
| {{ with .PortMatch }} |
| port: {{.}} |
| {{ end }} |
| hostnames: |
| {{- range $val := .Matches }} |
| - "{{$val}}" |
| {{ end }} |
| rules: |
| - backendRefs: |
| - kind: Hostname |
| group: networking.istio.io |
| name: {{.Dest}} |
| port: {{.Port | default 80}} |
| `, args) |
| case "virtualservice": |
| return tmpl.MustEvaluate(`apiVersion: networking.istio.io/v1alpha3 |
| kind: VirtualService |
| metadata: |
| name: "{{.Namespace}}{{.Match | replace "*" "wild"}}{{.Dest}}" |
| namespace: {{.Namespace}} |
| creationTimestamp: "{{.Time}}" |
| spec: |
| hosts: |
| {{- range $val := .Matches }} |
| - "{{$val}}" |
| {{ end }} |
| http: |
| - route: |
| - destination: |
| host: {{.Dest}} |
| {{ with .Port }} |
| port: |
| number: {{.}} |
| {{ end }} |
| {{ with .PortMatch }} |
| match: |
| - port: {{.}} |
| {{ end }} |
| `, args) |
| default: |
| panic(variant + " unknown") |
| } |
| } |
| |
| type scArgs struct { |
| Namespace string |
| Egress []string |
| } |
| |
| func (args scArgs) Config(variant string) string { |
| return tmpl.MustEvaluate(`apiVersion: networking.istio.io/v1alpha3 |
| kind: Sidecar |
| metadata: |
| name: "{{.Namespace}}" |
| namespace: "{{.Namespace}}" |
| spec: |
| egress: |
| - hosts: |
| {{- range $val := .Egress }} |
| - "{{$val}}" |
| {{- end }} |
| `, args) |
| } |
| |
| func TestSidecarRoutes(t *testing.T) { |
| knownServices := ` |
| apiVersion: networking.istio.io/v1alpha3 |
| kind: ServiceEntry |
| metadata: |
| name: known-default.example.com |
| namespace: default |
| spec: |
| hosts: |
| - known-default.example.com |
| addresses: |
| - 2.0.0.0 |
| endpoints: |
| - address: 1.0.0.0 |
| resolution: STATIC |
| ports: |
| - name: http |
| number: 80 |
| protocol: HTTP |
| - name: http-other |
| number: 8080 |
| protocol: HTTP |
| --- |
| apiVersion: networking.istio.io/v1alpha3 |
| kind: ServiceEntry |
| metadata: |
| name: alt-known-default.example.com |
| namespace: default |
| spec: |
| hosts: |
| - alt-known-default.example.com |
| addresses: |
| - 2.0.0.1 |
| endpoints: |
| - address: 1.0.0.1 |
| resolution: STATIC |
| ports: |
| - name: http |
| number: 80 |
| protocol: HTTP |
| - name: http-other |
| number: 8080 |
| protocol: HTTP |
| --- |
| apiVersion: networking.istio.io/v1alpha3 |
| kind: ServiceEntry |
| metadata: |
| name: not-default.example.org |
| namespace: not-default |
| spec: |
| hosts: |
| - not-default.example.org |
| addresses: |
| - 2.0.0.2 |
| endpoints: |
| - address: 1.0.0.2 |
| resolution: STATIC |
| ports: |
| - name: http |
| number: 80 |
| protocol: HTTP |
| - name: http-other |
| number: 8080 |
| protocol: HTTP |
| --- |
| ` |
| proxy := func(ns string) *model.Proxy { |
| return &model.Proxy{ConfigNamespace: ns} |
| } |
| cases := []struct { |
| name string |
| cfg []Configer |
| proxy *model.Proxy |
| routeName string |
| expected map[string][]string |
| expectedGateway map[string][]string |
| }{ |
| // Port 80 has special cases as there is defaulting logic around this port |
| { |
| name: "simple port 80", |
| cfg: []Configer{vsArgs{ |
| Namespace: "default", |
| Match: "known-default.example.com", |
| Dest: "alt-known-default.example.com", |
| }}, |
| proxy: proxy("default"), |
| routeName: "80", |
| expected: map[string][]string{ |
| "known-default.example.com": {"outbound|80||alt-known-default.example.com"}, |
| }, |
| }, |
| { |
| name: "simple port 8080", |
| cfg: []Configer{vsArgs{ |
| Namespace: "default", |
| Match: "known-default.example.com", |
| Dest: "alt-known-default.example.com", |
| }}, |
| proxy: proxy("default"), |
| routeName: "8080", |
| expected: map[string][]string{ |
| "known-default.example.com": {"outbound|8080||alt-known-default.example.com"}, |
| }, |
| expectedGateway: map[string][]string{ |
| "known-default.example.com": {"outbound|80||alt-known-default.example.com"}, |
| }, |
| }, |
| { |
| name: "unknown port 80", |
| cfg: []Configer{vsArgs{ |
| Namespace: "default", |
| Match: "foo.com", |
| Dest: "foo.com", |
| }}, |
| proxy: proxy("default"), |
| routeName: "80", |
| expected: map[string][]string{ |
| "foo.com": {"outbound|80||foo.com"}, |
| }, |
| }, |
| { |
| name: "unknown port 8080", |
| cfg: []Configer{vsArgs{ |
| Namespace: "default", |
| Match: "foo.com", |
| Dest: "foo.com", |
| }}, |
| proxy: proxy("default"), |
| routeName: "8080", |
| // For unknown services, we only will add a route to the port 80 |
| expected: map[string][]string{ |
| "default.com": nil, |
| }, |
| }, |
| { |
| name: "unknown port 8080 match 8080", |
| cfg: []Configer{vsArgs{ |
| Namespace: "default", |
| Match: "foo.com", |
| Dest: "foo.com", |
| PortMatch: 8080, |
| }}, |
| proxy: proxy("default"), |
| routeName: "8080", |
| // For unknown services, we only will add a route to the port 80 |
| expected: map[string][]string{ |
| "foo.com": nil, |
| }, |
| }, |
| { |
| name: "unknown port 8080 dest 8080 ", |
| cfg: []Configer{vsArgs{ |
| Namespace: "default", |
| Match: "foo.com", |
| Dest: "foo.com", |
| Port: 8080, |
| }}, |
| proxy: proxy("default"), |
| routeName: "8080", |
| // For unknown services, we only will add a route to the port 80 |
| expected: map[string][]string{ |
| "default.com": nil, |
| }, |
| }, |
| { |
| name: "producer rule port 80", |
| cfg: []Configer{vsArgs{ |
| Namespace: "default", |
| Match: "known-default.example.com", |
| Dest: "alt-known-default.example.com", |
| }}, |
| proxy: proxy("not-default"), |
| routeName: "80", |
| expected: map[string][]string{ |
| "known-default.example.com": {"outbound|80||alt-known-default.example.com"}, |
| }, |
| }, |
| { |
| name: "producer rule port 8080", |
| cfg: []Configer{vsArgs{ |
| Namespace: "default", |
| Match: "known-default.example.com", |
| Dest: "alt-known-default.example.com", |
| }}, |
| proxy: proxy("not-default"), |
| routeName: "8080", |
| expected: map[string][]string{ |
| "known-default.example.com": {"outbound|8080||alt-known-default.example.com"}, |
| }, |
| expectedGateway: map[string][]string{ // No implicit port matching for gateway |
| "known-default.example.com": {"outbound|80||alt-known-default.example.com"}, |
| }, |
| }, |
| { |
| name: "consumer rule port 80", |
| cfg: []Configer{vsArgs{ |
| Namespace: "not-default", |
| Match: "known-default.example.com", |
| Dest: "alt-known-default.example.com", |
| }}, |
| proxy: proxy("not-default"), |
| routeName: "80", |
| expected: map[string][]string{ |
| "known-default.example.com": {"outbound|80||alt-known-default.example.com"}, |
| }, |
| }, |
| { |
| name: "consumer rule port 8080", |
| cfg: []Configer{vsArgs{ |
| Namespace: "not-default", |
| Match: "known-default.example.com", |
| Dest: "alt-known-default.example.com", |
| }}, |
| proxy: proxy("not-default"), |
| routeName: "8080", |
| expected: map[string][]string{ |
| "known-default.example.com": {"outbound|8080||alt-known-default.example.com"}, |
| }, |
| expectedGateway: map[string][]string{ // No implicit port matching for gateway |
| "known-default.example.com": {"outbound|80||alt-known-default.example.com"}, |
| }, |
| }, |
| { |
| name: "arbitrary rule port 80", |
| cfg: []Configer{vsArgs{ |
| Namespace: "arbitrary", |
| Match: "known-default.example.com", |
| Dest: "alt-known-default.example.com", |
| }}, |
| proxy: proxy("not-default"), |
| routeName: "80", |
| expected: map[string][]string{ |
| "known-default.example.com": {"outbound|80||alt-known-default.example.com"}, |
| }, |
| }, |
| { |
| name: "arbitrary rule port 8080", |
| cfg: []Configer{vsArgs{ |
| Namespace: "arbitrary", |
| Match: "known-default.example.com", |
| Dest: "alt-known-default.example.com", |
| }}, |
| proxy: proxy("not-default"), |
| routeName: "8080", |
| expected: map[string][]string{ |
| "known-default.example.com": {"outbound|8080||alt-known-default.example.com"}, |
| }, |
| expectedGateway: map[string][]string{ // No implicit port matching for gateway |
| "known-default.example.com": {"outbound|80||alt-known-default.example.com"}, |
| }, |
| }, |
| { |
| name: "multiple rules 80", |
| cfg: []Configer{ |
| vsArgs{ |
| Namespace: "arbitrary", |
| Match: "known-default.example.com", |
| Dest: "arbitrary.example.com", |
| Time: TimeOlder, |
| }, |
| vsArgs{ |
| Namespace: "default", |
| Match: "known-default.example.com", |
| Dest: "default.example.com", |
| Time: TimeBase, |
| }, |
| vsArgs{ |
| Namespace: "not-default", |
| Match: "known-default.example.com", |
| Dest: "not-default.example.com", |
| Time: TimeNewer, |
| }, |
| }, |
| proxy: proxy("not-default"), |
| routeName: "80", |
| expected: map[string][]string{ |
| // Oldest wins |
| "known-default.example.com": {"outbound|80||arbitrary.example.com"}, |
| }, |
| expectedGateway: map[string][]string{ |
| // TODO: consumer namespace wins |
| "known-default.example.com": {"outbound|80||arbitrary.example.com"}, |
| }, |
| }, |
| { |
| name: "multiple rules 8080", |
| cfg: []Configer{ |
| vsArgs{ |
| Namespace: "arbitrary", |
| Match: "known-default.example.com", |
| Dest: "arbitrary.example.com", |
| Time: TimeOlder, |
| }, |
| vsArgs{ |
| Namespace: "default", |
| Match: "known-default.example.com", |
| Dest: "default.example.com", |
| Time: TimeBase, |
| }, |
| vsArgs{ |
| Namespace: "not-default", |
| Match: "known-default.example.com", |
| Dest: "not-default.example.com", |
| Time: TimeNewer, |
| }, |
| }, |
| proxy: proxy("not-default"), |
| routeName: "8080", |
| expected: map[string][]string{ |
| // Oldest wins |
| "known-default.example.com": {"outbound|8080||arbitrary.example.com"}, |
| }, |
| expectedGateway: map[string][]string{ |
| // TODO: Consumer gateway wins. No implicit destination port for Gateway |
| "known-default.example.com": {"outbound|80||arbitrary.example.com"}, |
| }, |
| }, |
| { |
| name: "wildcard random", |
| cfg: []Configer{vsArgs{ |
| Namespace: "default", |
| Match: "*.unknown.example.com", |
| Dest: "arbitrary.example.com", |
| }}, |
| proxy: proxy("default"), |
| routeName: "80", |
| expected: map[string][]string{ |
| // match no VS, get default config |
| "alt-known-default.example.com": {"outbound|80||alt-known-default.example.com"}, |
| "known-default.example.com": {"outbound|80||known-default.example.com"}, |
| // Wildcard doesn't match any known services, insert it as-is |
| "*.unknown.example.com": {"outbound|80||arbitrary.example.com"}, |
| }, |
| }, |
| { |
| name: "wildcard match with sidecar", |
| cfg: []Configer{ |
| vsArgs{ |
| Namespace: "default", |
| Match: "*.example.com", |
| Dest: "arbitrary.example.com", |
| }, |
| scArgs{ |
| Namespace: "default", |
| Egress: []string{"*/*.example.com"}, |
| }, |
| }, |
| proxy: proxy("default"), |
| routeName: "80", |
| expected: map[string][]string{ |
| "alt-known-default.example.com": {"outbound|80||arbitrary.example.com"}, |
| "known-default.example.com": {"outbound|80||arbitrary.example.com"}, |
| // Matched an exact service, so we have no route for the wildcard |
| "*.example.com": nil, |
| }, |
| expectedGateway: map[string][]string{ |
| // Exact service matches do not get the wildcard applied |
| "alt-known-default.example.com": {"outbound|80||alt-known-default.example.com"}, |
| "known-default.example.com": {"outbound|80||known-default.example.com"}, |
| // The wildcard |
| "*.example.com": {"outbound|80||arbitrary.example.com"}, |
| }, |
| }, |
| { |
| name: "wildcard first then explicit", |
| cfg: []Configer{ |
| vsArgs{ |
| Namespace: "default", |
| Match: "*.example.com", |
| Dest: "wild.example.com", |
| Time: TimeOlder, |
| }, |
| vsArgs{ |
| Namespace: "default", |
| Match: "known-default.example.com", |
| Dest: "explicit.example.com", |
| Time: TimeNewer, |
| }, |
| }, |
| proxy: proxy("default"), |
| routeName: "80", |
| expected: map[string][]string{ |
| "alt-known-default.example.com": {"outbound|80||wild.example.com"}, |
| "known-default.example.com": {"outbound|80||wild.example.com"}, // oldest wins |
| // Matched an exact service, so we have no route for the wildcard |
| "*.example.com": nil, |
| }, |
| expectedGateway: map[string][]string{ |
| // No overrides, use default |
| "alt-known-default.example.com": {"outbound|80||alt-known-default.example.com"}, |
| // Explicit has precedence |
| "known-default.example.com": {"outbound|80||explicit.example.com"}, |
| // Last is our wildcard |
| "*.example.com": {"outbound|80||wild.example.com"}, |
| }, |
| }, |
| { |
| name: "explicit first then wildcard", |
| cfg: []Configer{ |
| vsArgs{ |
| Namespace: "default", |
| Match: "*.example.com", |
| Dest: "wild.example.com", |
| Time: TimeNewer, |
| }, |
| vsArgs{ |
| Namespace: "default", |
| Match: "known-default.example.com", |
| Dest: "explicit.example.com", |
| Time: TimeOlder, |
| }, |
| }, |
| proxy: proxy("default"), |
| routeName: "80", |
| expected: map[string][]string{ |
| "alt-known-default.example.com": {"outbound|80||wild.example.com"}, |
| "known-default.example.com": {"outbound|80||explicit.example.com"}, // oldest wins |
| // Matched an exact service, so we have no route for the wildcard |
| "*.example.com": nil, |
| }, |
| expectedGateway: map[string][]string{ |
| // No overrides, use default |
| "alt-known-default.example.com": {"outbound|80||alt-known-default.example.com"}, |
| // Explicit has precedence |
| "known-default.example.com": {"outbound|80||explicit.example.com"}, |
| // Last is our wildcard |
| "*.example.com": {"outbound|80||wild.example.com"}, |
| }, |
| }, |
| { |
| name: "wildcard and explicit with sidecar", |
| cfg: []Configer{ |
| vsArgs{ |
| Namespace: "default", |
| Match: "*.example.com", |
| Dest: "wild.example.com", |
| Time: TimeOlder, |
| }, |
| vsArgs{ |
| Namespace: "default", |
| Match: "known-default.example.com", |
| Dest: "explicit.example.com", |
| Time: TimeNewer, |
| }, |
| scArgs{ |
| Namespace: "default", |
| Egress: []string{"default/known-default.example.com", "default/alt-known-default.example.com"}, |
| }, |
| }, |
| proxy: proxy("default"), |
| routeName: "80", |
| expected: map[string][]string{ |
| // Even though we did not import `*.example.com`, the VS attaches |
| "alt-known-default.example.com": {"outbound|80||wild.example.com"}, |
| "known-default.example.com": {"outbound|80||wild.example.com"}, |
| // Matched an exact service, so we have no route for the wildcard |
| "*.example.com": nil, |
| }, |
| expectedGateway: map[string][]string{ |
| // No rule imported |
| "alt-known-default.example.com": {"outbound|80||alt-known-default.example.com"}, |
| // Imported rule |
| "known-default.example.com": {"outbound|80||explicit.example.com"}, |
| // Not imported |
| "*.example.com": nil, |
| }, |
| }, |
| { |
| name: "explicit first then wildcard with sidecar cross namespace", |
| cfg: []Configer{ |
| vsArgs{ |
| Namespace: "not-default", |
| Match: "*.example.com", |
| Dest: "wild.example.com", |
| Time: TimeOlder, |
| }, |
| vsArgs{ |
| Namespace: "default", |
| Match: "known-default.example.com", |
| Dest: "explicit.example.com", |
| Time: TimeNewer, |
| }, |
| scArgs{ |
| Namespace: "default", |
| Egress: []string{"default/known-default.example.com", "default/alt-known-default.example.com"}, |
| }, |
| }, |
| proxy: proxy("default"), |
| routeName: "80", |
| expected: map[string][]string{ |
| // Similar to above, but now the older wildcard VS is in a complete different namespace which we don't import |
| "alt-known-default.example.com": {"outbound|80||alt-known-default.example.com"}, |
| "known-default.example.com": {"outbound|80||explicit.example.com"}, |
| // Matched an exact service, so we have no route for the wildcard |
| "*.example.com": nil, |
| }, |
| }, |
| { |
| name: "wildcard and explicit cross namespace", |
| cfg: []Configer{ |
| vsArgs{ |
| Namespace: "not-default", |
| Match: "*.com", |
| Dest: "wild.example.com", |
| Time: TimeOlder, |
| }, |
| vsArgs{ |
| Namespace: "default", |
| Match: "known-default.example.com", |
| Dest: "explicit.example.com", |
| Time: TimeNewer, |
| }, |
| }, |
| proxy: proxy("default"), |
| routeName: "80", |
| expected: map[string][]string{ |
| // Wildcard is older, so it wins, even though it is cross namespace |
| "alt-known-default.example.com": {"outbound|80||wild.example.com"}, |
| "known-default.example.com": {"outbound|80||wild.example.com"}, |
| // Matched an exact service, so we have no route for the wildcard |
| "*.com": nil, |
| }, |
| expectedGateway: map[string][]string{ |
| // Exact match wins |
| "alt-known-default.example.com": {"outbound|80||alt-known-default.example.com"}, |
| "known-default.example.com": {"outbound|80||explicit.example.com"}, |
| // Wildcard last |
| "*.com": {"outbound|80||wild.example.com"}, |
| }, |
| }, |
| { |
| name: "wildcard and explicit unknown", |
| cfg: []Configer{ |
| vsArgs{ |
| Namespace: "default", |
| Match: "*.tld", |
| Dest: "wild.example.com", |
| Time: TimeOlder, |
| }, |
| vsArgs{ |
| Namespace: "default", |
| Match: "example.tld", |
| Dest: "explicit.example.com", |
| Time: TimeNewer, |
| }, |
| }, |
| proxy: proxy("default"), |
| routeName: "80", |
| expected: map[string][]string{ |
| // wildcard does not match |
| "known-default.example.com": {"outbound|80||known-default.example.com"}, |
| // Even though its less exact, this wildcard wins |
| "*.tld": {"outbound|80||wild.example.com"}, |
| "*.example.tld": nil, |
| }, |
| }, |
| { |
| name: "explicit match with wildcard sidecar", |
| cfg: []Configer{ |
| vsArgs{ |
| Namespace: "default", |
| Match: "arbitrary.example.com", |
| Dest: "arbitrary.example.com", |
| }, |
| scArgs{ |
| Namespace: "default", |
| Egress: []string{"*/*.example.com"}, |
| }, |
| }, |
| proxy: proxy("default"), |
| routeName: "80", |
| expected: map[string][]string{ |
| "arbitrary.example.com": {"outbound|80||arbitrary.example.com"}, |
| }, |
| }, |
| { |
| name: "wildcard match with explicit sidecar", |
| cfg: []Configer{ |
| vsArgs{ |
| Namespace: "default", |
| Match: "*.example.com", |
| Dest: "arbitrary.example.com", |
| }, |
| scArgs{ |
| Namespace: "default", |
| Egress: []string{"*/known-default.example.com"}, |
| }, |
| }, |
| proxy: proxy("default"), |
| routeName: "80", |
| expected: map[string][]string{ |
| "known-default.example.com": {"outbound|80||arbitrary.example.com"}, |
| "*.example.com": nil, |
| }, |
| expectedGateway: map[string][]string{ |
| "known-default.example.com": {"outbound|80||known-default.example.com"}, |
| "*.example.com": nil, |
| }, |
| }, |
| { |
| name: "non-service wildcard match with explicit sidecar", |
| cfg: []Configer{ |
| vsArgs{ |
| Namespace: "default", |
| Match: "*.example.org", |
| Dest: "arbitrary.example.com", |
| }, |
| scArgs{ |
| Namespace: "default", |
| Egress: []string{"*/explicit.example.org", "*/alt-known-default.example.com"}, |
| }, |
| }, |
| proxy: proxy("default"), |
| routeName: "80", |
| expected: map[string][]string{ |
| "known-default.example.com": nil, // Not imported |
| "alt-known-default.example.com": {"outbound|80||alt-known-default.example.com"}, // No change |
| "*.example.org": {"outbound|80||arbitrary.example.com"}, |
| }, |
| expectedGateway: map[string][]string{ |
| "known-default.example.com": nil, // Not imported |
| "alt-known-default.example.com": {"outbound|80||alt-known-default.example.com"}, // No change |
| "*.example.org": nil, // Not imported |
| }, |
| }, |
| { |
| name: "sidecar filter", |
| cfg: []Configer{ |
| vsArgs{ |
| Namespace: "not-default", |
| Match: "*.example.com", |
| Dest: "arbitrary.example.com", |
| }, |
| vsArgs{ |
| Namespace: "default", |
| Match: "explicit.example.com", |
| Dest: "explicit.example.com", |
| }, |
| scArgs{ |
| Namespace: "not-default", |
| Egress: []string{"not-default/*.example.com", "not-default/not-default.example.org"}, |
| }, |
| }, |
| proxy: proxy("not-default"), |
| routeName: "80", |
| expected: map[string][]string{ |
| // even though there is an *.example.com, since we do not import it we should create a wildcard matcher |
| "*.example.com": {"outbound|80||arbitrary.example.com"}, |
| // We did not import this, shouldn't show up |
| "explicit.example.com": nil, |
| }, |
| }, |
| { |
| name: "same namespace conflict", |
| cfg: []Configer{ |
| vsArgs{ |
| Namespace: "default", |
| Match: "known-default.example.com", |
| Dest: "old.example.com", |
| Time: TimeOlder, |
| }, |
| vsArgs{ |
| Namespace: "default", |
| Match: "known-default.example.com", |
| Dest: "new.example.com", |
| Time: TimeNewer, |
| }, |
| }, |
| proxy: proxy("default"), |
| routeName: "80", |
| expected: map[string][]string{ |
| "known-default.example.com": {"outbound|80||old.example.com"}, // oldest wins |
| }, |
| }, |
| { |
| name: "cross namespace conflict", |
| cfg: []Configer{ |
| vsArgs{ |
| Namespace: "not-default", |
| Match: "known-default.example.com", |
| Dest: "producer.example.com", |
| Time: TimeOlder, |
| }, |
| vsArgs{ |
| Namespace: "default", |
| Match: "known-default.example.com", |
| Dest: "consumer.example.com", |
| Time: TimeNewer, |
| }, |
| }, |
| proxy: proxy("default"), |
| routeName: "80", |
| expected: map[string][]string{ |
| // oldest wins |
| "known-default.example.com": {"outbound|80||producer.example.com"}, |
| }, |
| expectedGateway: map[string][]string{ |
| // TODO: consumer namespace wins |
| "known-default.example.com": {"outbound|80||producer.example.com"}, |
| }, |
| }, |
| { |
| name: "import only a unknown service route", |
| cfg: []Configer{ |
| vsArgs{ |
| Namespace: "default", |
| Match: "a.example.org", |
| Dest: "example.com", |
| }, |
| scArgs{ |
| Namespace: "default", |
| Egress: []string{"*/a.example.com"}, |
| }, |
| }, |
| proxy: proxy("default"), |
| routeName: "80", |
| expected: nil, // We do not even get a route as there is no service on the port |
| }, |
| { |
| // https://github.com/istio/istio/issues/37087 |
| name: "multi-host import single", |
| cfg: []Configer{ |
| vsArgs{ |
| Namespace: "default", |
| Matches: []string{"a.example.org", "b.example.org"}, |
| Dest: "example.com", |
| }, |
| scArgs{ |
| Namespace: "default", |
| Egress: []string{"*/known-default.example.com", "*/a.example.org"}, |
| }, |
| }, |
| proxy: proxy("default"), |
| routeName: "80", |
| expected: map[string][]string{ |
| // imported |
| "a.example.org": {"outbound|80||example.com"}, |
| // Not imported but we include it anyway |
| "b.example.org": {"outbound|80||example.com"}, |
| }, |
| expectedGateway: map[string][]string{ |
| // imported |
| "a.example.org": {"outbound|80||example.com"}, |
| // Not imported but we include it anyway |
| "b.example.org": nil, |
| }, |
| }, |
| } |
| for _, variant := range []string{"httproute", "virtualservice"} { |
| t.Run(variant, func(t *testing.T) { |
| for _, tt := range cases { |
| t.Run(tt.name, func(t *testing.T) { |
| cfg := knownServices |
| for _, tc := range tt.cfg { |
| cfg = cfg + "\n---\n" + tc.Config(variant) |
| } |
| s := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{ConfigString: cfg}) |
| sim := simulation.NewSimulation(t, s, s.SetupProxy(tt.proxy)) |
| xdstest.ValidateListeners(t, sim.Listeners) |
| xdstest.ValidateRouteConfigurations(t, sim.Routes) |
| r := xdstest.ExtractRouteConfigurations(sim.Routes) |
| vh := r[tt.routeName] |
| exp := tt.expected |
| if variant == "httproute" && tt.expectedGateway != nil { |
| exp = tt.expectedGateway |
| } |
| if vh == nil && exp != nil { |
| t.Fatalf("route %q not found, have %v", tt.routeName, xdstest.MapKeys(r)) |
| } |
| gotHosts := xdstest.ExtractVirtualHosts(vh) |
| |
| for wk, wv := range exp { |
| got := gotHosts[wk] |
| if !reflect.DeepEqual(wv, got) { |
| t.Errorf("%v: wanted %v, got %v (had %v)", wk, wv, got, xdstest.MapKeys(gotHosts)) |
| } |
| } |
| }) |
| } |
| }) |
| } |
| } |