//go:build integ
// +build integ

// 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 common

import (
	"fmt"
	"net/http"
	"net/url"
	"reflect"
	"sort"
	"strings"
	"time"

	"google.golang.org/grpc/codes"
	wrappers "google.golang.org/protobuf/types/known/wrapperspb"

	"istio.io/istio/pilot/pkg/model"
	"istio.io/istio/pkg/config/host"
	"istio.io/istio/pkg/config/protocol"
	"istio.io/istio/pkg/config/security"
	"istio.io/istio/pkg/http/headers"
	echoClient "istio.io/istio/pkg/test/echo"
	"istio.io/istio/pkg/test/echo/common/scheme"
	"istio.io/istio/pkg/test/framework/components/echo"
	"istio.io/istio/pkg/test/framework/components/echo/check"
	"istio.io/istio/pkg/test/framework/components/echo/common/deployment"
	"istio.io/istio/pkg/test/framework/components/echo/common/ports"
	"istio.io/istio/pkg/test/framework/components/echo/echotest"
	"istio.io/istio/pkg/test/framework/components/echo/match"
	"istio.io/istio/pkg/test/framework/components/istio"
	"istio.io/istio/pkg/test/framework/components/istio/ingress"
	"istio.io/istio/pkg/test/framework/label"
	"istio.io/istio/pkg/test/scopes"
	"istio.io/istio/pkg/test/util/tmpl"
	"istio.io/istio/pkg/util/sets"
	"istio.io/istio/tests/common/jwt"
	ingressutil "istio.io/istio/tests/integration/security/sds_ingress/util"
)

const httpVirtualServiceTmpl = `
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: "{{.VirtualServiceHost|replace "*" "wild"}}"
spec:
  gateways:
  - {{.Gateway}}
  hosts:
  - "{{.VirtualServiceHost}}"
  http:
  - route:
    - destination:
        host: "{{.DestinationHost | default .VirtualServiceHost}}"
        port:
          number: {{.Port}}
{{- if .MatchScheme }}
    match:
    - scheme:
        exact: {{.MatchScheme}}
    headers:
      request:
        add:
          istio-custom-header: user-defined-value
{{- end }}
---
`

func httpVirtualService(gateway, host string, port int) string {
	return tmpl.MustEvaluate(httpVirtualServiceTmpl, struct {
		Gateway            string
		VirtualServiceHost string
		DestinationHost    string
		Port               int
		MatchScheme        string
	}{gateway, host, "", port, ""})
}

const gatewayTmpl = `
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: gateway
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: {{.GatewayPort}}
      name: {{.GatewayPortName}}
      protocol: {{.GatewayProtocol}}
{{- if .Credential }}
    tls:
      mode: SIMPLE
      credentialName: {{.Credential}}
{{- if .Ciphers }}
      cipherSuites:
{{- range $cipher := .Ciphers }}
      - "{{$cipher}}"
{{- end }}
{{- end }}
{{- end }}
    hosts:
    - "{{.GatewayHost}}"
---
`

func httpGateway(host string) string {
	return tmpl.MustEvaluate(gatewayTmpl, struct {
		GatewayHost     string
		GatewayPort     int
		GatewayPortName string
		GatewayProtocol string
		Credential      string
	}{
		host, 80, "http", "HTTP", "",
	})
}

func virtualServiceCases(t TrafficContext) {
	// reduce the total # of subtests that don't give valuable coverage or just don't work
	// TODO include proxyless as different features become supported
	t.SetDefaultSourceMatchers(match.NotNaked, match.NotHeadless, match.NotProxylessGRPC)
	t.SetDefaultTargetMatchers(match.NotNaked, match.NotHeadless, match.NotProxylessGRPC)
	includeProxyless := []match.Matcher{match.NotNaked, match.NotHeadless}

	skipVM := t.Settings().Skip(echo.VM)
	t.RunTraffic(TrafficTestCase{
		name: "added header",
		config: `
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: default
spec:
  hosts:
  - {{ .dstSvc }}
  http:
  - route:
    - destination:
        host: {{ .dstSvc }}
    headers:
      request:
        add:
          istio-custom-header: user-defined-value`,
		opts: echo.CallOptions{
			Port: echo.Port{
				Name: "http",
			},
			Count: 1,
			Check: check.And(
				check.OK(),
				check.RequestHeader("Istio-Custom-Header", "user-defined-value")),
		},
		workloadAgnostic: true,
	})
	t.RunTraffic(TrafficTestCase{
		name: "set header",
		config: `
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: default
spec:
  hosts:
  - {{ (index .dst 0).Config.Service }}
  http:
  - route:
    - destination:
        host: {{ (index .dst 0).Config.Service }}
    headers:
      request:
        set:
          x-custom: some-value`,
		opts: echo.CallOptions{
			Port: echo.Port{
				Name: "http",
			},
			Count: 1,
			Check: check.And(
				check.OK(),
				check.RequestHeader("X-Custom", "some-value")),
		},
		workloadAgnostic: true,
	})
	t.RunTraffic(TrafficTestCase{
		name: "set authority header",
		config: `
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: default
spec:
  hosts:
  - {{ (index .dst 0).Config.Service }}
  http:
  - route:
    - destination:
        host: {{ (index .dst 0).Config.Service }}
    headers:
      request:
        set:
          :authority: my-custom-authority`,
		opts: echo.CallOptions{
			Port: echo.Port{
				Name: "http",
			},
			Count: 1,
			Check: check.And(
				check.OK(),
				check.Host("my-custom-authority")),
		},
		workloadAgnostic: true,
		minIstioVersion:  "1.10.0",
	})
	t.RunTraffic(TrafficTestCase{
		name: "set host header in destination",
		config: `
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: default
spec:
  hosts:
  - {{ (index .dst 0).Config.Service }}
  http:
  - route:
    - destination:
        host: {{ (index .dst 0).Config.Service }}
      headers:
        request:
          set:
            Host: my-custom-authority`,
		opts: echo.CallOptions{
			Port: echo.Port{
				Name: "http",
			},
			Count: 1,
			Check: check.And(
				check.OK(),
				check.Host("my-custom-authority")),
		},
		workloadAgnostic: true,
		minIstioVersion:  "1.10.0",
	})
	t.RunTraffic(TrafficTestCase{
		name: "set host header in route and destination",
		config: `
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: default
spec:
  hosts:
  - {{ (index .dst 0).Config.Service }}
  http:
  - route:
    - destination:
        host: {{ (index .dst 0).Config.Service }}
      headers:
        request:
          set:
            Host: dest-authority
    headers:
      request:
        set:
          :authority: route-authority`,
		opts: echo.CallOptions{
			Port: echo.Port{
				Name: "http",
			},
			Count: 1,
			Check: check.And(
				check.OK(),
				check.Host("route-authority")),
		},
		workloadAgnostic: true,
		minIstioVersion:  "1.12.0",
	})
	t.RunTraffic(TrafficTestCase{
		name: "set host header in route and multi destination",
		config: `
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: default
spec:
  hosts:
  - {{ (index .dst 0).Config.Service }}
  http:
  - route:
    - destination:
        host: {{ (index .dst 0).Config.Service }}
      headers:
        request:
          set:
            Host: dest-authority
      weight: 50
    - destination:
        host: {{ (index .dst 0).Config.Service }}
      weight: 50
    headers:
      request:
        set:
          :authority: route-authority`,
		opts: echo.CallOptions{
			Port: echo.Port{
				Name: "http",
			},
			Count: 1,
			Check: check.And(
				check.OK(),
				check.Host("route-authority")),
		},
		workloadAgnostic: true,
		minIstioVersion:  "1.12.0",
	})
	t.RunTraffic(TrafficTestCase{
		name: "set host header multi destination",
		config: `
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: default
spec:
  hosts:
  - {{ (index .dst 0).Config.Service }}
  http:
  - route:
    - destination:
        host: {{ (index .dst 0).Config.Service }}
      headers:
        request:
          set:
            Host: dest-authority
      weight: 50
    - destination:
        host: {{ (index .dst 0).Config.Service }}
      headers:
        request:
          set:
            Host: dest-authority
      weight: 50`,
		opts: echo.CallOptions{
			Port: echo.Port{
				Name: "http",
			},
			Count: 1,
			Check: check.And(
				check.OK(),
				check.Host("dest-authority")),
		},
		workloadAgnostic: true,
		minIstioVersion:  "1.12.0",
	})
	t.RunTraffic(TrafficTestCase{
		name: "redirect",
		config: `
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: default
spec:
  hosts:
    - {{ .dstSvc }}
  http:
  - match:
    - uri:
        exact: /foo
    redirect:
      uri: /new/path
  - match:
    - uri:
        exact: /new/path
    route:
    - destination:
        host: {{ .dstSvc }}`,
		opts: echo.CallOptions{
			Port: echo.Port{
				Name: "http",
			},
			HTTP: echo.HTTP{
				Path:            "/foo?key=value",
				FollowRedirects: true,
			},
			Count: 1,
			Check: check.And(
				check.OK(),
				check.URL("/new/path?key=value")),
		},
		workloadAgnostic: true,
	})
	t.RunTraffic(TrafficTestCase{
		name: "redirect port and scheme",
		config: `
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: default
spec:
  hosts:
    - {{ .dstSvc }}
  http:
  - match:
    - uri:
        exact: /foo
    redirect:
      derivePort: FROM_REQUEST_PORT
      scheme: https
`,
		opts: echo.CallOptions{
			Port: echo.Port{
				Name: "http",
			},
			HTTP: echo.HTTP{
				Path:            "/foo",
				FollowRedirects: false,
			},
			Count: 1,
			Check: check.And(
				check.Status(http.StatusMovedPermanently),
				check.Each(
					func(r echoClient.Response) error {
						originalHostname, err := url.Parse(r.RequestURL)
						if err != nil {
							return err
						}
						return ExpectString(r.ResponseHeaders.Get("Location"),
							fmt.Sprintf("https://%s:%d/foo", originalHostname.Hostname(), ports.All().MustForName("http").ServicePort),
							"Location")
					})),
		},
		workloadAgnostic: true,
	})
	t.RunTraffic(TrafficTestCase{
		name: "rewrite uri",
		config: `
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: default
spec:
  hosts:
    - {{ .dstSvc }}
  http:
  - match:
    - uri:
        exact: /foo
    rewrite:
      uri: /new/path
    route:
    - destination:
        host: {{ .dstSvc }}`,
		opts: echo.CallOptions{
			Port: echo.Port{
				Name: "http",
			},
			HTTP: echo.HTTP{
				Path: "/foo?key=value#hash",
			},
			Count: 1,
			Check: check.And(
				check.OK(),
				check.URL("/new/path?key=value")),
		},
		workloadAgnostic: true,
	})
	t.RunTraffic(TrafficTestCase{
		name: "rewrite authority",
		config: `
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: default
spec:
  hosts:
    - {{ .dstSvc }}
  http:
  - match:
    - uri:
        exact: /foo
    rewrite:
      authority: new-authority
    route:
    - destination:
        host: {{ .dstSvc }}`,
		opts: echo.CallOptions{
			Port: echo.Port{
				Name: "http",
			},
			HTTP: echo.HTTP{
				Path: "/foo",
			},
			Count: 1,
			Check: check.And(
				check.OK(),
				check.Host("new-authority")),
		},
		workloadAgnostic: true,
	})
	t.RunTraffic(TrafficTestCase{
		name: "cors",
		// TODO https://github.com/istio/istio/issues/31532
		targetMatchers: []match.Matcher{match.NotTProxy, match.NotVM, match.NotNaked, match.NotHeadless, match.NotProxylessGRPC},

		config: `
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: default
spec:
  hosts:
    - {{ .dstSvc }}
  http:
  - corsPolicy:
      allowOrigins:
      - exact: cors.com
      allowMethods:
      - POST
      - GET
      allowCredentials: false
      allowHeaders:
      - X-Foo-Bar
      - X-Foo-Baz
      maxAge: "24h"
    route:
    - destination:
        host: {{ .dstSvc }}
`,
		children: []TrafficCall{
			{
				name: "preflight",
				opts: func() echo.CallOptions {
					return echo.CallOptions{
						Port: echo.Port{
							Name: "http",
						},
						HTTP: echo.HTTP{
							Method: "OPTIONS",
							Headers: headers.New().
								With(headers.Origin, "cors.com").
								With(headers.AccessControlRequestMethod, "DELETE").
								Build(),
						},
						Count: 1,
						Check: check.And(
							check.OK(),
							check.ResponseHeaders(map[string]string{
								"Access-Control-Allow-Origin":  "cors.com",
								"Access-Control-Allow-Methods": "POST,GET",
								"Access-Control-Allow-Headers": "X-Foo-Bar,X-Foo-Baz",
								"Access-Control-Max-Age":       "86400",
							})),
					}
				}(),
			},
			{
				name: "get",
				opts: func() echo.CallOptions {
					return echo.CallOptions{
						Port: echo.Port{
							Name: "http",
						},
						HTTP: echo.HTTP{
							Headers: headers.New().With(headers.Origin, "cors.com").Build(),
						},
						Count: 1,
						Check: check.And(
							check.OK(),
							check.ResponseHeader("Access-Control-Allow-Origin", "cors.com")),
					}
				}(),
			},
			{
				// GET without matching origin
				name: "get no origin match",
				opts: echo.CallOptions{
					Port: echo.Port{
						Name: "http",
					},
					Count: 1,
					Check: check.And(
						check.OK(),
						check.ResponseHeader("Access-Control-Allow-Origin", "")),
				},
			},
		},
		workloadAgnostic: true,
	})
	// Retry conditions have been added to just check that config is correct.
	// Retries are not specifically tested. TODO if we actually test retries, include proxyless
	t.RunTraffic(TrafficTestCase{
		name: "retry conditions",
		config: `
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: default
spec:
  hosts:
  - {{ .dstSvc }}
  http:
  - route:
    - destination:
        host: {{ .dstSvc }}
    retries:
      attempts: 3
      perTryTimeout: 2s
      retryOn: gateway-error,connect-failure,refused-stream
      retryRemoteLocalities: true`,
		opts: echo.CallOptions{
			Port: echo.Port{
				Name: "http",
			},
			Count: 1,
			Check: check.OK(),
		},
		workloadAgnostic: true,
	})
	t.RunTraffic(TrafficTestCase{
		name: "fault abort",
		config: `
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: default
spec:
  hosts:
  - {{ (index .dst 0).Config.Service }}
  http:
  - route:
    - destination:
        host: {{ (index .dst 0).Config.Service }}
    fault:
      abort:
        percentage:
          value: 100
        httpStatus: 418`,
		opts: echo.CallOptions{
			Port: echo.Port{
				Name: "http",
			},
			Count: 1,
			Check: check.Status(http.StatusTeapot),
		},
		workloadAgnostic: true,
	})

	t.RunTraffic(TrafficTestCase{
		name:            "fault abort gRPC",
		minIstioVersion: "1.15.0",
		config: `
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: default
spec:
  hosts:
  - {{ (index .dst 0).Config.Service }}
  http:
  - route:
    - destination:
        host: {{ (index .dst 0).Config.Service }}
    fault:
      abort:
        percentage:
          value: 100
        grpcStatus: "UNAVAILABLE"`,
		opts: echo.CallOptions{
			Port: echo.Port{
				Name: "grpc",
			},
			Scheme: scheme.GRPC,
			Count:  1,
			Check:  check.GRPCStatus(codes.Unavailable),
		},
		workloadAgnostic: true,
		sourceMatchers:   includeProxyless,
	})

	splits := [][]int{
		{50, 25, 25},
		{80, 10, 10},
	}
	if skipVM {
		splits = [][]int{
			{50, 50},
			{80, 20},
		}
	}
	for _, split := range splits {
		split := split
		t.RunTraffic(TrafficTestCase{
			name:           fmt.Sprintf("shifting-%d", split[0]),
			toN:            len(split),
			sourceMatchers: []match.Matcher{match.NotHeadless, match.NotNaked},
			targetMatchers: []match.Matcher{match.NotHeadless, match.NotNaked, match.NotExternal, match.NotProxylessGRPC},
			templateVars: func(_ echo.Callers, _ echo.Instances) map[string]any {
				return map[string]any{
					"split": split,
				}
			},
			config: `
{{ $split := .split }} 
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: default
spec:
  hosts:
    - {{ ( index .dstSvcs 0) }}
  http:
  - route:
{{- range $idx, $svc := .dstSvcs }}
    - destination:
        host: {{ $svc }}
      weight: {{ ( index $split $idx ) }}
{{- end }}
`,
			checkForN: func(src echo.Caller, dests echo.Services, opts *echo.CallOptions) echo.Checker {
				return check.And(
					check.OK(),
					func(result echo.CallResult, err error) error {
						errorThreshold := 10
						if len(split) != len(dests) {
							// shouldn't happen
							return fmt.Errorf("split configured for %d destinations, but framework gives %d", len(split), len(dests))
						}
						splitPerHost := map[echo.NamespacedName]int{}
						destNames := dests.NamespacedNames()
						for i, pct := range split {
							splitPerHost[destNames[i]] = pct
						}
						for serviceName, exp := range splitPerHost {
							hostResponses := result.Responses.Match(func(r echoClient.Response) bool {
								return strings.HasPrefix(r.Hostname, serviceName.Name)
							})
							if !AlmostEquals(len(hostResponses), exp, errorThreshold) {
								return fmt.Errorf("expected %v calls to %s, got %v", exp, serviceName, len(hostResponses))
							}
							// echotest should have filtered the deployment to only contain reachable clusters
							to := match.ServiceName(serviceName).GetMatches(dests.Instances())
							fromCluster := src.(echo.Instance).Config().Cluster
							toClusters := to.Clusters()
							// don't check headless since lb is unpredictable
							headlessTarget := match.Headless.Any(to)
							if !headlessTarget && len(toClusters.ByNetwork()[fromCluster.NetworkName()]) > 1 {
								// Conditionally check reached clusters to work around connection load balancing issues
								// See https://github.com/istio/istio/issues/32208 for details
								// We want to skip this for requests from the cross-network pod
								if err := check.ReachedClusters(t.AllClusters(), toClusters).Check(echo.CallResult{
									From:      result.From,
									Opts:      result.Opts,
									Responses: hostResponses,
								}, nil); err != nil {
									return fmt.Errorf("did not reach all clusters for %s: %v", serviceName, err)
								}
							}
						}
						return nil
					})
			},
			setupOpts: func(src echo.Caller, opts *echo.CallOptions) {
				// TODO force this globally in echotest?
				if src, ok := src.(echo.Instance); ok && src.Config().IsProxylessGRPC() {
					opts.Port.Name = "grpc"
				}
			},
			opts: echo.CallOptions{
				Port: echo.Port{
					Name: "http",
				},
				Count: 100,
			},
			workloadAgnostic: true,
		})
	}
}

func HostHeader(header string) http.Header {
	return headers.New().WithHost(header).Build()
}

// tlsOriginationCases contains tests TLS origination from DestinationRule
func tlsOriginationCases(t TrafficContext) {
	tc := TrafficTestCase{
		name: "",
		config: fmt.Sprintf(`
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: external
spec:
  host: %s
  trafficPolicy:
    tls:
      mode: SIMPLE
`, t.Apps.External.All.Config().DefaultHostHeader),
		children: []TrafficCall{},
	}
	expects := []struct {
		port int
		alpn string
	}{
		{8888, "http/1.1"},
		{8882, "h2"},
	}
	for _, c := range t.Apps.A {
		for _, e := range expects {
			c := c
			e := e

			tc.children = append(tc.children, TrafficCall{
				name: fmt.Sprintf("%s: %s", c.Config().Cluster.StableName(), e.alpn),
				opts: echo.CallOptions{
					Port:    echo.Port{ServicePort: e.port, Protocol: protocol.HTTP},
					Count:   1,
					Address: t.Apps.External.All[0].Address(),
					HTTP: echo.HTTP{
						Headers: HostHeader(t.Apps.External.All[0].Config().DefaultHostHeader),
					},
					Scheme: scheme.HTTP,
					Check: check.And(
						check.OK(),
						check.Alpn(e.alpn)),
				},
				call: c.CallOrFail,
			})
		}
	}
	t.RunTraffic(tc)
}

// useClientProtocolCases contains tests use_client_protocol from DestinationRule
func useClientProtocolCases(t TrafficContext) {
	client := t.Apps.A
	to := t.Apps.C
	t.RunTraffic(TrafficTestCase{
		name:   "use client protocol with h2",
		config: useClientProtocolDestinationRule(to.Config().Service),
		call:   client[0].CallOrFail,
		opts: echo.CallOptions{
			To: to,
			Port: echo.Port{
				Name: "http",
			},
			Count: 1,
			HTTP: echo.HTTP{
				HTTP2: true,
			},
			Check: check.And(
				check.OK(),
				check.Protocol("HTTP/2.0"),
			),
		},
		minIstioVersion: "1.10.0",
	})
	t.RunTraffic(TrafficTestCase{
		name:   "use client protocol with h1",
		config: useClientProtocolDestinationRule(to.Config().Service),
		call:   client[0].CallOrFail,
		opts: echo.CallOptions{
			Port: echo.Port{
				Name: "http",
			},
			Count: 1,
			To:    to,
			HTTP: echo.HTTP{
				HTTP2: false,
			},
			Check: check.And(
				check.OK(),
				check.Protocol("HTTP/1.1"),
			),
		},
	})
}

// destinationRuleCases contains tests some specific DestinationRule tests.
func destinationRuleCases(t TrafficContext) {
	from := t.Apps.A
	to := t.Apps.C
	// Validates the config is generated correctly when only idletimeout is specified in DR.
	t.RunTraffic(TrafficTestCase{
		name:   "only idletimeout specified in DR",
		config: idletimeoutDestinationRule("idletimeout-dr", to.Config().Service),
		call:   from[0].CallOrFail,
		opts: echo.CallOptions{
			To: to,
			Port: echo.Port{
				Name: "http",
			},
			Count: 1,
			HTTP: echo.HTTP{
				HTTP2: true,
			},
			Check: check.OK(),
		},
		minIstioVersion: "1.10.0",
	})
}

// trafficLoopCases contains tests to ensure traffic does not loop through the sidecar
func trafficLoopCases(t TrafficContext) {
	for _, c := range t.Apps.A {
		for _, d := range t.Apps.B {
			for _, port := range []int{15001, 15006} {
				c, d, port := c, d, port
				t.RunTraffic(TrafficTestCase{
					name: fmt.Sprint(port),
					call: c.CallOrFail,
					opts: echo.CallOptions{
						ToWorkload: d,
						Port:       echo.Port{ServicePort: port, Protocol: protocol.HTTP},
						// Ideally we would actually check to make sure we do not blow up the pod,
						// but I couldn't find a way to reliably detect this.
						Check: check.Error(),
					},
				})
			}
		}
	}
}

// autoPassthroughCases tests that we cannot hit unexpected destinations when using AUTO_PASSTHROUGH
func autoPassthroughCases(t TrafficContext) {
	// We test the cross product of all Istio ALPNs (or no ALPN), all mTLS modes, and various backends
	alpns := []string{"istio", "istio-peer-exchange", "istio-http/1.0", "istio-http/1.1", "istio-h2", ""}
	modes := []string{"STRICT", "PERMISSIVE", "DISABLE"}

	mtlsHost := host.Name(t.Apps.A.Config().ClusterLocalFQDN())
	nakedHost := host.Name(t.Apps.Naked.Config().ClusterLocalFQDN())
	httpsPort := ports.All().MustForName("https").ServicePort
	httpsAutoPort := ports.All().MustForName("auto-https").ServicePort
	snis := []string{
		model.BuildSubsetKey(model.TrafficDirectionOutbound, "", mtlsHost, httpsPort),
		model.BuildDNSSrvSubsetKey(model.TrafficDirectionOutbound, "", mtlsHost, httpsPort),
		model.BuildSubsetKey(model.TrafficDirectionOutbound, "", nakedHost, httpsPort),
		model.BuildDNSSrvSubsetKey(model.TrafficDirectionOutbound, "", nakedHost, httpsPort),
		model.BuildSubsetKey(model.TrafficDirectionOutbound, "", mtlsHost, httpsAutoPort),
		model.BuildDNSSrvSubsetKey(model.TrafficDirectionOutbound, "", mtlsHost, httpsAutoPort),
		model.BuildSubsetKey(model.TrafficDirectionOutbound, "", nakedHost, httpsAutoPort),
		model.BuildDNSSrvSubsetKey(model.TrafficDirectionOutbound, "", nakedHost, httpsAutoPort),
	}
	defaultIngress := istio.DefaultIngressOrFail(t, t)
	for _, mode := range modes {
		var childs []TrafficCall
		for _, sni := range snis {
			for _, alpn := range alpns {
				alpn, sni, mode := alpn, sni, mode
				al := []string{alpn}
				if alpn == "" {
					al = nil
				}
				childs = append(childs, TrafficCall{
					name: fmt.Sprintf("mode:%v,sni:%v,alpn:%v", mode, sni, alpn),
					call: defaultIngress.CallOrFail,
					opts: echo.CallOptions{
						Port: echo.Port{
							ServicePort: 443,
							Protocol:    protocol.HTTPS,
						},
						TLS: echo.TLS{
							ServerName: sni,
							Alpn:       al,
						},
						Check:   check.Error(),
						Timeout: 5 * time.Second,
					},
				},
				)
			}
		}
		t.RunTraffic(TrafficTestCase{
			config: globalPeerAuthentication(mode) + `
---
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: cross-network-gateway-test
  namespace: istio-system
spec:
  selector:
    istio: ingressgateway
  servers:
    - port:
        number: 443
        name: tls
        protocol: TLS
      tls:
        mode: AUTO_PASSTHROUGH
      hosts:
        - "*.local"
`,
			children: childs,
		})
	}
}

func gatewayCases(t TrafficContext) {
	templateParams := func(protocol protocol.Instance, src echo.Callers, dests echo.Instances, ciphers []string) map[string]any {
		hostName, dest, portN, cred := "*", dests[0], 80, ""
		if protocol.IsTLS() {
			hostName, portN, cred = dest.Config().ClusterLocalFQDN(), 443, "cred"
		}
		return map[string]any{
			"IngressNamespace":   src[0].(ingress.Instance).Namespace(),
			"GatewayHost":        hostName,
			"GatewayPort":        portN,
			"GatewayPortName":    strings.ToLower(string(protocol)),
			"GatewayProtocol":    string(protocol),
			"Gateway":            "gateway",
			"VirtualServiceHost": dest.Config().ClusterLocalFQDN(),
			"Port":               dest.PortForName("http").ServicePort,
			"Credential":         cred,
			"Ciphers":            ciphers,
		}
	}

	// clears the To to avoid echo internals trying to match the protocol with the port on echo.Config
	noTarget := func(_ echo.Caller, opts *echo.CallOptions) {
		opts.To = nil
	}
	// allows setting the target indirectly via the host header
	fqdnHostHeader := func(src echo.Caller, opts *echo.CallOptions) {
		if opts.HTTP.Headers == nil {
			opts.HTTP.Headers = make(http.Header)
		}
		opts.HTTP.Headers.Set(headers.Host, opts.To.Config().ClusterLocalFQDN())
		noTarget(src, opts)
	}

	// SingleRegualrPod is already applied leaving one regular pod, to only regular pods should leave a single workload.
	singleTarget := []match.Matcher{match.RegularPod}
	// the following cases don't actually target workloads, we use the singleTarget filter to avoid duplicate cases
	t.RunTraffic(TrafficTestCase{
		name:             "404",
		targetMatchers:   singleTarget,
		workloadAgnostic: true,
		viaIngress:       true,
		config:           httpGateway("*"),
		opts: echo.CallOptions{
			Count: 1,
			Port: echo.Port{
				Protocol: protocol.HTTP,
			},
			HTTP: echo.HTTP{
				Headers: headers.New().WithHost("foo.bar").Build(),
			},
			Check: check.Status(http.StatusNotFound),
		},
		setupOpts: noTarget,
	})
	t.RunTraffic(TrafficTestCase{
		name:             "https redirect",
		targetMatchers:   singleTarget,
		workloadAgnostic: true,
		viaIngress:       true,
		config: `apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: gateway
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "*"
    tls:
      httpsRedirect: true
---
`,
		opts: echo.CallOptions{
			Count: 1,
			Port: echo.Port{
				Protocol: protocol.HTTP,
			},
			Check: check.Status(http.StatusMovedPermanently),
		},
		setupOpts: fqdnHostHeader,
	})
	t.RunTraffic(TrafficTestCase{
		// See https://github.com/istio/istio/issues/27315
		name:             "https with x-forwarded-proto",
		targetMatchers:   singleTarget,
		workloadAgnostic: true,
		viaIngress:       true,
		config: `apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: gateway
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "*"
    tls:
      httpsRedirect: true
---
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: ingressgateway-redirect-config
  namespace: istio-system
spec:
  configPatches:
  - applyTo: NETWORK_FILTER
    match:
      context: GATEWAY
      listener:
        filterChain:
          filter:
            name: envoy.filters.network.http_connection_manager
    patch:
      operation: MERGE
      value:
        typed_config:
          '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
          xff_num_trusted_hops: 1
          normalize_path: true
  workloadSelector:
    labels:
      istio: ingressgateway
---
` + httpVirtualServiceTmpl,
		opts: echo.CallOptions{
			Count: 1,
			Port: echo.Port{
				Protocol: protocol.HTTP,
			},
			HTTP: echo.HTTP{
				// In real world, this may be set by a downstream LB that terminates the TLS
				Headers: headers.New().With(headers.XForwardedProto, "https").Build(),
			},
			Check: check.OK(),
		},
		setupOpts: fqdnHostHeader,
		templateVars: func(_ echo.Callers, dests echo.Instances) map[string]any {
			dest := dests[0]
			return map[string]any{
				"Gateway":            "gateway",
				"VirtualServiceHost": dest.Config().ClusterLocalFQDN(),
				"Port":               dest.PortForName("http").ServicePort,
			}
		},
	})
	t.RunTraffic(TrafficTestCase{
		name: "cipher suite",
		config: gatewayTmpl + httpVirtualServiceTmpl +
			ingressutil.IngressKubeSecretYAML("cred", "{{.IngressNamespace}}", ingressutil.TLS, ingressutil.IngressCredentialA),
		templateVars: func(src echo.Callers, dests echo.Instances) map[string]any {
			// Test all cipher suites, including a fake one. Envoy should accept all of the ones on the "valid" list,
			// and control plane should filter our invalid one.
			return templateParams(protocol.HTTPS, src, dests, append(sets.SortedList(security.ValidCipherSuites), "fake"))
		},
		setupOpts: fqdnHostHeader,
		opts: echo.CallOptions{
			Count: 1,
			Port: echo.Port{
				Protocol: protocol.HTTPS,
			},
		},
		viaIngress:       true,
		workloadAgnostic: true,
	})
	t.RunTraffic(TrafficTestCase{
		// See https://github.com/istio/istio/issues/34609
		name:             "http redirect when vs port specify https",
		targetMatchers:   singleTarget,
		workloadAgnostic: true,
		viaIngress:       true,
		config: `apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: gateway
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "*"
    tls:
      httpsRedirect: true
---
` + httpVirtualServiceTmpl,
		opts: echo.CallOptions{
			Count: 1,
			Port: echo.Port{
				Protocol: protocol.HTTP,
			},
			Check: check.Status(http.StatusMovedPermanently),
		},
		setupOpts: fqdnHostHeader,
		templateVars: func(_ echo.Callers, dests echo.Instances) map[string]any {
			dest := dests[0]
			return map[string]any{
				"Gateway":            "gateway",
				"VirtualServiceHost": dest.Config().ClusterLocalFQDN(),
				"Port":               443,
			}
		},
	})
	t.RunTraffic(TrafficTestCase{
		// See https://github.com/istio/istio/issues/27315
		// See https://github.com/istio/istio/issues/34609
		name:             "http return 400 with with x-forwarded-proto https when vs port specify https",
		targetMatchers:   singleTarget,
		workloadAgnostic: true,
		viaIngress:       true,
		config: `apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: gateway
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "*"
    tls:
      httpsRedirect: true
---
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: ingressgateway-redirect-config
  namespace: istio-system
spec:
  configPatches:
  - applyTo: NETWORK_FILTER
    match:
      context: GATEWAY
      listener:
        filterChain:
          filter:
            name: envoy.filters.network.http_connection_manager
    patch:
      operation: MERGE
      value:
        typed_config:
          '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
          xff_num_trusted_hops: 1
          normalize_path: true
  workloadSelector:
    labels:
      istio: ingressgateway
---
` + httpVirtualServiceTmpl,
		opts: echo.CallOptions{
			Count: 1,
			Port: echo.Port{
				Protocol: protocol.HTTP,
			},
			HTTP: echo.HTTP{
				// In real world, this may be set by a downstream LB that terminates the TLS
				Headers: headers.New().With(headers.XForwardedProto, "https").Build(),
			},
			Check: check.Status(http.StatusBadRequest),
		},
		setupOpts: fqdnHostHeader,
		templateVars: func(_ echo.Callers, dests echo.Instances) map[string]any {
			dest := dests[0]
			return map[string]any{
				"Gateway":            "gateway",
				"VirtualServiceHost": dest.Config().ClusterLocalFQDN(),
				"Port":               443,
			}
		},
	})
	t.RunTraffic(TrafficTestCase{
		// https://github.com/istio/istio/issues/37196
		name:             "client protocol - http1",
		targetMatchers:   singleTarget,
		workloadAgnostic: true,
		viaIngress:       true,
		config: `apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: gateway
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "*"
---
` + httpVirtualServiceTmpl,
		opts: echo.CallOptions{
			Count: 1,
			Port: echo.Port{
				Protocol: protocol.HTTP,
			},
			Check: check.And(
				check.OK(),
				check.Protocol("HTTP/1.1")),
		},
		setupOpts: fqdnHostHeader,
		templateVars: func(_ echo.Callers, dests echo.Instances) map[string]any {
			dest := dests[0]
			return map[string]any{
				"Gateway":            "gateway",
				"VirtualServiceHost": dest.Config().ClusterLocalFQDN(),
				"Port":               ports.All().MustForName("auto-http").ServicePort,
			}
		},
	})
	t.RunTraffic(TrafficTestCase{
		// https://github.com/istio/istio/issues/37196
		name:             "client protocol - http2",
		targetMatchers:   singleTarget,
		workloadAgnostic: true,
		viaIngress:       true,
		config: `apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: gateway
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "*"
---
` + httpVirtualServiceTmpl,
		opts: echo.CallOptions{
			HTTP: echo.HTTP{
				HTTP2: true,
			},
			Count: 1,
			Port: echo.Port{
				Protocol: protocol.HTTP,
			},
			Check: check.And(
				check.OK(),
				// Gateway doesn't implicitly use downstream
				check.Protocol("HTTP/1.1"),
				// Regression test; if this is set it means the inbound sidecar is treating it as TCP
				check.RequestHeader("X-Envoy-Peer-Metadata", "")),
		},
		setupOpts: fqdnHostHeader,
		templateVars: func(_ echo.Callers, dests echo.Instances) map[string]any {
			dest := dests[0]
			return map[string]any{
				"Gateway":            "gateway",
				"VirtualServiceHost": dest.Config().ClusterLocalFQDN(),
				"Port":               ports.All().MustForName("auto-http").ServicePort,
			}
		},
	})
	t.RunTraffic(TrafficTestCase{
		name:             "wildcard hostname",
		targetMatchers:   singleTarget,
		workloadAgnostic: true,
		viaIngress:       true,
		config: `apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: gateway
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "*.example.com"
---
` + httpVirtualServiceTmpl,
		children: []TrafficCall{
			{
				name: "no port",
				call: nil,
				opts: echo.CallOptions{
					HTTP: echo.HTTP{
						HTTP2:   true,
						Headers: headers.New().WithHost("foo.example.com").Build(),
					},
					Port: echo.Port{
						Protocol: protocol.HTTP,
					},
					Check: check.OK(),
				},
			},
			{
				name: "correct port",
				call: nil,
				opts: echo.CallOptions{
					HTTP: echo.HTTP{
						HTTP2:   true,
						Headers: headers.New().WithHost("foo.example.com:80").Build(),
					},
					Port: echo.Port{
						Protocol: protocol.HTTP,
					},
					Check: check.OK(),
				},
			},
			{
				name: "random port",
				call: nil,
				opts: echo.CallOptions{
					HTTP: echo.HTTP{
						HTTP2:   true,
						Headers: headers.New().WithHost("foo.example.com:12345").Build(),
					},
					Port: echo.Port{
						Protocol: protocol.HTTP,
					},
					Check: check.OK(),
				},
			},
		},
		minIstioVersion: "1.15.0",
		setupOpts:       noTarget,
		templateVars: func(_ echo.Callers, dests echo.Instances) map[string]any {
			return map[string]any{
				"Gateway":            "gateway",
				"VirtualServiceHost": "*.example.com",
				"DestinationHost":    dests[0].Config().ClusterLocalFQDN(),
				"Port":               ports.All().MustForName(ports.HTTP).ServicePort,
			}
		},
	})

	for _, port := range []string{"auto-http", "http", "http2"} {
		for _, h2 := range []bool{true, false} {
			port, h2 := port, h2
			protoName := "http1"
			expectedProto := "HTTP/1.1"
			if h2 {
				protoName = "http2"
				expectedProto = "HTTP/2.0"
			}

			t.RunTraffic(TrafficTestCase{
				// https://github.com/istio/istio/issues/37196
				name:             fmt.Sprintf("client protocol - %v use client with %v", protoName, port),
				targetMatchers:   singleTarget,
				workloadAgnostic: true,
				viaIngress:       true,
				config: `apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: gateway
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "*"
---
` + httpVirtualServiceTmpl + useClientProtocolDestinationRuleTmpl,
				opts: echo.CallOptions{
					HTTP: echo.HTTP{
						HTTP2: h2,
					},
					Count: 1,
					Port: echo.Port{
						Protocol: protocol.HTTP,
					},
					Check: check.And(
						check.OK(),
						// We did configure to use client protocol
						check.Protocol(expectedProto),
						// Regression test; if this is set it means the inbound sidecar is treating it as TCP
						check.RequestHeader("X-Envoy-Peer-Metadata", "")),
				},
				setupOpts: fqdnHostHeader,
				templateVars: func(_ echo.Callers, dests echo.Instances) map[string]any {
					dest := dests[0]
					return map[string]any{
						"Gateway":            "gateway",
						"VirtualServiceHost": dest.Config().ClusterLocalFQDN(),
						"Port":               ports.All().MustForName(port).ServicePort,
					}
				},
			})
		}
	}

	for _, proto := range []protocol.Instance{protocol.HTTP, protocol.HTTPS} {
		proto, secret := proto, ""
		if proto.IsTLS() {
			secret = ingressutil.IngressKubeSecretYAML("cred", "{{.IngressNamespace}}", ingressutil.TLS, ingressutil.IngressCredentialA)
		}
		t.RunTraffic(TrafficTestCase{
			name:   string(proto),
			config: gatewayTmpl + httpVirtualServiceTmpl + secret,
			templateVars: func(src echo.Callers, dests echo.Instances) map[string]any {
				return templateParams(proto, src, dests, nil)
			},
			setupOpts: fqdnHostHeader,
			opts: echo.CallOptions{
				Count: 1,
				Port: echo.Port{
					Protocol: proto,
				},
			},
			viaIngress:       true,
			workloadAgnostic: true,
		})
		t.RunTraffic(TrafficTestCase{
			name:   fmt.Sprintf("%s scheme match", proto),
			config: gatewayTmpl + httpVirtualServiceTmpl + secret,
			templateVars: func(src echo.Callers, dests echo.Instances) map[string]any {
				params := templateParams(proto, src, dests, nil)
				params["MatchScheme"] = strings.ToLower(string(proto))
				return params
			},
			setupOpts: fqdnHostHeader,
			opts: echo.CallOptions{
				Count: 1,
				Port: echo.Port{
					Protocol: proto,
				},
				Check: check.And(
					check.OK(),
					check.RequestHeader("Istio-Custom-Header", "user-defined-value")),
			},
			// to keep tests fast, we only run the basic protocol test per-workload and scheme match once (per cluster)
			targetMatchers:   singleTarget,
			viaIngress:       true,
			workloadAgnostic: true,
		})
	}
}

func XFFGatewayCase(apps *deployment.SingleNamespaceView, gateway string) []TrafficTestCase {
	var cases []TrafficTestCase

	destinationSets := []echo.Instances{
		apps.A,
	}

	for _, d := range destinationSets {
		d := d
		if len(d) == 0 {
			continue
		}
		fqdn := d[0].Config().ClusterLocalFQDN()
		cases = append(cases, TrafficTestCase{
			name:   d[0].Config().Service,
			config: httpGateway("*") + httpVirtualService("gateway", fqdn, d[0].PortForName("http").ServicePort),
			call:   apps.Naked[0].CallOrFail,
			opts: echo.CallOptions{
				Count:   1,
				Port:    echo.Port{ServicePort: 80},
				Scheme:  scheme.HTTP,
				Address: gateway,
				HTTP: echo.HTTP{
					Headers: headers.New().
						WithHost(fqdn).
						With(headers.XForwardedFor, "56.5.6.7, 72.9.5.6, 98.1.2.3").
						Build(),
				},
				Check: check.Each(
					func(r echoClient.Response) error {
						externalAddress, ok := r.RequestHeaders["X-Envoy-External-Address"]
						if !ok {
							return fmt.Errorf("missing X-Envoy-External-Address Header")
						}
						if err := ExpectString(externalAddress[0], "72.9.5.6", "envoy-external-address header"); err != nil {
							return err
						}
						xffHeader, ok := r.RequestHeaders["X-Forwarded-For"]
						if !ok {
							return fmt.Errorf("missing X-Forwarded-For Header")
						}

						xffIPs := strings.Split(xffHeader[0], ",")
						if len(xffIPs) != 4 {
							return fmt.Errorf("did not receive expected 4 hosts in X-Forwarded-For header")
						}

						return ExpectString(strings.TrimSpace(xffIPs[1]), "72.9.5.6", "ip in xff header")
					}),
			},
		})
	}
	return cases
}

func envoyFilterCases(t TrafficContext) {
	// Test adding envoyfilter to inbound and outbound route/cluster/listeners
	cfg := `
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: outbound
spec:
  workloadSelector:
    labels:
      app: a
  configPatches:
  - applyTo: HTTP_FILTER
    match:
      context: SIDECAR_OUTBOUND
      listener:
        filterChain:
          filter:
            name: "envoy.filters.network.http_connection_manager"
            subFilter:
              name: "envoy.filters.http.router"
    patch:
      operation: INSERT_BEFORE
      value:
       name: envoy.lua
       typed_config:
          "@type": "type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua"
          inlineCode: |
            function envoy_on_request(request_handle)
              request_handle:headers():add("x-lua-outbound", "hello world")
            end
  - applyTo: VIRTUAL_HOST
    match:
      context: SIDECAR_OUTBOUND
    patch:
      operation: MERGE
      value:
        request_headers_to_add:
        - header:
            key: x-vhost-outbound
            value: "hello world"
  - applyTo: CLUSTER
    match:
      context: SIDECAR_OUTBOUND
      cluster: {}
    patch:
      operation: MERGE
      value:
        http2_protocol_options: {}
---
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: inbound
spec:
  workloadSelector:
    labels:
      app: b
  configPatches:
  - applyTo: HTTP_FILTER
    match:
      context: SIDECAR_INBOUND
      listener:
        filterChain:
          filter:
            name: "envoy.filters.network.http_connection_manager"
            subFilter:
              name: "envoy.filters.http.router"
    patch:
      operation: INSERT_BEFORE
      value:
       name: envoy.lua
       typed_config:
          "@type": "type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua"
          inlineCode: |
            function envoy_on_request(request_handle)
              request_handle:headers():add("x-lua-inbound", "hello world")
            end
  - applyTo: VIRTUAL_HOST
    match:
      context: SIDECAR_INBOUND
    patch:
      operation: MERGE
      value:
        request_headers_to_add:
        - header:
            key: x-vhost-inbound
            value: "hello world"
  - applyTo: CLUSTER
    match:
      context: SIDECAR_INBOUND
      cluster: {}
    patch:
      operation: MERGE
      value:
        http2_protocol_options: {}
`
	for _, c := range t.Apps.A {
		t.RunTraffic(TrafficTestCase{
			config: cfg,
			call:   c.CallOrFail,
			opts: echo.CallOptions{
				To:    t.Apps.B,
				Count: 1,
				Port: echo.Port{
					Name: "http",
				},
				Check: check.And(
					check.OK(),
					check.Protocol("HTTP/2.0"),
					check.RequestHeaders(map[string]string{
						"X-Vhost-Inbound":  "hello world",
						"X-Vhost-Outbound": "hello world",
						"X-Lua-Inbound":    "hello world",
						"X-Lua-Outbound":   "hello world",
					}),
				),
			},
		})
	}
}

// hostCases tests different forms of host header to use
func hostCases(t TrafficContext) {
	for _, c := range t.Apps.A {
		cfg := t.Apps.Headless.Config()
		port := ports.All().MustForName("auto-http").WorkloadPort
		wl := t.Apps.Headless[0].WorkloadsOrFail(t)
		if len(wl) == 0 {
			t.Fatalf("no workloads found")
		}
		address := wl[0].Address()
		// We test all variants with no port, the expected port, and a random port.
		hosts := []string{
			cfg.ClusterLocalFQDN(),
			fmt.Sprintf("%s:%d", cfg.ClusterLocalFQDN(), port),
			fmt.Sprintf("%s:12345", cfg.ClusterLocalFQDN()),
			fmt.Sprintf("%s.%s.svc", cfg.Service, cfg.Namespace.Name()),
			fmt.Sprintf("%s.%s.svc:%d", cfg.Service, cfg.Namespace.Name(), port),
			fmt.Sprintf("%s.%s.svc:12345", cfg.Service, cfg.Namespace.Name()),
			cfg.Service,
			fmt.Sprintf("%s:%d", cfg.Service, port),
			fmt.Sprintf("%s:12345", cfg.Service),
			fmt.Sprintf("some-instances.%s:%d", cfg.ClusterLocalFQDN(), port),
			fmt.Sprintf("some-instances.%s:12345", cfg.ClusterLocalFQDN()),
			fmt.Sprintf("some-instances.%s.%s.svc", cfg.Service, cfg.Namespace.Name()),
			fmt.Sprintf("some-instances.%s.%s.svc:12345", cfg.Service, cfg.Namespace.Name()),
			fmt.Sprintf("some-instances.%s", cfg.Service),
			fmt.Sprintf("some-instances.%s:%d", cfg.Service, port),
			fmt.Sprintf("some-instances.%s:12345", cfg.Service),
			address,
			fmt.Sprintf("%s:%d", address, port),
		}
		for _, h := range hosts {
			name := strings.Replace(h, address, "ip", -1) + "/auto-http"
			t.RunTraffic(TrafficTestCase{
				name:            name,
				minIstioVersion: "1.15.0",
				call:            c.CallOrFail,
				opts: echo.CallOptions{
					To:    t.Apps.Headless,
					Count: 1,
					Port: echo.Port{
						Name: "auto-http",
					},
					HTTP: echo.HTTP{
						Headers: HostHeader(h),
					},
					// check mTLS to ensure we are not hitting pass-through cluster
					Check: check.And(check.OK(), check.MTLSForHTTP()),
				},
			})
		}
		port = ports.All().MustForName("http").WorkloadPort
		hosts = []string{
			cfg.ClusterLocalFQDN(),
			fmt.Sprintf("%s:%d", cfg.ClusterLocalFQDN(), port),
			fmt.Sprintf("%s:12345", cfg.ClusterLocalFQDN()),
			fmt.Sprintf("%s.%s.svc", cfg.Service, cfg.Namespace.Name()),
			fmt.Sprintf("%s.%s.svc:%d", cfg.Service, cfg.Namespace.Name(), port),
			fmt.Sprintf("%s.%s.svc:12345", cfg.Service, cfg.Namespace.Name()),
			cfg.Service,
			fmt.Sprintf("%s:%d", cfg.Service, port),
			fmt.Sprintf("%s:12345", cfg.Service),
			fmt.Sprintf("some-instances.%s:%d", cfg.ClusterLocalFQDN(), port),
			fmt.Sprintf("some-instances.%s:12345", cfg.ClusterLocalFQDN()),
			fmt.Sprintf("some-instances.%s.%s.svc", cfg.Service, cfg.Namespace.Name()),
			fmt.Sprintf("some-instances.%s.%s.svc:%d", cfg.Service, cfg.Namespace.Name(), port),
			fmt.Sprintf("some-instances.%s.%s.svc:12345", cfg.Service, cfg.Namespace.Name()),
			fmt.Sprintf("some-instances.%s", cfg.Service),
			fmt.Sprintf("some-instances.%s:%d", cfg.Service, port),
			fmt.Sprintf("some-instances.%s:12345", cfg.Service),
			address,
			fmt.Sprintf("%s:%d", address, port),
		}
		for _, h := range hosts {
			name := strings.Replace(h, address, "ip", -1) + "/http"
			assertion := check.And(check.OK(), check.MTLSForHTTP())
			if strings.Contains(name, "ip") {
				// we expect to actually do passthrough for the IP case
				assertion = check.OK()
			}
			t.RunTraffic(TrafficTestCase{
				name:            name,
				minIstioVersion: "1.15.0",
				call:            c.CallOrFail,
				opts: echo.CallOptions{
					To: t.Apps.Headless,
					Port: echo.Port{
						Name: "http",
					},
					HTTP: echo.HTTP{
						Headers: HostHeader(h),
					},
					// check mTLS to ensure we are not hitting pass-through cluster
					Check: assertion,
				},
			})
		}
	}
}

// serviceCases tests overlapping Services. There are a few cases.
// Consider we have our base service B, with service port P and target port T
//  1. Another service, B', with P -> T. In this case, both the listener and the cluster will conflict.
//     Because everything is workload oriented, this is not a problem unless they try to make them different
//     protocols (this is explicitly called out as "not supported") or control inbound connectionPool settings
//     (which is moving to Sidecar soon)
//  2. Another service, B', with P -> T'. In this case, the listener will be distinct, since its based on the target.
//     The cluster, however, will be shared, which is broken, because we should be forwarding to T when we call B, and T' when we call B'.
//  3. Another service, B', with P' -> T. In this case, the listener is shared. This is fine, with the exception of different protocols
//     The cluster is distinct.
//  4. Another service, B', with P' -> T'. There is no conflicts here at all.
func serviceCases(t TrafficContext) {
	for _, c := range t.Apps.A {
		c := c

		// Case 1
		// Identical to port "http" or service B, just behind another service name
		svc := fmt.Sprintf(`apiVersion: v1
kind: Service
metadata:
  name: b-alt-1
  labels:
    app: b
spec:
  ports:
  - name: http
    port: %d
    targetPort: %d
  selector:
    app: b`, ports.All().MustForName("http").ServicePort, ports.All().MustForName("http").WorkloadPort)
		t.RunTraffic(TrafficTestCase{
			name:   fmt.Sprintf("case 1 both match in cluster %v", c.Config().Cluster.StableName()),
			config: svc,
			call:   c.CallOrFail,
			opts: echo.CallOptions{
				Count:   1,
				Address: "b-alt-1",
				Port:    echo.Port{ServicePort: ports.All().MustForName("http").ServicePort, Protocol: protocol.HTTP},
				Timeout: time.Millisecond * 100,
				Check:   check.OK(),
			},
		})

		// Case 2
		// We match the service port, but forward to a different port
		// Here we make the new target tcp so the test would fail if it went to the http port
		svc = fmt.Sprintf(`apiVersion: v1
kind: Service
metadata:
  name: b-alt-2
  labels:
    app: b
spec:
  ports:
  - name: tcp
    port: %d
    targetPort: %d
  selector:
    app: b`, ports.All().MustForName("http").ServicePort, ports.All().GetWorkloadOnlyPorts()[0].WorkloadPort)
		t.RunTraffic(TrafficTestCase{
			name:   fmt.Sprintf("case 2 service port match in cluster %v", c.Config().Cluster.StableName()),
			config: svc,
			call:   c.CallOrFail,
			opts: echo.CallOptions{
				Count:   1,
				Address: "b-alt-2",
				Port:    echo.Port{ServicePort: ports.All().MustForName("http").ServicePort, Protocol: protocol.TCP},
				Scheme:  scheme.TCP,
				Timeout: time.Millisecond * 100,
				Check:   check.OK(),
			},
		})

		// Case 3
		// We match the target port, but front with a different service port
		svc = fmt.Sprintf(`apiVersion: v1
kind: Service
metadata:
  name: b-alt-3
  labels:
    app: b
spec:
  ports:
  - name: http
    port: 12345
    targetPort: %d
  selector:
    app: b`, ports.All().MustForName("http").WorkloadPort)
		t.RunTraffic(TrafficTestCase{
			name:   fmt.Sprintf("case 3 target port match in cluster %v", c.Config().Cluster.StableName()),
			config: svc,
			call:   c.CallOrFail,
			opts: echo.CallOptions{
				Count:   1,
				Address: "b-alt-3",
				Port:    echo.Port{ServicePort: 12345, Protocol: protocol.HTTP},
				Timeout: time.Millisecond * 100,
				Check:   check.OK(),
			},
		})

		// Case 4
		// Completely new set of ports
		svc = fmt.Sprintf(`apiVersion: v1
kind: Service
metadata:
  name: b-alt-4
  labels:
    app: b
spec:
  ports:
  - name: http
    port: 12346
    targetPort: %d
  selector:
    app: b`, ports.All().GetWorkloadOnlyPorts()[1].WorkloadPort)
		t.RunTraffic(TrafficTestCase{
			name:   fmt.Sprintf("case 4 no match in cluster %v", c.Config().Cluster.StableName()),
			config: svc,
			call:   c.CallOrFail,
			opts: echo.CallOptions{
				Count:   1,
				Address: "b-alt-4",
				Port:    echo.Port{ServicePort: 12346, Protocol: protocol.HTTP},
				Timeout: time.Millisecond * 100,
				Check:   check.OK(),
			},
		})
	}
}

// consistentHashCases tests destination rule's consistent hashing mechanism
func consistentHashCases(t TrafficContext) {
	if len(t.Clusters().ByNetwork()) != 1 {
		// Consistent hashing does not work for multinetwork. The first request will consistently go to a
		// gateway, but that gateway will tcp_proxy it to a random pod.
		t.Skipf("multi-network is not supported")
	}
	for _, app := range []echo.Instances{t.Apps.A, t.Apps.B} {
		app := app
		for _, c := range app {
			c := c

			// First setup a service selecting a few services. This is needed to ensure we can load balance across many pods.
			svcName := "consistent-hash"
			if nw := c.Config().Cluster.NetworkName(); nw != "" {
				svcName += "-" + nw
			}
			svc := tmpl.MustEvaluate(`apiVersion: v1
kind: Service
metadata:
  name: {{.Service}}
spec:
  ports:
  - name: http
    port: {{.Port}}
    targetPort: {{.TargetPort}}
  - name: tcp
    port: {{.TcpPort}}
    targetPort: {{.TcpTargetPort}}
  selector:
    test.istio.io/class: standard
    {{- if .Network }}
    topology.istio.io/network: {{.Network}}
	{{- end }}
`, map[string]any{
				"Service":        svcName,
				"Network":        c.Config().Cluster.NetworkName(),
				"Port":           ports.All().MustForName("http").ServicePort,
				"TargetPort":     ports.All().MustForName("http").WorkloadPort,
				"TcpPort":        ports.All().MustForName("tcp").ServicePort,
				"TcpTargetPort":  ports.All().MustForName("tcp").WorkloadPort,
				"GrpcPort":       ports.All().MustForName("grpc").ServicePort,
				"GrpcTargetPort": ports.All().MustForName("grpc").WorkloadPort,
			})

			destRule := fmt.Sprintf(`
---
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: %s
spec:
  host: %s
  trafficPolicy:
    loadBalancer:
      consistentHash:
        {{. | indent 8}}
`, svcName, svcName)
			// Add a negative test case. This ensures that the test is actually valid; its not a super trivial check
			// and could be broken by having only 1 pod so its good to have this check in place
			t.RunTraffic(TrafficTestCase{
				name:   "no consistent",
				config: svc,
				call:   c.CallOrFail,
				opts: echo.CallOptions{
					Count:   10,
					Address: svcName,
					Port:    echo.Port{ServicePort: ports.All().MustForName("http").ServicePort, Protocol: protocol.HTTP},
					Check: check.And(
						check.OK(),
						func(result echo.CallResult, rerr error) error {
							err := ConsistentHostChecker.Check(result, rerr)
							if err == nil {
								return fmt.Errorf("expected inconsistent hash, but it was consistent")
							}
							return nil
						},
					),
				},
			})
			callOpts := echo.CallOptions{
				Count:   10,
				Address: svcName,
				HTTP: echo.HTTP{
					Path:    "/?some-query-param=bar",
					Headers: headers.New().With("x-some-header", "baz").Build(),
				},
				Port: echo.Port{ServicePort: ports.All().MustForName("http").ServicePort, Protocol: protocol.HTTP},
				Check: check.And(
					check.OK(),
					ConsistentHostChecker,
				),
			}
			tcpCallopts := echo.CallOptions{
				Count:   10,
				Address: svcName,
				Port:    echo.Port{ServicePort: ports.All().MustForName("tcp").ServicePort, Protocol: protocol.TCP},
				Check: check.And(
					check.OK(),
					ConsistentHostChecker,
				),
			}
			if c.Config().WorkloadClass() == echo.Proxyless {
				callOpts.Port = echo.Port{ServicePort: ports.All().MustForName("grpc").ServicePort, Protocol: protocol.GRPC}
			}
			// Setup tests for various forms of the API
			// TODO: it may be necessary to vary the inputs of the hash and ensure we get a different backend
			// But its pretty hard to test that, so for now just ensure we hit the same one.
			t.RunTraffic(TrafficTestCase{
				name:   "source ip " + c.Config().Service,
				config: svc + tmpl.MustEvaluate(destRule, "useSourceIp: true"),
				call:   c.CallOrFail,
				opts:   callOpts,
			})
			t.RunTraffic(TrafficTestCase{
				name:   "query param" + c.Config().Service,
				config: svc + tmpl.MustEvaluate(destRule, "httpQueryParameterName: some-query-param"),
				call:   c.CallOrFail,
				opts:   callOpts,
			})
			t.RunTraffic(TrafficTestCase{
				name:   "http header" + c.Config().Service,
				config: svc + tmpl.MustEvaluate(destRule, "httpHeaderName: x-some-header"),
				call:   c.CallOrFail,
				opts:   callOpts,
			})
			t.RunTraffic(TrafficTestCase{
				name:            "tcp source ip " + c.Config().Service,
				minIstioVersion: "1.14.0",
				config:          svc + tmpl.MustEvaluate(destRule, "useSourceIp: true"),
				call:            c.CallOrFail,
				opts:            tcpCallopts,
				skip: skip{
					skip:   c.Config().WorkloadClass() == echo.Proxyless,
					reason: "", // TODO: is this a bug or WAI?
				},
			})
		}
	}
}

var ConsistentHostChecker echo.Checker = func(result echo.CallResult, _ error) error {
	hostnames := make([]string, len(result.Responses))
	for i, r := range result.Responses {
		hostnames[i] = r.Hostname
	}
	scopes.Framework.Infof("requests landed on hostnames: %v", hostnames)
	unique := sets.SortedList(sets.New(hostnames...))
	if len(unique) != 1 {
		return fmt.Errorf("expected only one destination, got: %v", unique)
	}
	return nil
}

func flatten(clients ...[]echo.Instance) []echo.Instance {
	var instances []echo.Instance
	for _, c := range clients {
		instances = append(instances, c...)
	}
	return instances
}

// selfCallsCases checks that pods can call themselves
func selfCallsCases(t TrafficContext) {
	t.SetDefaultSourceMatchers(match.NotExternal, match.NotNaked, match.NotHeadless, match.NotProxylessGRPC)
	t.SetDefaultComboFilter(func(from echo.Instance, to echo.Instances) echo.Instances {
		return match.ServiceName(from.NamespacedName()).GetMatches(to)
	})
	// Calls to the Service will go through envoy outbound and inbound, so we get envoy headers added
	t.RunTraffic(TrafficTestCase{
		name:             "to service",
		workloadAgnostic: true,

		opts: echo.CallOptions{
			Count: 1,
			Port: echo.Port{
				Name: "http",
			},
			Check: check.And(
				check.OK(),
				check.RequestHeader("X-Envoy-Attempt-Count", "1")),
		},
	})
	// Localhost calls will go directly to localhost, bypassing Envoy. No envoy headers added.
	t.RunTraffic(TrafficTestCase{
		name:             "to localhost",
		workloadAgnostic: true,
		setupOpts: func(_ echo.Caller, opts *echo.CallOptions) {
			// the framework will try to set this when enumerating test cases
			opts.To = nil
		},
		opts: echo.CallOptions{
			Count:   1,
			Address: "localhost",
			Port:    echo.Port{ServicePort: 8080},
			Scheme:  scheme.HTTP,
			Check: check.And(
				check.OK(),
				check.RequestHeader("X-Envoy-Attempt-Count", "")),
		},
	})
	// PodIP calls will go directly to podIP, bypassing Envoy. No envoy headers added.
	t.RunTraffic(TrafficTestCase{
		name:             "to podIP",
		workloadAgnostic: true,
		setupOpts: func(srcCaller echo.Caller, opts *echo.CallOptions) {
			src := srcCaller.(echo.Instance)
			workloads, _ := src.Workloads()
			opts.Address = workloads[0].Address()
			// the framework will try to set this when enumerating test cases
			opts.To = nil
		},
		opts: echo.CallOptions{
			Count:  1,
			Scheme: scheme.HTTP,
			Port:   echo.Port{ServicePort: 8080},
			Check: check.And(
				check.OK(),
				check.RequestHeader("X-Envoy-Attempt-Count", "")),
		},
	})
}

// TODO: merge with security TestReachability code
func protocolSniffingCases(t TrafficContext) {
	type protocolCase struct {
		// The port we call
		port string
		// The actual type of traffic we send to the port
		scheme scheme.Instance
	}
	protocols := []protocolCase{
		{"http", scheme.HTTP},
		{"auto-http", scheme.HTTP},
		{"tcp", scheme.TCP},
		{"auto-tcp", scheme.TCP},
		{"grpc", scheme.GRPC},
		{"auto-grpc", scheme.GRPC},
	}

	// so we can check all clusters are hit
	for _, call := range protocols {
		call := call
		t.RunTraffic(TrafficTestCase{
			skip: skip{
				skip:   call.scheme == scheme.TCP,
				reason: "https://github.com/istio/istio/issues/26798: enable sniffing tcp",
			},
			name: call.port,
			opts: echo.CallOptions{
				Count: 1,
				Port: echo.Port{
					Name: call.port,
				},
				Scheme:  call.scheme,
				Timeout: time.Second * 5,
			},
			check: func(src echo.Caller, opts *echo.CallOptions) echo.Checker {
				if call.scheme == scheme.TCP || src.(echo.Instance).Config().IsProxylessGRPC() {
					// no host header for TCP
					// TODO understand why proxyless adds the port to :authority md
					return check.OK()
				}
				return check.And(
					check.OK(),
					check.Host(opts.GetHost()))
			},
			comboFilters: func() []echotest.CombinationFilter {
				if call.scheme != scheme.GRPC {
					return []echotest.CombinationFilter{func(from echo.Instance, to echo.Instances) echo.Instances {
						if from.Config().IsProxylessGRPC() && match.VM.Any(to) {
							return nil
						}
						return to
					}}
				}
				return nil
			}(),
			workloadAgnostic: true,
		})
	}

	autoPort := ports.All().MustForName("auto-http")
	httpPort := ports.All().MustForName("http")
	// Tests for http1.0. Golang does not support 1.0 client requests at all
	// To simulate these, we use TCP and hand-craft the requests.
	t.RunTraffic(TrafficTestCase{
		name: "http10 to http",
		call: t.Apps.A[0].CallOrFail,
		opts: echo.CallOptions{
			To:    t.Apps.B,
			Count: 1,
			Port: echo.Port{
				Name: "http",
			},
			Scheme: scheme.TCP,
			Message: `GET / HTTP/1.0
`,
			Timeout: time.Second * 5,
			TCP: echo.TCP{
				// Explicitly declared as HTTP, so we always go through http filter which fails
				ExpectedResponse: &wrappers.StringValue{Value: `HTTP/1.1 426 Upgrade Required`},
			},
		},
	})
	t.RunTraffic(TrafficTestCase{
		name: "http10 to auto",
		call: t.Apps.A[0].CallOrFail,
		opts: echo.CallOptions{
			To:    t.Apps.B,
			Count: 1,
			Port: echo.Port{
				Name: "auto-http",
			},
			Scheme: scheme.TCP,
			Message: `GET / HTTP/1.0
`,
			Timeout: time.Second * 5,
			TCP: echo.TCP{
				// Auto should be detected as TCP
				ExpectedResponse: &wrappers.StringValue{Value: `HTTP/1.0 200 OK`},
			},
		},
	})
	t.RunTraffic(TrafficTestCase{
		name: "http10 to external",
		call: t.Apps.A[0].CallOrFail,
		opts: echo.CallOptions{
			Address: t.Apps.External.All[0].Address(),
			HTTP: echo.HTTP{
				Headers: HostHeader(t.Apps.External.All.Config().DefaultHostHeader),
			},
			Port:   httpPort,
			Count:  1,
			Scheme: scheme.TCP,
			Message: `GET / HTTP/1.0
`,
			Timeout: time.Second * 5,
			TCP: echo.TCP{
				// There is no VIP so we fall back to 0.0.0.0 listener which sniffs
				ExpectedResponse: &wrappers.StringValue{Value: `HTTP/1.0 200 OK`},
			},
		},
	})
	t.RunTraffic(TrafficTestCase{
		name: "http10 to external auto",
		call: t.Apps.A[0].CallOrFail,
		opts: echo.CallOptions{
			Address: t.Apps.External.All[0].Address(),
			HTTP: echo.HTTP{
				Headers: HostHeader(t.Apps.External.All.Config().DefaultHostHeader),
			},
			Port:   autoPort,
			Count:  1,
			Scheme: scheme.TCP,
			Message: `GET / HTTP/1.0
`,
			Timeout: time.Second * 5,
			TCP: echo.TCP{
				// Auto should be detected as TCP
				ExpectedResponse: &wrappers.StringValue{Value: `HTTP/1.0 200 OK`},
			},
		},
	},
	)
}

// Todo merge with security TestReachability code
func instanceIPTests(t TrafficContext) {
	// proxyless doesn't get valuable coverage here
	t.SetDefaultTargetMatchers(match.NotProxylessGRPC)
	t.SetDefaultSourceMatchers(match.NotProxylessGRPC)

	ipCases := []struct {
		name            string
		endpoint        string
		disableSidecar  bool
		port            string
		code            int
		minIstioVersion string
	}{
		// instance IP bind
		{
			name:           "instance IP without sidecar",
			disableSidecar: true,
			port:           "http-instance",
			code:           http.StatusOK,
		},
		{
			name:     "instance IP with wildcard sidecar",
			endpoint: "0.0.0.0",
			port:     "http-instance",
			code:     http.StatusOK,
		},
		{
			name:     "instance IP with localhost sidecar",
			endpoint: "127.0.0.1",
			port:     "http-instance",
			code:     http.StatusServiceUnavailable,
		},
		{
			name:     "instance IP with empty sidecar",
			endpoint: "",
			port:     "http-instance",
			code:     http.StatusOK,
		},

		// Localhost bind
		{
			name:           "localhost IP without sidecar",
			disableSidecar: true,
			port:           "http-localhost",
			code:           http.StatusServiceUnavailable,
			// when testing with pre-1.10 versions this request succeeds
			minIstioVersion: "1.10.0",
		},
		{
			name:     "localhost IP with wildcard sidecar",
			endpoint: "0.0.0.0",
			port:     "http-localhost",
			code:     http.StatusServiceUnavailable,
		},
		{
			name:     "localhost IP with localhost sidecar",
			endpoint: "127.0.0.1",
			port:     "http-localhost",
			code:     http.StatusOK,
		},
		{
			name:     "localhost IP with empty sidecar",
			endpoint: "",
			port:     "http-localhost",
			code:     http.StatusServiceUnavailable,
			// when testing with pre-1.10 versions this request succeeds
			minIstioVersion: "1.10.0",
		},

		// Wildcard bind
		{
			name:           "wildcard IP without sidecar",
			disableSidecar: true,
			port:           "http",
			code:           http.StatusOK,
		},
		{
			name:     "wildcard IP with wildcard sidecar",
			endpoint: "0.0.0.0",
			port:     "http",
			code:     http.StatusOK,
		},
		{
			name:     "wildcard IP with localhost sidecar",
			endpoint: "127.0.0.1",
			port:     "http",
			code:     http.StatusOK,
		},
		{
			name:     "wildcard IP with empty sidecar",
			endpoint: "",
			port:     "http",
			code:     http.StatusOK,
		},
	}
	for _, ipCase := range ipCases {
		for _, client := range t.Apps.A {
			ipCase := ipCase
			client := client
			to := t.Apps.B
			var config string
			if !ipCase.disableSidecar {
				config = fmt.Sprintf(`
apiVersion: networking.istio.io/v1alpha3
kind: Sidecar
metadata:
  name: sidecar
spec:
  workloadSelector:
    labels:
      app: b
  egress:
  - hosts:
    - "./*"
  ingress:
  - port:
      number: %d
      protocol: HTTP
    defaultEndpoint: %s:%d
`, ports.All().MustForName(ipCase.port).WorkloadPort, ipCase.endpoint, ports.All().MustForName(ipCase.port).WorkloadPort)
			}
			t.RunTraffic(TrafficTestCase{
				name:   ipCase.name,
				call:   client.CallOrFail,
				config: config,
				opts: echo.CallOptions{
					Count: 1,
					To:    to,
					Port: echo.Port{
						Name: ipCase.port,
					},
					Scheme:  scheme.HTTP,
					Timeout: time.Second * 5,
					Check:   check.Status(ipCase.code),
				},
				minIstioVersion: ipCase.minIstioVersion,
			})
		}
	}
}

type vmCase struct {
	name string
	from echo.Instance
	to   echo.Instances
	host string
}

func DNSTestCases(t TrafficContext) {
	makeSE := func(ips ...string) string {
		return tmpl.MustEvaluate(`
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: dns
spec:
  hosts:
  - "fake.service.local"
  addresses:
{{ range $ip := .IPs }}
  - "{{$ip}}"
{{ end }}
  resolution: STATIC
  endpoints: []
  ports:
  - number: 80
    name: http
    protocol: HTTP
`, map[string]any{"IPs": ips})
	}
	ipv4 := "1.2.3.4"
	ipv6 := "1234:1234:1234::1234:1234:1234"
	dummyLocalhostServer := "127.0.0.1"
	cases := []struct {
		name string
		// TODO(https://github.com/istio/istio/issues/30282) support multiple vips
		ips      string
		protocol string
		server   string
		skipCNI  bool
		expected []string
	}{
		{
			name:     "tcp ipv4",
			ips:      ipv4,
			expected: []string{ipv4},
			protocol: "tcp",
		},
		{
			name:     "udp ipv4",
			ips:      ipv4,
			expected: []string{ipv4},
			protocol: "udp",
		},
		{
			name:     "tcp ipv6",
			ips:      ipv6,
			expected: []string{ipv6},
			protocol: "tcp",
		},
		{
			name:     "udp ipv6",
			ips:      ipv6,
			expected: []string{ipv6},
			protocol: "udp",
		},
		{
			// We should only capture traffic to servers in /etc/resolv.conf nameservers
			// This checks we do not capture traffic to other servers.
			// This is important for cases like app -> istio dns server -> dnsmasq -> upstream
			// If we captured all DNS traffic, we would loop dnsmasq traffic back to our server.
			name:     "tcp localhost server",
			ips:      ipv4,
			expected: nil,
			protocol: "tcp",
			skipCNI:  true,
			server:   dummyLocalhostServer,
		},
		{
			name:     "udp localhost server",
			ips:      ipv4,
			expected: nil,
			protocol: "udp",
			skipCNI:  true,
			server:   dummyLocalhostServer,
		},
	}
	for _, client := range flatten(t.Apps.VM, t.Apps.A, t.Apps.Tproxy) {
		for _, tt := range cases {
			if tt.skipCNI && t.Istio.Settings().EnableCNI {
				continue
			}
			tt, client := tt, client
			address := "fake.service.local?"
			if tt.protocol != "" {
				address += "&protocol=" + tt.protocol
			}
			if tt.server != "" {
				address += "&server=" + tt.server
			}
			var checker echo.Checker = func(result echo.CallResult, _ error) error {
				for _, r := range result.Responses {
					if !reflect.DeepEqual(r.Body(), tt.expected) {
						return fmt.Errorf("unexpected dns response: wanted %v, got %v", tt.expected, r.Body())
					}
				}
				return nil
			}
			if tt.expected == nil {
				checker = check.Error()
			}
			t.RunTraffic(TrafficTestCase{
				name:   fmt.Sprintf("%s/%s", client.Config().Service, tt.name),
				config: makeSE(tt.ips),
				call:   client.CallOrFail,
				opts: echo.CallOptions{
					Scheme:  scheme.DNS,
					Count:   1,
					Address: address,
					Check:   checker,
				},
			})
		}
	}
	svcCases := []struct {
		name     string
		protocol string
		server   string
	}{
		{
			name:     "tcp",
			protocol: "tcp",
		},
		{
			name:     "udp",
			protocol: "udp",
		},
	}
	for _, client := range flatten(t.Apps.VM, t.Apps.A, t.Apps.Tproxy) {
		for _, tt := range svcCases {
			tt, client := tt, client
			aInCluster := match.Cluster(client.Config().Cluster).GetMatches(t.Apps.A)
			if len(aInCluster) == 0 {
				// The cluster doesn't contain A, but connects to a cluster containing A
				aInCluster = match.Cluster(client.Config().Cluster.Config()).GetMatches(t.Apps.A)
			}
			address := aInCluster[0].Config().ClusterLocalFQDN() + "?"
			if tt.protocol != "" {
				address += "&protocol=" + tt.protocol
			}
			if tt.server != "" {
				address += "&server=" + tt.server
			}
			expected := aInCluster[0].Address()
			t.RunTraffic(TrafficTestCase{
				name: fmt.Sprintf("svc/%s/%s", client.Config().Service, tt.name),
				call: client.CallOrFail,
				opts: echo.CallOptions{
					Count:   1,
					Scheme:  scheme.DNS,
					Address: address,
					Check: func(result echo.CallResult, _ error) error {
						for _, r := range result.Responses {
							ips := r.Body()
							sort.Strings(ips)
							exp := []string{expected}
							if !reflect.DeepEqual(ips, exp) {
								return fmt.Errorf("unexpected dns response: wanted %v, got %v", exp, ips)
							}
						}
						return nil
					},
				},
			})
		}
	}
}

func VMTestCases(vms echo.Instances) func(t TrafficContext) {
	return func(t TrafficContext) {
		if t.Settings().Skip(echo.VM) {
			t.Skipf("VMs are disabled")
		}
		var testCases []vmCase

		for _, vm := range vms {
			testCases = append(testCases,
				vmCase{
					name: "dns: VM to k8s cluster IP service name.namespace host",
					from: vm,
					to:   t.Apps.A,
					host: deployment.ASvc + "." + t.Apps.Namespace.Name(),
				},
				vmCase{
					name: "dns: VM to k8s cluster IP service fqdn host",
					from: vm,
					to:   t.Apps.A,
					host: t.Apps.A[0].Config().ClusterLocalFQDN(),
				},
				vmCase{
					name: "dns: VM to k8s cluster IP service short name host",
					from: vm,
					to:   t.Apps.A,
					host: deployment.ASvc,
				},
				vmCase{
					name: "dns: VM to k8s headless service",
					from: vm,
					to:   match.Cluster(vm.Config().Cluster.Config()).GetMatches(t.Apps.Headless),
					host: t.Apps.Headless.Config().ClusterLocalFQDN(),
				},
				vmCase{
					name: "dns: VM to k8s statefulset service",
					from: vm,
					to:   match.Cluster(vm.Config().Cluster.Config()).GetMatches(t.Apps.StatefulSet),
					host: t.Apps.StatefulSet.Config().ClusterLocalFQDN(),
				},
				// TODO(https://github.com/istio/istio/issues/32552) re-enable
				//vmCase{
				//	name: "dns: VM to k8s statefulset instance.service",
				//	from: vm,
				//	to:   apps.StatefulSet.Match(echo.Cluster(vm.Config().Cluster.Config())),
				//	host: fmt.Sprintf("%s-v1-0.%s", StatefulSetSvc, StatefulSetSvc),
				//},
				//vmCase{
				//	name: "dns: VM to k8s statefulset instance.service.namespace",
				//	from: vm,
				//	to:   apps.StatefulSet.Match(echo.Cluster(vm.Config().Cluster.Config())),
				//	host: fmt.Sprintf("%s-v1-0.%s.%s", StatefulSetSvc, StatefulSetSvc, apps.Namespace.Name()),
				//},
				//vmCase{
				//	name: "dns: VM to k8s statefulset instance.service.namespace.svc",
				//	from: vm,
				//	to:   apps.StatefulSet.Match(echo.Cluster(vm.Config().Cluster.Config())),
				//	host: fmt.Sprintf("%s-v1-0.%s.%s.svc", StatefulSetSvc, StatefulSetSvc, apps.Namespace.Name()),
				//},
				//vmCase{
				//	name: "dns: VM to k8s statefulset instance FQDN",
				//	from: vm,
				//	to:   apps.StatefulSet.Match(echo.Cluster(vm.Config().Cluster.Config())),
				//	host: fmt.Sprintf("%s-v1-0.%s", StatefulSetSvc, apps.StatefulSet[0].Config().ClusterLocalFQDN()),
				//},
			)
		}
		for _, podA := range t.Apps.A {
			testCases = append(testCases, vmCase{
				name: "k8s to vm",
				from: podA,
				to:   vms,
			})
		}
		for _, c := range testCases {
			c := c
			checker := check.OK()
			if !match.Headless.Any(c.to) {
				// headless load-balancing can be inconsistent
				checker = check.And(checker, check.ReachedTargetClusters(t))
			}
			t.RunTraffic(TrafficTestCase{
				name: fmt.Sprintf("%s from %s", c.name, c.from.Config().Cluster.StableName()),
				call: c.from.CallOrFail,
				opts: echo.CallOptions{
					// assume that all echos in `to` only differ in which cluster they're deployed in
					To: c.to,
					Port: echo.Port{
						Name: "http",
					},
					Address: c.host,
					Check:   checker,
				},
			})
		}
	}
}

func destinationRule(app, mode string) string {
	return fmt.Sprintf(`apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: %s
spec:
  host: %s
  trafficPolicy:
    tls:
      mode: %s
---
`, app, app, mode)
}

const useClientProtocolDestinationRuleTmpl = `apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: use-client-protocol
spec:
  host: {{.VirtualServiceHost}}
  trafficPolicy:
    tls:
      mode: DISABLE
    connectionPool:
      http:
        useClientProtocol: true
---
`

func useClientProtocolDestinationRule(app string) string {
	return tmpl.MustEvaluate(useClientProtocolDestinationRuleTmpl, map[string]string{"VirtualServiceHost": app})
}

func idletimeoutDestinationRule(name, app string) string {
	return fmt.Sprintf(`apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: %s
spec:
  host: %s
  trafficPolicy:
    tls:
      mode: DISABLE
    connectionPool:
      http:
        idleTimeout: 100s
---
`, name, app)
}

func peerAuthentication(app, mode string) string {
	return fmt.Sprintf(`apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: %s
spec:
  selector:
    matchLabels:
      app: %s
  mtls:
    mode: %s
---
`, app, app, mode)
}

func globalPeerAuthentication(mode string) string {
	return fmt.Sprintf(`apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
spec:
  mtls:
    mode: %s
---
`, mode)
}

func serverFirstTestCases(t TrafficContext) {
	from := t.Apps.A
	to := t.Apps.C
	configs := []struct {
		port    string
		dest    string
		auth    string
		checker echo.Checker
	}{
		// TODO: All these cases *should* succeed (except the TLS mismatch cases) - but don't due to issues in our implementation

		// For auto port, outbound request will be delayed by the protocol sniffer, regardless of configuration
		{"auto-tcp-server", "DISABLE", "DISABLE", check.Error()},
		{"auto-tcp-server", "DISABLE", "PERMISSIVE", check.Error()},
		{"auto-tcp-server", "DISABLE", "STRICT", check.Error()},
		{"auto-tcp-server", "ISTIO_MUTUAL", "DISABLE", check.Error()},
		{"auto-tcp-server", "ISTIO_MUTUAL", "PERMISSIVE", check.Error()},
		{"auto-tcp-server", "ISTIO_MUTUAL", "STRICT", check.Error()},

		// These is broken because we will still enable inbound sniffing for the port. Since there is no tls,
		// there is no server-first "upgrading" to client-first
		{"tcp-server", "DISABLE", "DISABLE", check.OK()},
		{"tcp-server", "DISABLE", "PERMISSIVE", check.Error()},

		// Expected to fail, incompatible configuration
		{"tcp-server", "DISABLE", "STRICT", check.Error()},
		{"tcp-server", "ISTIO_MUTUAL", "DISABLE", check.Error()},

		// In these cases, we expect success
		// There is no sniffer on either side
		{"tcp-server", "DISABLE", "DISABLE", check.OK()},

		// On outbound, we have no sniffer involved
		// On inbound, the request is TLS, so its not server first
		{"tcp-server", "ISTIO_MUTUAL", "PERMISSIVE", check.OK()},
		{"tcp-server", "ISTIO_MUTUAL", "STRICT", check.OK()},
	}
	for _, client := range from {
		for _, c := range configs {
			client, c := client, c
			t.RunTraffic(TrafficTestCase{
				name: fmt.Sprintf("%v:%v/%v", c.port, c.dest, c.auth),
				skip: skip{
					skip:   t.Apps.All.Instances().Clusters().IsMulticluster(),
					reason: "https://github.com/istio/istio/issues/37305: stabilize tcp connection breaks",
				},
				config: destinationRule(to.Config().Service, c.dest) + peerAuthentication(to.Config().Service, c.auth),
				call:   client.CallOrFail,
				opts: echo.CallOptions{
					To: to,
					Port: echo.Port{
						Name: c.port,
					},
					Scheme: scheme.TCP,
					// Inbound timeout is 1s. We want to test this does not hit the listener filter timeout
					Timeout: time.Millisecond * 100,
					Count:   1,
					Check:   c.checker,
				},
			})
		}
	}
}

func jwtClaimRoute(t TrafficContext) {
	if t.Settings().Selector.Excludes(label.NewSet(label.IPv4)) {
		t.Skipf("https://github.com/istio/istio/issues/35835")
	}
	configRoute := `
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: gateway
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "*"
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: default
spec:
  hosts:
  - foo.bar
  gateways:
  - gateway
  http:
  - match:
    - uri:
        prefix: /
      {{- if .Headers }}
      headers:
        {{- range $data := .Headers }}
          "{{$data.Name}}":
            {{$data.Match}}: {{$data.Value}}
        {{- end }}
      {{- end }}
      {{- if .WithoutHeaders }}
      withoutHeaders:
        {{- range $data := .WithoutHeaders }}
          "{{$data.Name}}":
            {{$data.Match}}: {{$data.Value}}
        {{- end }}
      {{- end }}
    route:
    - destination:
        host: {{ .dstSvc }}
---
`
	configAll := configRoute + `
apiVersion: security.istio.io/v1beta1
kind: RequestAuthentication
metadata:
  name: default
  namespace: istio-system
spec:
  jwtRules:
  - issuer: "test-issuer-1@istio.io"
    jwksUri: "https://raw.githubusercontent.com/istio/istio/master/tests/common/jwt/jwks.json"
    outputClaimToHeaders:
    - header: "x-jwt-nested-key"
      claim: "nested.nested-2.key2"
    - header: "x-jwt-iss"
      claim: "iss"
    - header: "x-jwt-wrong-header"
      claim: "wrong_claim"
---
`
	podB := []match.Matcher{match.ServiceName(t.Apps.B.NamespacedName())}
	headersWithToken := map[string][]string{
		"Host":          {"foo.bar"},
		"Authorization": {"Bearer " + jwt.TokenIssuer1WithNestedClaims1},
	}
	headersWithInvalidToken := map[string][]string{
		"Host":          {"foo.bar"},
		"Authorization": {"Bearer " + jwt.TokenExpired},
	}
	headersWithNoToken := map[string][]string{"Host": {"foo.bar"}}
	headersWithNoTokenButSameHeader := map[string][]string{
		"Host":                            {"foo.bar"},
		"request.auth.claims.nested.key1": {"valueA"},
	}
	headersWithToken2 := map[string][]string{
		"Host":             {"foo.bar"},
		"Authorization":    {"Bearer " + jwt.TokenIssuer1WithNestedClaims2},
		"X-Jwt-Nested-Key": {"value_to_be_replaced"},
	}
	headersWithToken2WithAddedHeader := map[string][]string{
		"Host":               {"foo.bar"},
		"Authorization":      {"Bearer " + jwt.TokenIssuer1WithNestedClaims2},
		"x-jwt-wrong-header": {"header_to_be_deleted"},
	}

	type configData struct {
		Name, Match, Value string
	}

	t.RunTraffic(TrafficTestCase{
		name:             "matched with nested claim using claim to header:200",
		targetMatchers:   podB,
		workloadAgnostic: true,
		viaIngress:       true,
		config:           configAll,
		templateVars: func(src echo.Callers, dest echo.Instances) map[string]any {
			return map[string]any{
				"Headers": []configData{{"X-Jwt-Nested-Key", "exact", "valueC"}},
			}
		},
		opts: echo.CallOptions{
			Count: 1,
			Port: echo.Port{
				Name:     "http",
				Protocol: protocol.HTTP,
			},
			HTTP: echo.HTTP{
				Headers: headersWithToken2,
			},
			Check: check.Status(http.StatusOK),
		},
	})
	t.RunTraffic(TrafficTestCase{
		name:             "matched with nested claim and single claim using claim to header:200",
		targetMatchers:   podB,
		workloadAgnostic: true,
		viaIngress:       true,
		config:           configAll,
		templateVars: func(src echo.Callers, dest echo.Instances) map[string]any {
			return map[string]any{
				"Headers": []configData{
					{"X-Jwt-Nested-Key", "exact", "valueC"},
					{"X-Jwt-Iss", "exact", "test-issuer-1@istio.io"},
				},
			}
		},
		opts: echo.CallOptions{
			Count: 1,
			Port: echo.Port{
				Name:     "http",
				Protocol: protocol.HTTP,
			},
			HTTP: echo.HTTP{
				Headers: headersWithToken2,
			},
			Check: check.Status(http.StatusOK),
		},
	})
	t.RunTraffic(TrafficTestCase{
		name:             "unmatched with wrong claim and added header:404",
		targetMatchers:   podB,
		workloadAgnostic: true,
		viaIngress:       true,
		config:           configAll,
		templateVars: func(src echo.Callers, dest echo.Instances) map[string]any {
			return map[string]any{
				"Headers": []configData{{"x-jwt-wrong-header", "exact", "header_to_be_deleted"}},
			}
		},
		opts: echo.CallOptions{
			Count: 1,
			Port: echo.Port{
				Name:     "http",
				Protocol: protocol.HTTP,
			},
			HTTP: echo.HTTP{
				Headers: headersWithToken2WithAddedHeader,
			},
			Check: check.Status(http.StatusNotFound),
		},
	})
	t.RunTraffic(TrafficTestCase{
		name:             "matched with nested claims:200",
		targetMatchers:   podB,
		workloadAgnostic: true,
		viaIngress:       true,
		config:           configAll,
		templateVars: func(src echo.Callers, dest echo.Instances) map[string]any {
			return map[string]any{
				"Headers": []configData{{"@request.auth.claims.nested.key1", "exact", "valueA"}},
			}
		},
		opts: echo.CallOptions{
			Count: 1,
			Port: echo.Port{
				Name:     "http",
				Protocol: protocol.HTTP,
			},
			HTTP: echo.HTTP{
				Headers: headersWithToken,
			},
			Check: check.Status(http.StatusOK),
		},
	})
	t.RunTraffic(TrafficTestCase{
		name:             "matched with single claim:200",
		targetMatchers:   podB,
		workloadAgnostic: true,
		viaIngress:       true,
		config:           configAll,
		templateVars: func(src echo.Callers, dest echo.Instances) map[string]any {
			return map[string]any{
				"Headers": []configData{{"@request.auth.claims.sub", "prefix", "sub"}},
			}
		},
		opts: echo.CallOptions{
			Count: 1,
			Port: echo.Port{
				Name:     "http",
				Protocol: protocol.HTTP,
			},
			HTTP: echo.HTTP{
				Headers: headersWithToken,
			},
			Check: check.Status(http.StatusOK),
		},
	})
	t.RunTraffic(TrafficTestCase{
		name:             "matched multiple claims with regex:200",
		targetMatchers:   podB,
		workloadAgnostic: true,
		viaIngress:       true,
		config:           configAll,
		templateVars: func(src echo.Callers, dest echo.Instances) map[string]any {
			return map[string]any{
				"Headers": []configData{
					{"@request.auth.claims.sub", "regex", "(\\W|^)(sub-1|sub-2)(\\W|$)"},
					{"@request.auth.claims.nested.key1", "regex", "(\\W|^)value[AB](\\W|$)"},
				},
			}
		},
		opts: echo.CallOptions{
			Count: 1,
			Port: echo.Port{
				Name:     "http",
				Protocol: protocol.HTTP,
			},
			HTTP: echo.HTTP{
				Headers: headersWithToken,
			},
			Check: check.Status(http.StatusOK),
		},
	})
	t.RunTraffic(TrafficTestCase{
		name:             "matched multiple claims:200",
		targetMatchers:   podB,
		workloadAgnostic: true,
		viaIngress:       true,
		config:           configAll,
		templateVars: func(src echo.Callers, dest echo.Instances) map[string]any {
			return map[string]any{
				"Headers": []configData{
					{"@request.auth.claims.nested.key1", "exact", "valueA"},
					{"@request.auth.claims.sub", "prefix", "sub"},
				},
			}
		},
		opts: echo.CallOptions{
			Count: 1,
			Port: echo.Port{
				Name:     "http",
				Protocol: protocol.HTTP,
			},
			HTTP: echo.HTTP{
				Headers: headersWithToken,
			},
			Check: check.Status(http.StatusOK),
		},
	})
	t.RunTraffic(TrafficTestCase{
		name:             "matched without claim:200",
		targetMatchers:   podB,
		workloadAgnostic: true,
		viaIngress:       true,
		config:           configAll,
		templateVars: func(src echo.Callers, dest echo.Instances) map[string]any {
			return map[string]any{
				"WithoutHeaders": []configData{{"@request.auth.claims.nested.key1", "exact", "value-not-matched"}},
			}
		},
		opts: echo.CallOptions{
			Count: 1,
			Port: echo.Port{
				Name:     "http",
				Protocol: protocol.HTTP,
			},
			HTTP: echo.HTTP{
				Headers: headersWithToken,
			},
			Check: check.Status(http.StatusOK),
		},
	})
	t.RunTraffic(TrafficTestCase{
		name:             "unmatched without claim:404",
		targetMatchers:   podB,
		workloadAgnostic: true,
		viaIngress:       true,
		config:           configAll,
		templateVars: func(src echo.Callers, dest echo.Instances) map[string]any {
			return map[string]any{
				"WithoutHeaders": []configData{{"@request.auth.claims.nested.key1", "exact", "valueA"}},
			}
		},
		opts: echo.CallOptions{
			Count: 1,
			Port: echo.Port{
				Name:     "http",
				Protocol: protocol.HTTP,
			},
			HTTP: echo.HTTP{
				Headers: headersWithToken,
			},
			Check: check.Status(http.StatusNotFound),
		},
	})
	t.RunTraffic(TrafficTestCase{
		name:             "matched both with and without claims with regex:200",
		targetMatchers:   podB,
		workloadAgnostic: true,
		viaIngress:       true,
		config:           configAll,
		templateVars: func(src echo.Callers, dest echo.Instances) map[string]any {
			return map[string]any{
				"Headers": []configData{{"@request.auth.claims.sub", "prefix", "sub"}},
				"WithoutHeaders": []configData{
					{"@request.auth.claims.nested.key1", "exact", "value-not-matched"},
					{"@request.auth.claims.nested.key1", "regex", "(\\W|^)value\\s{0,3}not{0,1}\\s{0,3}matched(\\W|$)"},
				},
			}
		},
		opts: echo.CallOptions{
			Count: 1,
			Port: echo.Port{
				Name:     "http",
				Protocol: protocol.HTTP,
			},
			HTTP: echo.HTTP{
				Headers: headersWithToken,
			},
			Check: check.Status(http.StatusOK),
		},
	})
	t.RunTraffic(TrafficTestCase{
		name:             "unmatched multiple claims:404",
		targetMatchers:   podB,
		workloadAgnostic: true,
		viaIngress:       true,
		config:           configAll,
		templateVars: func(src echo.Callers, dest echo.Instances) map[string]any {
			return map[string]any{
				"Headers": []configData{
					{"@request.auth.claims.nested.key1", "exact", "valueA"},
					{"@request.auth.claims.sub", "prefix", "value-not-matched"},
				},
			}
		},
		opts: echo.CallOptions{
			Count: 1,
			Port: echo.Port{
				Name:     "http",
				Protocol: protocol.HTTP,
			},
			HTTP: echo.HTTP{
				Headers: headersWithToken,
			},
			Check: check.Status(http.StatusNotFound),
		},
	})
	t.RunTraffic(TrafficTestCase{
		name:             "unmatched token:404",
		targetMatchers:   podB,
		workloadAgnostic: true,
		viaIngress:       true,
		config:           configAll,
		templateVars: func(src echo.Callers, dest echo.Instances) map[string]any {
			return map[string]any{
				"Headers": []configData{{"@request.auth.claims.sub", "exact", "value-not-matched"}},
			}
		},
		opts: echo.CallOptions{
			Count: 1,
			Port: echo.Port{
				Name:     "http",
				Protocol: protocol.HTTP,
			},
			HTTP: echo.HTTP{
				Headers: headersWithToken,
			},
			Check: check.Status(http.StatusNotFound),
		},
	})
	t.RunTraffic(TrafficTestCase{
		name:             "unmatched with invalid token:401",
		targetMatchers:   podB,
		workloadAgnostic: true,
		viaIngress:       true,
		config:           configAll,
		templateVars: func(src echo.Callers, dest echo.Instances) map[string]any {
			return map[string]any{
				"Headers": []configData{{"@request.auth.claims.nested.key1", "exact", "valueA"}},
			}
		},
		opts: echo.CallOptions{
			Count: 1,
			Port: echo.Port{
				Name:     "http",
				Protocol: protocol.HTTP,
			},
			HTTP: echo.HTTP{
				Headers: headersWithInvalidToken,
			},
			Check: check.Status(http.StatusUnauthorized),
		},
	})
	t.RunTraffic(TrafficTestCase{
		name:             "unmatched with no token:404",
		targetMatchers:   podB,
		workloadAgnostic: true,
		viaIngress:       true,
		config:           configAll,
		templateVars: func(src echo.Callers, dest echo.Instances) map[string]any {
			return map[string]any{
				"Headers": []configData{{"@request.auth.claims.nested.key1", "exact", "valueA"}},
			}
		},
		opts: echo.CallOptions{
			Count: 1,
			Port: echo.Port{
				Name:     "http",
				Protocol: protocol.HTTP,
			},
			HTTP: echo.HTTP{
				Headers: headersWithNoToken,
			},
			Check: check.Status(http.StatusNotFound),
		},
	})
	t.RunTraffic(TrafficTestCase{
		name:             "unmatched with no token but same header:404",
		targetMatchers:   podB,
		workloadAgnostic: true,
		viaIngress:       true,
		config:           configAll,
		templateVars: func(src echo.Callers, dest echo.Instances) map[string]any {
			return map[string]any{
				"Headers": []configData{{"@request.auth.claims.nested.key1", "exact", "valueA"}},
			}
		},
		opts: echo.CallOptions{
			Count: 1,
			Port: echo.Port{
				Name:     "http",
				Protocol: protocol.HTTP,
			},
			HTTP: echo.HTTP{
				// Include a header @request.auth.claims.nested.key1 and value same as the JWT claim, should not be routed.
				Headers: headersWithNoTokenButSameHeader,
			},
			Check: check.Status(http.StatusNotFound),
		},
	})
	t.RunTraffic(TrafficTestCase{
		name:             "unmatched with no request authentication:404",
		targetMatchers:   podB,
		workloadAgnostic: true,
		viaIngress:       true,
		config:           configRoute,
		templateVars: func(src echo.Callers, dest echo.Instances) map[string]any {
			return map[string]any{
				"Headers": []configData{{"@request.auth.claims.nested.key1", "exact", "valueA"}},
			}
		},
		opts: echo.CallOptions{
			Count: 1,
			Port: echo.Port{
				Name:     "http",
				Protocol: protocol.HTTP,
			},
			HTTP: echo.HTTP{
				Headers: headersWithToken,
			},
			Check: check.Status(http.StatusNotFound),
		},
	})
}
