blob: 1e7d4ff459e05d40fe9d1cfbff1e55e50b128099 [file] [log] [blame]
// 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
import (
"fmt"
"reflect"
"strconv"
"strings"
"testing"
"time"
)
import (
core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
hcm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
tcp "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/tcp_proxy/v3"
tracing "github.com/envoyproxy/go-control-plane/envoy/type/tracing/v3"
xdstype "github.com/envoyproxy/go-control-plane/envoy/type/v3"
"github.com/envoyproxy/go-control-plane/pkg/conversion"
"github.com/envoyproxy/go-control-plane/pkg/wellknown"
"github.com/google/go-cmp/cmp"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/testing/protocmp"
"google.golang.org/protobuf/types/known/durationpb"
wrappers "google.golang.org/protobuf/types/known/wrapperspb"
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/listenertest"
"github.com/apache/dubbo-go-pixiu/pilot/pkg/networking/util"
"github.com/apache/dubbo-go-pixiu/pilot/pkg/serviceregistry/provider"
xdsfilters "github.com/apache/dubbo-go-pixiu/pilot/pkg/xds/filters"
"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/collections"
"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/assert"
)
const (
wildcardIP = "0.0.0.0"
)
func getProxy() *model.Proxy {
pr := &model.Proxy{
Type: model.SidecarProxy,
IPAddresses: []string{"1.1.1.1"},
ID: "v0.default",
DNSDomain: "default.example.org",
Metadata: &model.NodeMetadata{
Namespace: "not-default",
},
ConfigNamespace: "not-default",
}
pr.DiscoverIPMode()
return pr
}
var (
tnow = time.Now()
proxyHTTP10 = model.Proxy{
Type: model.SidecarProxy,
IPAddresses: []string{"1.1.1.1"},
ID: "v0.default",
DNSDomain: "default.example.org",
Metadata: &model.NodeMetadata{
Namespace: "not-default",
HTTP10: "1",
},
ConfigNamespace: "not-default",
}
proxyGateway = model.Proxy{
Type: model.Router,
IPAddresses: []string{"1.1.1.1"},
ID: "v0.default",
DNSDomain: "default.example.org",
Metadata: &proxyGatewayMetadata,
ConfigNamespace: "not-default",
}
proxyGatewayMetadata = model.NodeMetadata{
Namespace: "not-default",
Labels: map[string]string{
"istio": "ingressgateway",
},
}
virtualServiceSpec = &networking.VirtualService{
Hosts: []string{"test.com"},
Gateways: []string{"mesh"},
Tcp: []*networking.TCPRoute{
{
Match: []*networking.L4MatchAttributes{
{
DestinationSubnets: []string{"10.10.0.0/24"},
Port: 8080,
},
},
Route: []*networking.RouteDestination{
{
Destination: &networking.Destination{
Host: "test.org",
Port: &networking.PortSelector{
Number: 80,
},
},
Weight: 100,
},
},
},
},
}
)
func TestInboundListenerConfig(t *testing.T) {
for _, p := range []*model.Proxy{getProxy(), &proxyHTTP10} {
t.Run("multiple services", func(t *testing.T) {
testInboundListenerConfig(t, p,
buildService("test1.com", wildcardIP, protocol.HTTP, tnow.Add(1*time.Second)),
buildService("test2.com", wildcardIP, "unknown", tnow),
buildService("test3.com", wildcardIP, protocol.HTTP, tnow.Add(2*time.Second)))
})
t.Run("no service", func(t *testing.T) {
testInboundListenerConfigWithoutService(t, p)
})
t.Run("sidecar", func(t *testing.T) {
testInboundListenerConfigWithSidecar(t, p,
buildService("test.com", wildcardIP, protocol.HTTP, tnow))
})
t.Run("sidecar with service", func(t *testing.T) {
testInboundListenerConfigWithSidecarWithoutServices(t, p)
})
}
t.Run("grpc", func(t *testing.T) {
testInboundListenerConfigWithGrpc(t, getProxy(),
buildService("test1.com", wildcardIP, protocol.GRPC, tnow.Add(1*time.Second)))
})
}
func TestOutboundListenerConflict_HTTPWithCurrentUnknown(t *testing.T) {
test.SetBoolForTest(t, &features.EnableProtocolSniffingForOutbound, true)
// The oldest service port is unknown. We should encounter conflicts when attempting to add the HTTP ports. Purposely
// storing the services out of time order to test that it's being sorted properly.
testOutboundListenerConflict(t,
buildService("test1.com", wildcardIP, protocol.HTTP, tnow.Add(1*time.Second)),
buildService("test2.com", wildcardIP, "unknown", tnow),
buildService("test3.com", wildcardIP, protocol.HTTP, tnow.Add(2*time.Second)))
}
func TestOutboundListenerConflict_WellKnowPorts(t *testing.T) {
test.SetBoolForTest(t, &features.EnableProtocolSniffingForOutbound, true)
// The oldest service port is unknown. We should encounter conflicts when attempting to add the HTTP ports. Purposely
// storing the services out of time order to test that it's being sorted properly.
testOutboundListenerConflict(t,
buildServiceWithPort("test1.com", 3306, protocol.HTTP, tnow.Add(1*time.Second)),
buildServiceWithPort("test2.com", 3306, protocol.MySQL, tnow))
testOutboundListenerConflict(t,
buildServiceWithPort("test1.com", 9999, protocol.HTTP, tnow.Add(1*time.Second)),
buildServiceWithPort("test2.com", 9999, protocol.MySQL, tnow))
}
func TestOutboundListenerConflict_TCPWithCurrentUnknown(t *testing.T) {
test.SetBoolForTest(t, &features.EnableProtocolSniffingForOutbound, true)
// The oldest service port is unknown. We should encounter conflicts when attempting to add the HTTP ports. Purposely
// storing the services out of time order to test that it's being sorted properly.
testOutboundListenerConflict(t,
buildService("test1.com", wildcardIP, protocol.TCP, tnow.Add(1*time.Second)),
buildService("test2.com", wildcardIP, "unknown", tnow),
buildService("test3.com", wildcardIP, protocol.TCP, tnow.Add(2*time.Second)))
}
func TestOutboundListenerConflict_UnknownWithCurrentTCP(t *testing.T) {
test.SetBoolForTest(t, &features.EnableProtocolSniffingForOutbound, true)
// The oldest service port is TCP. We should encounter conflicts when attempting to add the HTTP ports. Purposely
// storing the services out of time order to test that it's being sorted properly.
testOutboundListenerConflict(t,
buildService("test1.com", wildcardIP, "unknown", tnow.Add(1*time.Second)),
buildService("test2.com", wildcardIP, protocol.TCP, tnow),
buildService("test3.com", wildcardIP, "unknown", tnow.Add(2*time.Second)))
}
func TestOutboundListenerConflict_UnknownWithCurrentHTTP(t *testing.T) {
test.SetBoolForTest(t, &features.EnableProtocolSniffingForOutbound, true)
// The oldest service port is Auto. We should encounter conflicts when attempting to add the HTTP ports. Purposely
// storing the services out of time order to test that it's being sorted properly.
testOutboundListenerConflict(t,
buildService("test1.com", wildcardIP, "unknown", tnow.Add(1*time.Second)),
buildService("test2.com", wildcardIP, protocol.HTTP, tnow),
buildService("test3.com", wildcardIP, "unknown", tnow.Add(2*time.Second)))
}
func TestOutboundListenerRoute(t *testing.T) {
test.SetBoolForTest(t, &features.EnableProtocolSniffingForOutbound, true)
testOutboundListenerRoute(t,
buildService("test1.com", "1.2.3.4", "unknown", tnow.Add(1*time.Second)),
buildService("test2.com", "2.3.4.5", protocol.HTTP, tnow),
buildService("test3.com", "3.4.5.6", "unknown", tnow.Add(2*time.Second)))
}
func TestOutboundListenerConfig_WithSidecar(t *testing.T) {
// Add a service and verify it's config
services := []*model.Service{
buildService("test1.com", wildcardIP, protocol.HTTP, tnow.Add(1*time.Second)),
buildService("test2.com", wildcardIP, protocol.TCP, tnow),
buildService("test3.com", wildcardIP, "unknown", tnow.Add(2*time.Second)),
}
service4 := &model.Service{
CreationTime: tnow.Add(1 * time.Second),
Hostname: host.Name("test4.com"),
DefaultAddress: wildcardIP,
Ports: model.PortList{
&model.Port{
Name: "udp",
Port: 9000,
Protocol: protocol.GRPC,
},
},
Resolution: model.Passthrough,
Attributes: model.ServiceAttributes{
Namespace: "default",
},
}
services = append(services, service4)
service5 := &model.Service{
CreationTime: tnow.Add(1 * time.Second),
Hostname: host.Name("test5.com"),
DefaultAddress: "8.8.8.8",
Ports: model.PortList{
&model.Port{
Name: "MySQL",
Port: 3306,
Protocol: protocol.MySQL,
},
},
Resolution: model.Passthrough,
Attributes: model.ServiceAttributes{
Namespace: "default",
},
}
services = append(services, service5)
service6 := &model.Service{
CreationTime: tnow.Add(1 * time.Second),
Hostname: host.Name("test6.com"),
DefaultAddress: "2.2.2.2",
Ports: model.PortList{
&model.Port{
Name: "unknown",
Port: 8888,
Protocol: "unknown",
},
},
Resolution: model.Passthrough,
Attributes: model.ServiceAttributes{
Namespace: "default",
},
}
services = append(services, service6)
testOutboundListenerConfigWithSidecar(t, services...)
}
func TestOutboundListenerConflict_HTTPWithCurrentTCP(t *testing.T) {
// The oldest service port is TCP. We should encounter conflicts when attempting to add the HTTP ports. Purposely
// storing the services out of time order to test that it's being sorted properly.
testOutboundListenerConflictWithSniffingDisabled(t,
buildService("test1.com", wildcardIP, protocol.HTTP, tnow.Add(1*time.Second)),
buildService("test2.com", wildcardIP, protocol.TCP, tnow),
buildService("test3.com", wildcardIP, protocol.HTTP, tnow.Add(2*time.Second)))
}
func TestOutboundListenerConflict_TCPWithCurrentHTTP(t *testing.T) {
// The oldest service port is HTTP. We should encounter conflicts when attempting to add the TCP ports. Purposely
// storing the services out of time order to test that it's being sorted properly.
testOutboundListenerConflictWithSniffingDisabled(t,
buildService("test1.com", wildcardIP, protocol.TCP, tnow.Add(1*time.Second)),
buildService("test2.com", wildcardIP, protocol.HTTP, tnow),
buildService("test3.com", wildcardIP, protocol.TCP, tnow.Add(2*time.Second)))
}
func TestOutboundListenerConflict_TCPWithCurrentTCP(t *testing.T) {
services := []*model.Service{
buildService("test1.com", "1.2.3.4", protocol.TCP, tnow.Add(1*time.Second)),
buildService("test2.com", "1.2.3.4", protocol.TCP, tnow),
buildService("test3.com", "1.2.3.4", protocol.TCP, tnow.Add(2*time.Second)),
}
listeners := buildOutboundListeners(t, getProxy(), nil, nil, services...)
if len(listeners) != 1 {
t.Fatalf("expected %d listeners, found %d", 1, len(listeners))
}
// The filter chains should all be merged into one.
if len(listeners[0].FilterChains) != 1 {
t.Fatalf("expected %d filter chains, found %d", 1, len(listeners[0].FilterChains))
}
oldestService := getOldestService(services...)
oldestProtocol := oldestService.Ports[0].Protocol
if oldestProtocol != protocol.HTTP && isHTTPListener(listeners[0]) {
t.Fatal("expected TCP listener, found HTTP")
} else if oldestProtocol == protocol.HTTP && !isHTTPListener(listeners[0]) {
t.Fatal("expected HTTP listener, found TCP")
}
// Validate that listener conflict preserves the listener of oldest service.
verifyOutboundTCPListenerHostname(t, listeners[0], oldestService.Hostname)
}
func TestOutboundListenerTCPWithVS(t *testing.T) {
tests := []struct {
name string
CIDR string
expectedChains []string
}{
{
name: "same CIDR",
CIDR: "10.10.0.0/24",
expectedChains: []string{"10.10.0.0"},
},
{
name: "different CIDR",
CIDR: "10.10.10.0/24",
expectedChains: []string{"10.10.0.0", "10.10.10.0"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
services := []*model.Service{
buildService("test.com", tt.CIDR, protocol.TCP, tnow),
}
virtualService := config.Config{
Meta: config.Meta{
GroupVersionKind: collections.IstioNetworkingV1Alpha3Virtualservices.Resource().GroupVersionKind(),
Name: "test_vs",
Namespace: "default",
},
Spec: virtualServiceSpec,
}
listeners := buildOutboundListeners(t, getProxy(), nil, &virtualService, services...)
if len(listeners) != 1 {
t.Fatalf("expected %d listeners, found %d", 1, len(listeners))
}
var chains []string
for _, fc := range listeners[0].FilterChains {
for _, cidr := range fc.FilterChainMatch.PrefixRanges {
chains = append(chains, cidr.AddressPrefix)
}
}
// There should not be multiple filter chains with same CIDR match
if !reflect.DeepEqual(chains, tt.expectedChains) {
t.Fatalf("expected filter chains %v, found %v", tt.expectedChains, chains)
}
if listeners[0].ConnectionBalanceConfig != nil {
t.Fatalf("expected connection balance config to be set to empty, found %v", listeners[0].ConnectionBalanceConfig)
}
for _, l := range listeners {
for _, fc := range l.GetFilterChains() {
listenertest.VerifyFilterChain(t, fc, listenertest.FilterChainTest{
NetworkFilters: []string{wellknown.TCPProxy},
TotalMatch: true,
})
}
}
})
}
}
func TestOutboundListenerTCPWithVSExactBalance(t *testing.T) {
tests := []struct {
name string
CIDR string
expectedChains []string
}{
{
name: "same CIDR",
CIDR: "10.10.0.0/24",
expectedChains: []string{"10.10.0.0"},
},
{
name: "different CIDR",
CIDR: "10.10.10.0/24",
expectedChains: []string{"10.10.0.0", "10.10.10.0"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
services := []*model.Service{
buildService("test.com", tt.CIDR, protocol.TCP, tnow),
}
virtualService := config.Config{
Meta: config.Meta{
GroupVersionKind: collections.IstioNetworkingV1Alpha3Virtualservices.Resource().GroupVersionKind(),
Name: "test_vs",
Namespace: "default",
},
Spec: virtualServiceSpec,
}
proxy := getProxy()
proxy.Metadata.InboundListenerExactBalance = true
proxy.Metadata.OutboundListenerExactBalance = true
listeners := buildOutboundListeners(t, proxy, nil, &virtualService, services...)
if len(listeners) != 1 {
t.Fatalf("expected %d listeners, found %d", 1, len(listeners))
}
var chains []string
for _, fc := range listeners[0].FilterChains {
for _, cidr := range fc.FilterChainMatch.PrefixRanges {
chains = append(chains, cidr.AddressPrefix)
}
}
// There should not be multiple filter chains with same CIDR match
if !reflect.DeepEqual(chains, tt.expectedChains) {
t.Fatalf("expected filter chains %v, found %v", tt.expectedChains, chains)
}
if listeners[0].ConnectionBalanceConfig == nil || listeners[0].ConnectionBalanceConfig.GetExactBalance() == nil {
t.Fatalf("expected connection balance config to be set to exact_balance, found %v", listeners[0].ConnectionBalanceConfig)
}
})
}
}
func TestOutboundListenerForHeadlessServices(t *testing.T) {
svc := buildServiceWithPort("test.com", 9999, protocol.TCP, tnow)
svc.Resolution = model.Passthrough
svc.Attributes.ServiceRegistry = provider.Kubernetes
services := []*model.Service{svc}
autoSvc := buildServiceWithPort("test.com", 9999, protocol.Unsupported, tnow)
autoSvc.Resolution = model.Passthrough
autoSvc.Attributes.ServiceRegistry = provider.Kubernetes
extSvc := buildServiceWithPort("example1.com", 9999, protocol.TCP, tnow)
extSvc.Resolution = model.Passthrough
extSvc.Attributes.ServiceRegistry = provider.External
extSvcSelector := buildServiceWithPort("example2.com", 9999, protocol.TCP, tnow)
extSvcSelector.Resolution = model.Passthrough
extSvcSelector.Attributes.ServiceRegistry = provider.External
extSvcSelector.Attributes.LabelSelectors = map[string]string{"foo": "bar"}
tests := []struct {
name string
instances []*model.ServiceInstance
services []*model.Service
numListenersOnServicePort int
}{
{
name: "gen a listener per IP instance",
instances: []*model.ServiceInstance{
// This instance is the proxy itself, will not gen a outbound listener for it.
buildServiceInstance(services[0], "1.1.1.1"),
buildServiceInstance(services[0], "10.10.10.10"),
buildServiceInstance(services[0], "11.11.11.11"),
buildServiceInstance(services[0], "12.11.11.11"),
},
services: []*model.Service{svc},
numListenersOnServicePort: 3,
},
{
name: "no listeners for empty services",
instances: []*model.ServiceInstance{},
services: []*model.Service{svc},
numListenersOnServicePort: 0,
},
{
name: "no listeners for DNS instance",
instances: []*model.ServiceInstance{
buildServiceInstance([]*model.Service{svc}[0], "example.com"),
},
services: services,
numListenersOnServicePort: 0,
},
{
name: "external service",
instances: []*model.ServiceInstance{},
services: []*model.Service{extSvc},
numListenersOnServicePort: 1,
},
{
name: "external service with selector",
instances: []*model.ServiceInstance{},
services: []*model.Service{extSvcSelector},
numListenersOnServicePort: 0,
},
{
name: "external service with selector and endpoints",
instances: []*model.ServiceInstance{
buildServiceInstance(extSvcSelector, "10.10.10.10"),
buildServiceInstance(extSvcSelector, "11.11.11.11"),
},
services: []*model.Service{extSvcSelector},
numListenersOnServicePort: 2,
},
{
name: "no listeners for empty Kubernetes auto protocol",
instances: []*model.ServiceInstance{},
services: []*model.Service{autoSvc},
numListenersOnServicePort: 0,
},
{
name: "listeners per instance for Kubernetes auto protocol",
instances: []*model.ServiceInstance{
buildServiceInstance(autoSvc, "10.10.10.10"),
buildServiceInstance(autoSvc, "11.11.11.11"),
},
services: []*model.Service{autoSvc},
numListenersOnServicePort: 2,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cg := NewConfigGenTest(t, TestOptions{
Services: tt.services,
Instances: tt.instances,
})
proxy := cg.SetupProxy(nil)
proxy.Metadata.InboundListenerExactBalance = true
proxy.Metadata.OutboundListenerExactBalance = true
listeners := NewListenerBuilder(proxy, cg.env.PushContext).buildSidecarOutboundListeners(proxy, cg.env.PushContext)
listenersToCheck := make([]string, 0)
for _, l := range listeners {
if l.Address.GetSocketAddress().GetPortValue() == 9999 {
listenersToCheck = append(listenersToCheck, l.Name)
}
if l.ConnectionBalanceConfig == nil || l.ConnectionBalanceConfig.GetExactBalance() == nil {
t.Fatalf("expected connection balance config to be set to exact_balance, found %v", listeners[0].ConnectionBalanceConfig)
}
}
if len(listenersToCheck) != tt.numListenersOnServicePort {
t.Errorf("Expected %d listeners on service port 9999, got %d (%v)", tt.numListenersOnServicePort, len(listenersToCheck), listenersToCheck)
}
})
}
}
func TestInboundHTTPListenerConfig(t *testing.T) {
sidecarConfig := config.Config{
Meta: config.Meta{
Name: "foo",
Namespace: "not-default",
GroupVersionKind: gvk.Sidecar,
},
Spec: &networking.Sidecar{
Ingress: []*networking.IstioIngressListener{
{
Port: &networking.Port{
Number: 8080,
Protocol: "HTTP",
Name: "uds",
},
Bind: "1.1.1.1",
DefaultEndpoint: "127.0.0.1:80",
},
},
},
}
svc := buildService("test.com", wildcardIP, protocol.HTTP, tnow)
for _, p := range []*model.Proxy{getProxy(), &proxyHTTP10} {
cases := []struct {
name string
p *model.Proxy
cfg []config.Config
services []*model.Service
}{
{
name: "simple",
p: p,
services: []*model.Service{svc},
},
{
name: "sidecar with service",
p: p,
services: []*model.Service{svc},
cfg: []config.Config{sidecarConfig},
},
{
name: "sidecar",
p: p,
services: []*model.Service{svc},
cfg: []config.Config{sidecarConfig},
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
t.Helper()
listeners := buildListeners(t, TestOptions{
Services: tt.services,
Configs: tt.cfg,
}, tt.p)
l := xdstest.ExtractListener(model.VirtualInboundListenerName, listeners)
listenertest.VerifyListener(t, l, listenertest.ListenerTest{
FilterChains: []listenertest.FilterChainTest{
{
TotalMatch: true,
Port: 8080,
HTTPFilters: []string{xdsfilters.MxFilterName, xdsfilters.Fault.Name, xdsfilters.Cors.Name, xdsfilters.Router.Name},
ValidateHCM: func(t test.Failer, hcm *hcm.HttpConnectionManager) {
assert.Equal(t, "istio-envoy", hcm.GetServerName(), "server name")
if len(tt.cfg) == 0 {
assert.Equal(t, "inbound_0.0.0.0_8080", hcm.GetStatPrefix(), "stat prefix")
} else {
// Sidecar impacts stat prefix
assert.Equal(t, "inbound_1.1.1.1_8080", hcm.GetStatPrefix(), "stat prefix")
}
assert.Equal(t, "APPEND_FORWARD", hcm.GetForwardClientCertDetails().String(), "forward client cert details")
assert.Equal(t, true, hcm.GetSetCurrentClientCertDetails().GetSubject().GetValue(), "subject")
assert.Equal(t, true, hcm.GetSetCurrentClientCertDetails().GetDns(), "dns")
assert.Equal(t, true, hcm.GetSetCurrentClientCertDetails().GetUri(), "uri")
assert.Equal(t, true, hcm.GetNormalizePath().GetValue(), "normalize path")
assert.Equal(t, enableHTTP10(tt.p.Metadata.HTTP10), hcm.GetHttpProtocolOptions().GetAcceptHttp_10(), "http/1.0")
},
},
},
})
})
}
}
}
func TestOutboundListenerConfig_WithDisabledSniffing_WithSidecar(t *testing.T) {
test.SetBoolForTest(t, &features.EnableProtocolSniffingForOutbound, false)
// Add a service and verify it's config
services := []*model.Service{
buildService("test1.com", wildcardIP, protocol.HTTP, tnow.Add(1*time.Second)),
buildService("test2.com", wildcardIP, protocol.TCP, tnow),
buildService("test3.com", wildcardIP, protocol.HTTP, tnow.Add(2*time.Second)),
}
service4 := &model.Service{
CreationTime: tnow.Add(1 * time.Second),
Hostname: host.Name("test4.com"),
DefaultAddress: wildcardIP,
Ports: model.PortList{
&model.Port{
Name: "default",
Port: 9090,
Protocol: protocol.HTTP,
},
},
Resolution: model.Passthrough,
Attributes: model.ServiceAttributes{
Namespace: "default",
},
}
testOutboundListenerConfigWithSidecarWithSniffingDisabled(t, services...)
services = append(services, service4)
testOutboundListenerConfigWithSidecarWithCaptureModeNone(t, services...)
testOutboundListenerConfigWithSidecarWithUseRemoteAddress(t, services...)
}
func TestOutboundTlsTrafficWithoutTimeout(t *testing.T) {
services := []*model.Service{
{
CreationTime: tnow,
Hostname: host.Name("test.com"),
DefaultAddress: wildcardIP,
Ports: model.PortList{
&model.Port{
Name: "https",
Port: 8080,
Protocol: protocol.HTTPS,
},
},
Resolution: model.Passthrough,
Attributes: model.ServiceAttributes{
Namespace: "default",
},
},
{
CreationTime: tnow,
Hostname: host.Name("test1.com"),
DefaultAddress: wildcardIP,
Ports: model.PortList{
&model.Port{
Name: "foo",
Port: 9090,
Protocol: "unknown",
},
},
Resolution: model.Passthrough,
Attributes: model.ServiceAttributes{
Namespace: "default",
},
},
}
testOutboundListenerFilterTimeout(t, services...)
}
func TestOutboundTls(t *testing.T) {
services := []*model.Service{
{
CreationTime: tnow,
Hostname: host.Name("test.com"),
DefaultAddress: wildcardIP,
Ports: model.PortList{
&model.Port{
Name: "https",
Port: 8080,
Protocol: protocol.HTTPS,
},
},
Resolution: model.Passthrough,
Attributes: model.ServiceAttributes{
Namespace: "default",
},
},
}
virtualService := config.Config{
Meta: config.Meta{
GroupVersionKind: gvk.VirtualService,
Name: "test",
Namespace: "default",
},
Spec: &networking.VirtualService{
Hosts: []string{"test.com"},
Gateways: []string{"mesh"},
Tls: []*networking.TLSRoute{
{
Match: []*networking.TLSMatchAttributes{
{
DestinationSubnets: []string{"10.10.0.0/24", "11.10.0.0/24"},
Port: 8080,
SniHosts: []string{"a", "b", "c"},
},
},
Route: []*networking.RouteDestination{
{
Destination: &networking.Destination{
Host: "test.org",
Port: &networking.PortSelector{
Number: 80,
},
},
Weight: 100,
},
},
},
},
},
}
virtualService2 := config.Config{
Meta: config.Meta{
GroupVersionKind: gvk.VirtualService,
Name: "test2",
Namespace: "default",
},
Spec: &networking.VirtualService{
Hosts: []string{"test.com"},
Gateways: []string{"mesh"},
Tls: []*networking.TLSRoute{
{
Match: []*networking.TLSMatchAttributes{
{
DestinationSubnets: []string{"12.10.0.0/24", "13.10.0.0/24"},
Port: 8080,
SniHosts: []string{"e", "f", "g"},
},
},
Route: []*networking.RouteDestination{
{
Destination: &networking.Destination{
Host: "test.org",
Port: &networking.PortSelector{
Number: 80,
},
},
Weight: 100,
},
},
},
},
},
}
buildOutboundListeners(t, getProxy(), &virtualService2, &virtualService, services...)
}
func TestOutboundListenerConfigWithSidecarHTTPProxy(t *testing.T) {
sidecarConfig := &config.Config{
Meta: config.Meta{
Name: "sidecar-with-http-proxy",
Namespace: "not-default",
GroupVersionKind: gvk.Sidecar,
},
Spec: &networking.Sidecar{
Egress: []*networking.IstioEgressListener{
{
Hosts: []string{"default/*"},
Port: &networking.Port{
Number: 15080,
Protocol: "HTTP_PROXY",
Name: "15080",
},
Bind: "127.0.0.1",
CaptureMode: networking.CaptureMode_NONE,
},
},
},
}
services := []*model.Service{buildService("httpbin.com", wildcardIP, protocol.HTTP, tnow.Add(1*time.Second))}
listeners := buildOutboundListeners(t, getProxy(), sidecarConfig, nil, services...)
if expected := 1; len(listeners) != expected {
t.Fatalf("expected %d listeners, found %d", expected, len(listeners))
}
l := findListenerByPort(listeners, 15080)
if l == nil {
t.Fatalf("expected listener on port %d, but not found", 15080)
}
if len(l.FilterChains) != 1 {
t.Fatalf("expectd %d filter chains, found %d", 1, len(l.FilterChains))
} else {
if !isHTTPFilterChain(l.FilterChains[0]) {
t.Fatalf("expected http filter chain, found %s", l.FilterChains[1].Filters[0].Name)
}
if len(l.ListenerFilters) > 0 {
t.Fatalf("expected %d listener filter, found %d", 0, len(l.ListenerFilters))
}
}
}
func TestGetActualWildcardAndLocalHost(t *testing.T) {
tests := []struct {
name string
proxy *model.Proxy
expected [2]string
}{
{
name: "ipv4 only",
proxy: &model.Proxy{
IPAddresses: []string{"1.1.1.1", "127.0.0.1", "2.2.2.2"},
},
expected: [2]string{WildcardAddress, LocalhostAddress},
},
{
name: "ipv6 only",
proxy: &model.Proxy{
IPAddresses: []string{"1111:2222::1", "::1", "2222:3333::1"},
},
expected: [2]string{WildcardIPv6Address, LocalhostIPv6Address},
},
{
name: "mixed ipv4 and ipv6",
proxy: &model.Proxy{
IPAddresses: []string{"1111:2222::1", "::1", "127.0.0.1", "2.2.2.2", "2222:3333::1"},
},
expected: [2]string{WildcardAddress, LocalhostAddress},
},
}
for _, tt := range tests {
tt.proxy.DiscoverIPMode()
wm, lh := getActualWildcardAndLocalHost(tt.proxy)
if wm != tt.expected[0] && lh != tt.expected[1] {
t.Errorf("Test %s failed, expected: %s / %s got: %s / %s", tt.name, tt.expected[0], tt.expected[1], wm, lh)
}
}
}
// Test to catch new fields in FilterChainMatch message.
func TestFilterChainMatchFields(t *testing.T) {
fcm := listener.FilterChainMatch{}
e := reflect.ValueOf(&fcm).Elem()
// If this fails, that means new fields have been added to FilterChainMatch, filterChainMatchEqual function needs to be updated.
if e.NumField() != 14 {
t.Fatalf("Expected 14 fields, got %v. This means we need to update filterChainMatchEqual implementation", e.NumField())
}
}
func TestInboundListener_PrivilegedPorts(t *testing.T) {
// Verify that an explicit ingress listener will not bind to privileged ports
// if proxy is not using Iptables and cannot bind to privileged ports (1-1023).
//
// Even if a user explicitly created a Sidecar config with an ingress listener for a privileged port,
// it is still not worth it creating such a listener if we already known that a proxy will end up
// rejecting it.
testPrivilegedPorts(t, func(t *testing.T, proxy *model.Proxy, port uint32) []*listener.Listener {
// simulate user-defined Sidecar config with an ingress listener for a given port
sidecarConfig := config.Config{
Meta: config.Meta{
Name: "sidecar-with-ingress-listener",
Namespace: proxy.ConfigNamespace,
GroupVersionKind: gvk.Sidecar,
},
Spec: &networking.Sidecar{
Ingress: []*networking.IstioIngressListener{
{
Port: &networking.Port{
Number: port,
Protocol: "HTTP",
Name: strconv.Itoa(int(port)),
},
DefaultEndpoint: "127.0.0.1:8080",
},
},
},
}
return buildListeners(t, TestOptions{
Configs: []config.Config{sidecarConfig},
}, proxy)
})
}
func TestOutboundListener_PrivilegedPorts(t *testing.T) {
// Verify that an implicit catch all egress listener will not bind to privileged ports
// if proxy is not using Iptables and cannot bind to privileged ports (1-1023).
//
// It is very common for the catch all egress listener to match services on ports 80 and 443.
// Therefore, the default behavior should not force users to start from looking for a workaround.
t.Run("implicit catch all egress listener", func(t *testing.T) {
testPrivilegedPorts(t, func(t *testing.T, proxy *model.Proxy, port uint32) []*listener.Listener {
return buildListeners(t, TestOptions{
Services: []*model.Service{buildServiceWithPort("httpbin.com", int(port), protocol.HTTP, tnow)},
}, proxy)
})
})
// Verify that an explicit per-port egress listener will not bind to privileged ports
// if proxy is not using Iptables and cannot bind to privileged ports (1-1023).
//
// Even if a user explicitly created a Sidecar config with an egress listener for a privileged port,
// it is still not worth it creating such a listener if we already known that a proxy will end up
// rejecting it.
t.Run("explicit per-port egress listener", func(t *testing.T) {
testPrivilegedPorts(t, func(t *testing.T, proxy *model.Proxy, port uint32) []*listener.Listener {
// simulate user-defined Sidecar config with an egress listener for a given port
sidecarConfig := config.Config{
Meta: config.Meta{
Name: "sidecar-with-per-port-egress-listener",
Namespace: proxy.ConfigNamespace,
GroupVersionKind: gvk.Sidecar,
},
Spec: &networking.Sidecar{
Egress: []*networking.IstioEgressListener{
{
Hosts: []string{"default/*"},
Port: &networking.Port{
Number: port,
Protocol: "HTTP",
Name: strconv.Itoa(int(port)),
},
},
},
},
}
return buildListeners(t, TestOptions{
Services: []*model.Service{buildServiceWithPort("httpbin.com", int(port), protocol.HTTP, tnow)},
Configs: []config.Config{sidecarConfig},
}, proxy)
})
})
}
func testPrivilegedPorts(t *testing.T, buildListeners func(t *testing.T, proxy *model.Proxy, port uint32) []*listener.Listener) {
privilegedPorts := []uint32{1, 80, 443, 1023}
unprivilegedPorts := []uint32{1024, 8080, 8443, 15443}
anyPorts := append(privilegedPorts, unprivilegedPorts...)
// multiple test cases to ensure that privileged ports get treated differently
// only under certain conditions, namely
// 1) proxy explicitly indicated it is not using Iptables
// 2) proxy explicitly indicated it is not a privileged process (cannot bind to 1-1023)
cases := []struct {
name string
unprivileged bool // whether proxy is unprivileged
mode model.TrafficInterceptionMode
ports []uint32
expectListener bool // expect listener to be generated
}{
{
name: "privileged proxy; implicit REDIRECT mode; any ports",
unprivileged: false,
mode: "",
ports: anyPorts,
expectListener: true,
},
{
name: "privileged proxy; explicit REDIRECT mode; any ports",
unprivileged: false,
mode: model.InterceptionRedirect,
ports: anyPorts,
expectListener: true,
},
{
name: "privileged proxy; explicit TPROXY mode; any ports",
unprivileged: false,
mode: model.InterceptionTproxy,
ports: anyPorts,
expectListener: true,
},
{
name: "privileged proxy; explicit NONE mode; any ports",
unprivileged: false,
mode: model.InterceptionNone,
ports: anyPorts,
expectListener: true,
},
{
name: "unprivileged proxy; implicit REDIRECT mode; any ports",
unprivileged: true,
mode: "",
ports: anyPorts,
expectListener: true,
},
{
name: "unprivileged proxy; explicit REDIRECT mode; any ports",
unprivileged: true,
mode: model.InterceptionRedirect,
ports: anyPorts,
expectListener: true,
},
{
name: "unprivileged proxy; explicit TPROXY mode; any ports",
unprivileged: true,
mode: model.InterceptionTproxy,
ports: anyPorts,
expectListener: true,
},
{
name: "unprivileged proxy; explicit NONE mode; privileged ports",
unprivileged: true,
mode: model.InterceptionNone,
ports: privilegedPorts,
expectListener: false,
},
{
name: "unprivileged proxy; explicit NONE mode; unprivileged ports",
unprivileged: true,
mode: model.InterceptionNone,
ports: unprivilegedPorts,
expectListener: true,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
for _, port := range tc.ports {
t.Run(strconv.Itoa(int(port)), func(t *testing.T) {
proxy := getProxy()
proxy.Metadata.UnprivilegedPod = strconv.FormatBool(tc.unprivileged)
proxy.Metadata.InterceptionMode = tc.mode
listeners := buildListeners(t, proxy, port)
found := hasListenerOrFilterChainForPort(listeners, port)
if tc.expectListener {
if !found {
t.Fatalf("expected listener on port %d, but not found", port)
}
} else {
if found {
t.Fatalf("expected no listener on port %d, but found found one", port)
}
}
})
}
})
}
}
func testOutboundListenerConflictWithSniffingDisabled(t *testing.T, services ...*model.Service) {
t.Helper()
test.SetBoolForTest(t, &features.EnableProtocolSniffingForOutbound, false)
oldestService := getOldestService(services...)
listeners := buildOutboundListeners(t, getProxy(), nil, nil, services...)
if len(listeners) != 1 {
t.Fatalf("expected %d listeners, found %d", 1, len(listeners))
}
oldestProtocol := oldestService.Ports[0].Protocol
if oldestProtocol != protocol.HTTP && isHTTPListener(listeners[0]) {
t.Fatal("expected TCP listener, found HTTP")
} else if oldestProtocol == protocol.HTTP && !isHTTPListener(listeners[0]) {
t.Fatal("expected HTTP listener, found TCP")
}
}
func testOutboundListenerRoute(t *testing.T, services ...*model.Service) {
t.Helper()
listeners := buildOutboundListeners(t, getProxy(), nil, nil, services...)
if len(listeners) != 3 {
t.Fatalf("expected %d listeners, found %d", 3, len(listeners))
}
l := findListenerByAddress(listeners, wildcardIP)
if l == nil {
t.Fatalf("expect listener %s", "0.0.0.0_8080")
}
f := l.FilterChains[0].Filters[0]
cfg, _ := conversion.MessageToStruct(f.GetTypedConfig())
rds := cfg.Fields["rds"].GetStructValue().Fields["route_config_name"].GetStringValue()
if rds != "8080" {
t.Fatalf("expect routes %s, found %s", "8080", rds)
}
l = findListenerByAddress(listeners, "1.2.3.4")
if l == nil {
t.Fatalf("expect listener %s", "1.2.3.4_8080")
}
f = l.FilterChains[0].Filters[0]
cfg, _ = conversion.MessageToStruct(f.GetTypedConfig())
rds = cfg.Fields["rds"].GetStructValue().Fields["route_config_name"].GetStringValue()
if rds != "test1.com:8080" {
t.Fatalf("expect routes %s, found %s", "test1.com:8080", rds)
}
l = findListenerByAddress(listeners, "3.4.5.6")
if l == nil {
t.Fatalf("expect listener %s", "3.4.5.6_8080")
}
f = l.FilterChains[0].Filters[0]
cfg, _ = conversion.MessageToStruct(f.GetTypedConfig())
rds = cfg.Fields["rds"].GetStructValue().Fields["route_config_name"].GetStringValue()
if rds != "test3.com:8080" {
t.Fatalf("expect routes %s, found %s", "test3.com:8080", rds)
}
}
func testOutboundListenerFilterTimeout(t *testing.T, services ...*model.Service) {
listeners := buildOutboundListeners(t, getProxy(), nil, nil, services...)
if len(listeners) != 2 {
t.Fatalf("expected %d listeners, found %d", 2, len(listeners))
}
if listeners[0].ContinueOnListenerFiltersTimeout {
t.Fatalf("expected timeout disabled, found ContinueOnListenerFiltersTimeout %v",
listeners[0].ContinueOnListenerFiltersTimeout)
}
if !listeners[1].ContinueOnListenerFiltersTimeout || listeners[1].ListenerFiltersTimeout == nil {
t.Fatalf("expected timeout enabled, found ContinueOnListenerFiltersTimeout %v, ListenerFiltersTimeout %v",
listeners[1].ContinueOnListenerFiltersTimeout,
listeners[1].ListenerFiltersTimeout)
}
}
func testOutboundListenerConflict(t *testing.T, services ...*model.Service) {
oldestService := getOldestService(services...)
proxy := getProxy()
proxy.DiscoverIPMode()
listeners := buildOutboundListeners(t, getProxy(), nil, nil, services...)
if len(listeners) != 1 {
t.Fatalf("expected %d listeners, found %d", 1, len(listeners))
}
oldestProtocol := oldestService.Ports[0].Protocol
if oldestProtocol == protocol.MySQL {
if len(listeners[0].FilterChains) != 1 {
t.Fatalf("expected %d filter chains, found %d", 1, len(listeners[0].FilterChains))
} else if !isTCPFilterChain(listeners[0].FilterChains[0]) {
t.Fatalf("expected tcp filter chain, found %s", listeners[0].FilterChains[1].Filters[0].Name)
}
} else if oldestProtocol != protocol.HTTP && oldestProtocol != protocol.TCP {
if len(listeners[0].FilterChains) != 1 {
t.Fatalf("expectd %d filter chains, found %d", 1, len(listeners[0].FilterChains))
}
if !isHTTPFilterChain(listeners[0].FilterChains[0]) {
t.Fatalf("expected http filter chain, found %s", listeners[0].FilterChains[0].Filters[0].Name)
}
if !isTCPFilterChain(listeners[0].DefaultFilterChain) {
t.Fatalf("expected tcp filter chain, found %s", listeners[0].DefaultFilterChain.Filters[0].Name)
}
verifyHTTPFilterChainMatch(t, listeners[0].FilterChains[0])
verifyListenerFilters(t, listeners[0].ListenerFilters)
if !listeners[0].ContinueOnListenerFiltersTimeout || listeners[0].ListenerFiltersTimeout == nil {
t.Fatalf("exptected timeout, found ContinueOnListenerFiltersTimeout %v, ListenerFiltersTimeout %v",
listeners[0].ContinueOnListenerFiltersTimeout,
listeners[0].ListenerFiltersTimeout)
}
f := listeners[0].FilterChains[0].Filters[0]
cfg, _ := conversion.MessageToStruct(f.GetTypedConfig())
rds := cfg.Fields["rds"].GetStructValue().Fields["route_config_name"].GetStringValue()
expect := fmt.Sprintf("%d", oldestService.Ports[0].Port)
if rds != expect {
t.Fatalf("expect routes %s, found %s", expect, rds)
}
} else {
if len(listeners[0].FilterChains) != 1 {
t.Fatalf("expectd %d filter chains, found %d", 1, len(listeners[0].FilterChains))
}
if listeners[0].DefaultFilterChain == nil {
t.Fatalf("expected default filter chains, found none")
}
_ = getTCPFilterChain(t, listeners[0])
http := getHTTPFilterChain(t, listeners[0])
verifyHTTPFilterChainMatch(t, http)
verifyListenerFilters(t, listeners[0].ListenerFilters)
if !listeners[0].ContinueOnListenerFiltersTimeout || listeners[0].ListenerFiltersTimeout == nil {
t.Fatalf("exptected timeout, found ContinueOnListenerFiltersTimeout %v, ListenerFiltersTimeout %v",
listeners[0].ContinueOnListenerFiltersTimeout,
listeners[0].ListenerFiltersTimeout)
}
}
}
func getFilterChains(l *listener.Listener) []*listener.FilterChain {
res := l.FilterChains
if l.DefaultFilterChain != nil {
res = append(res, l.DefaultFilterChain)
}
return res
}
func getTCPFilterChain(t *testing.T, l *listener.Listener) *listener.FilterChain {
t.Helper()
for _, fc := range getFilterChains(l) {
for _, f := range fc.Filters {
if f.Name == wellknown.TCPProxy {
return fc
}
}
}
t.Fatalf("tcp filter chain not found")
return nil
}
func getTCPFilter(fc *listener.FilterChain) *listener.Filter {
for _, f := range fc.Filters {
if f.Name == wellknown.TCPProxy {
return f
}
}
return nil
}
func getHTTPFilter(fc *listener.FilterChain) *listener.Filter {
for _, f := range fc.Filters {
if f.Name == wellknown.HTTPConnectionManager {
return f
}
}
return nil
}
func getHTTPFilterChain(t *testing.T, l *listener.Listener) *listener.FilterChain {
t.Helper()
for _, fc := range getFilterChains(l) {
for _, f := range fc.Filters {
if f.Name == wellknown.HTTPConnectionManager {
return fc
}
}
}
t.Fatalf("tcp filter chain not found")
return nil
}
func testInboundListenerConfig(t *testing.T, proxy *model.Proxy, services ...*model.Service) {
t.Helper()
listeners := buildListeners(t, TestOptions{Services: services}, proxy)
verifyFilterChainMatch(t, xdstest.ExtractListener(model.VirtualInboundListenerName, listeners))
}
func testInboundListenerConfigWithGrpc(t *testing.T, proxy *model.Proxy, services ...*model.Service) {
t.Helper()
listeners := buildListeners(t, TestOptions{Services: services}, proxy)
l := xdstest.ExtractListener(model.VirtualInboundListenerName, listeners)
listenertest.VerifyListener(t, l, listenertest.ListenerTest{
FilterChains: []listenertest.FilterChainTest{
{
Port: 8080,
HTTPFilters: []string{wellknown.HTTPGRPCStats},
},
},
})
}
func testInboundListenerConfigWithSidecar(t *testing.T, proxy *model.Proxy, services ...*model.Service) {
t.Helper()
sidecarConfig := config.Config{
Meta: config.Meta{
Name: "foo",
Namespace: "not-default",
GroupVersionKind: gvk.Sidecar,
},
Spec: &networking.Sidecar{
Ingress: []*networking.IstioIngressListener{
{
Port: &networking.Port{
Number: 8080,
Protocol: "unknown",
Name: "uds",
},
Bind: "1.1.1.1",
DefaultEndpoint: "127.0.0.1:80",
},
},
},
}
listeners := buildListeners(t, TestOptions{
Services: services,
Configs: []config.Config{sidecarConfig},
}, proxy)
l := xdstest.ExtractListener(model.VirtualInboundListenerName, listeners)
verifyFilterChainMatch(t, l)
}
func testInboundListenerConfigWithSidecarWithoutServices(t *testing.T, proxy *model.Proxy) {
t.Helper()
sidecarConfig := config.Config{
Meta: config.Meta{
Name: "foo-without-service",
Namespace: "not-default",
GroupVersionKind: gvk.Sidecar,
},
Spec: &networking.Sidecar{
Ingress: []*networking.IstioIngressListener{
{
Port: &networking.Port{
Number: 8080,
Protocol: "unknown",
Name: "uds",
},
Bind: "1.1.1.1",
DefaultEndpoint: "127.0.0.1:80",
},
},
},
}
listeners := buildListeners(t, TestOptions{
Configs: []config.Config{sidecarConfig},
}, proxy)
l := xdstest.ExtractListener(model.VirtualInboundListenerName, listeners)
verifyFilterChainMatch(t, l)
}
func testInboundListenerConfigWithoutService(t *testing.T, proxy *model.Proxy) {
t.Helper()
listeners := buildListeners(t, TestOptions{}, proxy)
l := xdstest.ExtractListener(model.VirtualInboundListenerName, listeners)
verifyFilterChainMatch(t, l)
}
func verifyListenerFilters(t *testing.T, lfilters []*listener.ListenerFilter) {
t.Helper()
if len(lfilters) != 2 {
t.Fatalf("expected %d listener filter, found %d", 2, len(lfilters))
}
if lfilters[0].Name != wellknown.TlsInspector ||
lfilters[1].Name != wellknown.HttpInspector {
t.Fatalf("expected listener filters not found, got %v", lfilters)
}
}
func verifyHTTPFilterChainMatch(t *testing.T, fc *listener.FilterChain) {
t.Helper()
if fc.FilterChainMatch.TransportProtocol != xdsfilters.RawBufferTransportProtocol {
t.Fatalf("exepct %q transport protocol, found %q", xdsfilters.RawBufferTransportProtocol, fc.FilterChainMatch.TransportProtocol)
}
if !reflect.DeepEqual(plaintextHTTPALPNs, fc.FilterChainMatch.ApplicationProtocols) {
t.Fatalf("expected %d application protocols, %v got %v",
len(plaintextHTTPALPNs), plaintextHTTPALPNs, fc.FilterChainMatch.ApplicationProtocols)
}
hcm := &hcm.HttpConnectionManager{}
if err := getFilterConfig(getHTTPFilter(fc), hcm); err != nil {
t.Fatalf("failed to get HCM, config %v", hcm)
}
hasAlpn := hasAlpnFilter(hcm.HttpFilters)
if !hasAlpn {
t.Fatal("ALPN filter is not found")
}
}
func hasAlpnFilter(filters []*hcm.HttpFilter) bool {
for _, f := range filters {
if f.Name == xdsfilters.AlpnFilterName {
return true
}
}
return false
}
func isHTTPFilterChain(fc *listener.FilterChain) bool {
return getHTTPFilter(fc) != nil
}
func isTCPFilterChain(fc *listener.FilterChain) bool {
return getTCPFilter(fc) != nil
}
func testOutboundListenerConfigWithSidecar(t *testing.T, services ...*model.Service) {
t.Helper()
sidecarConfig := &config.Config{
Meta: config.Meta{
Name: "foo",
Namespace: "not-default",
GroupVersionKind: gvk.Sidecar,
},
Spec: &networking.Sidecar{
Egress: []*networking.IstioEgressListener{
{
Port: &networking.Port{
Number: 9000,
Protocol: "GRPC",
Name: "uds",
},
Hosts: []string{"*/*"},
},
{
Port: &networking.Port{
Number: 3306,
Protocol: string(protocol.MySQL),
Name: "MySQL",
},
Bind: "8.8.8.8",
Hosts: []string{"*/*"},
},
{
Port: &networking.Port{
Number: 8888,
Protocol: "unknown",
Name: "unknown",
},
Bind: "2.2.2.2",
Hosts: []string{"*/*"},
},
{
Hosts: []string{"*/*"},
},
},
},
}
// enable mysql filter that is used here
test.SetBoolForTest(t, &features.EnableMysqlFilter, true)
listeners := buildOutboundListeners(t, getProxy(), sidecarConfig, nil, services...)
if len(listeners) != 4 {
t.Fatalf("expected %d listeners, found %d", 4, len(listeners))
}
l := findListenerByPort(listeners, 8080)
if len(l.FilterChains) != 1 {
t.Fatalf("expectd %d filter chains, found %d", 1, len(l.FilterChains))
}
if !isHTTPFilterChain(l.FilterChains[0]) {
t.Fatalf("expected http filter chain, found %s", l.FilterChains[0].Filters[0].Name)
}
if !isTCPFilterChain(l.DefaultFilterChain) {
t.Fatalf("expected tcp filter chain, found %s", l.DefaultFilterChain.Filters[0].Name)
}
verifyHTTPFilterChainMatch(t, l.FilterChains[0])
verifyListenerFilters(t, l.ListenerFilters)
if l := findListenerByPort(listeners, 3306); !isMysqlListener(l) {
t.Fatalf("expected MySQL listener on port 3306, found %v", l)
}
if l := findListenerByPort(listeners, 9000); !isHTTPListener(l) {
t.Fatalf("expected HTTP listener on port 9000, found TCP\n%v", l)
}
l = findListenerByPort(listeners, 8888)
if len(l.FilterChains) != 1 {
t.Fatalf("expected %d filter chains, found %d", 1, len(l.FilterChains))
}
if !isHTTPFilterChain(l.FilterChains[0]) {
t.Fatalf("expected http filter chain, found %s", l.FilterChains[0].Filters[0].Name)
}
if !isTCPFilterChain(l.DefaultFilterChain) {
t.Fatalf("expected tcp filter chain, found %s", l.DefaultFilterChain.Filters[0].Name)
}
verifyHTTPFilterChainMatch(t, l.FilterChains[0])
verifyListenerFilters(t, l.ListenerFilters)
}
func testOutboundListenerConfigWithSidecarWithSniffingDisabled(t *testing.T, services ...*model.Service) {
t.Helper()
sidecarConfig := &config.Config{
Meta: config.Meta{
Name: "foo",
Namespace: "not-default",
GroupVersionKind: gvk.Sidecar,
},
Spec: &networking.Sidecar{
Egress: []*networking.IstioEgressListener{
{
Port: &networking.Port{
Number: 9000,
Protocol: "HTTP",
Name: "uds",
},
Bind: "1.1.1.1",
Hosts: []string{"*/*"},
},
{
Port: &networking.Port{
Number: 3306,
Protocol: string(protocol.MySQL),
Name: "MySQL",
},
Bind: "8.8.8.8",
Hosts: []string{"*/*"},
},
{
Hosts: []string{"*/*"},
},
},
},
}
// enable mysql filter that is used here
test.SetBoolForTest(t, &features.EnableMysqlFilter, true)
listeners := buildOutboundListeners(t, getProxy(), sidecarConfig, nil, services...)
if len(listeners) != 1 {
t.Fatalf("expected %d listeners, found %d", 1, len(listeners))
}
if l := findListenerByPort(listeners, 8080); isHTTPListener(l) {
t.Fatalf("expected TCP listener on port 8080, found HTTP: %v", l)
}
}
func testOutboundListenerConfigWithSidecarWithUseRemoteAddress(t *testing.T, services ...*model.Service) {
t.Helper()
sidecarConfig := &config.Config{
Meta: config.Meta{
Name: "foo",
Namespace: "not-default",
GroupVersionKind: gvk.Sidecar,
},
Spec: &networking.Sidecar{
Egress: []*networking.IstioEgressListener{
{
Port: &networking.Port{
Number: 9090,
Protocol: "HTTP",
Name: "uds",
},
Bind: "1.1.1.1",
Hosts: []string{"*/*"},
},
},
},
}
// enable use remote address to true
test.SetBoolForTest(t, &features.UseRemoteAddress, true)
listeners := buildOutboundListeners(t, getProxy(), sidecarConfig, nil, services...)
if l := findListenerByPort(listeners, 9090); !isHTTPListener(l) {
t.Fatalf("expected HTTP listener on port 9090, found TCP\n%v", l)
} else {
f := l.FilterChains[0].Filters[0]
cfg, _ := conversion.MessageToStruct(f.GetTypedConfig())
if useRemoteAddress, exists := cfg.Fields["use_remote_address"]; exists {
if !exists || !useRemoteAddress.GetBoolValue() {
t.Fatalf("expected useRemoteAddress true, found false %v", l)
}
}
}
}
func testOutboundListenerConfigWithSidecarWithCaptureModeNone(t *testing.T, services ...*model.Service) {
t.Helper()
sidecarConfig := &config.Config{
Meta: config.Meta{
Name: "foo",
Namespace: "not-default",
GroupVersionKind: gvk.Sidecar,
},
Spec: &networking.Sidecar{
Egress: []*networking.IstioEgressListener{
{
// Bind + Port
CaptureMode: networking.CaptureMode_NONE,
Port: &networking.Port{
Number: 9000,
Protocol: "HTTP",
Name: "grpc",
},
Bind: "127.1.1.2",
Hosts: []string{"*/*"},
},
{
// Bind Only
CaptureMode: networking.CaptureMode_NONE,
Bind: "127.1.1.2",
Hosts: []string{"*/*"},
},
{
// Port Only
CaptureMode: networking.CaptureMode_NONE,
Port: &networking.Port{
Number: 9000,
Protocol: "HTTP",
Name: "grpc",
},
Hosts: []string{"*/*"},
},
{
// None
CaptureMode: networking.CaptureMode_NONE,
Hosts: []string{"*/*"},
},
},
},
}
listeners := buildOutboundListeners(t, getProxy(), sidecarConfig, nil, services...)
if len(listeners) != 4 {
t.Fatalf("expected %d listeners, found %d", 4, len(listeners))
}
expectedListeners := map[string]string{
"127.1.1.2_9090": "HTTP",
"127.1.1.2_8080": "TCP",
"127.0.0.1_9090": "HTTP",
"127.0.0.1_8080": "TCP",
}
for _, l := range listeners {
listenerName := l.Name
expectedListenerType := expectedListeners[listenerName]
if expectedListenerType == "" {
t.Fatalf("listener %s not expected", listenerName)
}
if expectedListenerType == "TCP" && isHTTPListener(l) {
t.Fatalf("expected TCP listener %s, but found HTTP", listenerName)
}
if expectedListenerType == "HTTP" && !isHTTPListener(l) {
t.Fatalf("expected HTTP listener %s, but found TCP", listenerName)
}
if l.ConnectionBalanceConfig != nil {
t.Fatalf("expected connection balance config to be nil, found %v", l.ConnectionBalanceConfig)
}
}
if l := findListenerByPort(listeners, 9090); !isHTTPListener(l) {
t.Fatalf("expected HTTP listener on port 9090, but not found\n%v", l)
} else {
f := l.FilterChains[0].Filters[0]
cfg, _ := conversion.MessageToStruct(f.GetTypedConfig())
if useRemoteAddress, exists := cfg.Fields["use_remote_address"]; exists {
if exists && useRemoteAddress.GetBoolValue() {
t.Fatalf("expected useRemoteAddress false, found true %v", l)
}
}
}
}
func TestVirtualListeners_TrafficRedirectionEnabled(t *testing.T) {
cases := []struct {
name string
mode model.TrafficInterceptionMode
}{
{
name: "empty value",
mode: "",
},
{
name: "unknown value",
mode: model.TrafficInterceptionMode("UNKNOWN_VALUE"),
},
{
name: string(model.InterceptionTproxy),
mode: model.InterceptionTproxy,
},
{
name: string(model.InterceptionRedirect),
mode: model.InterceptionRedirect,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
listeners := buildListeners(t, TestOptions{}, &model.Proxy{Metadata: &model.NodeMetadata{InterceptionMode: tc.mode}})
if l := xdstest.ExtractListener(model.VirtualInboundListenerName, listeners); l == nil {
t.Fatalf("did not generate virtual inbound listener")
}
if l := xdstest.ExtractListener(model.VirtualOutboundListenerName, listeners); l == nil {
t.Fatalf("did not generate virtual outbound listener")
}
})
}
}
func TestVirtualListeners_TrafficRedirectionDisabled(t *testing.T) {
listeners := buildListeners(t, TestOptions{}, &model.Proxy{Metadata: &model.NodeMetadata{InterceptionMode: model.InterceptionNone}})
if l := xdstest.ExtractListener(model.VirtualInboundListenerName, listeners); l != nil {
t.Fatalf("unexpectedly generated virtual inbound listener")
}
if l := xdstest.ExtractListener(model.VirtualOutboundListenerName, listeners); l != nil {
t.Fatalf("unexpectedly generated virtual outbound listener")
}
}
func TestOutboundListenerAccessLogs(t *testing.T) {
m := mesh.DefaultMeshConfig()
m.AccessLogFile = "foo"
listeners := buildListeners(t, TestOptions{MeshConfig: m}, nil)
validateAccessLog(t, xdstest.ExtractListener(model.VirtualOutboundListenerName, listeners), "")
// Update MeshConfig
m.AccessLogFormat = "format modified"
// Trigger MeshConfig change and validate that access log is recomputed.
accessLogBuilder.reset()
listeners = buildListeners(t, TestOptions{MeshConfig: m}, nil)
// Validate that access log filter uses the new format.
validateAccessLog(t, xdstest.ExtractListener(model.VirtualOutboundListenerName, listeners), "format modified")
}
func TestListenerAccessLogs(t *testing.T) {
t.Helper()
m := mesh.DefaultMeshConfig()
m.AccessLogFile = "foo"
listeners := buildListeners(t, TestOptions{MeshConfig: m}, nil)
for _, l := range listeners {
if l.AccessLog == nil {
t.Fatalf("expected access log configuration for %v", l)
}
if l.AccessLog[0].Filter == nil {
t.Fatal("expected filter config in listener access log configuration")
}
}
}
func validateAccessLog(t *testing.T, l *listener.Listener, format string) {
t.Helper()
if l == nil {
t.Fatalf("nil listener")
}
fc := &tcp.TcpProxy{}
if err := getFilterConfig(xdstest.ExtractFilterChain("virtualOutbound-catchall-tcp", l).Filters[0], fc); err != nil {
t.Fatalf("failed to get TCP Proxy config: %s", err)
}
if fc.AccessLog == nil {
t.Fatal("expected access log configuration")
}
cfg, _ := conversion.MessageToStruct(fc.AccessLog[0].GetTypedConfig())
textFormat := cfg.GetFields()["log_format"].GetStructValue().GetFields()["text_format_source"].GetStructValue().
GetFields()["inline_string"].GetStringValue()
if format != "" && textFormat != format {
t.Fatalf("expected format to be %s, but got %s", format, textFormat)
}
}
func TestHttpProxyListener(t *testing.T) {
m := mesh.DefaultMeshConfig()
m.ProxyHttpPort = 15007
listeners := buildListeners(t, TestOptions{MeshConfig: m}, nil)
httpProxy := xdstest.ExtractListener("127.0.0.1_15007", listeners)
f := httpProxy.FilterChains[0].Filters[0]
cfg, _ := conversion.MessageToStruct(f.GetTypedConfig())
if httpProxy.Address.GetSocketAddress().GetPortValue() != 15007 {
t.Fatalf("expected http proxy is not listening on %d, but on port %d", 15007,
httpProxy.Address.GetSocketAddress().GetPortValue())
}
if !strings.HasPrefix(cfg.Fields["stat_prefix"].GetStringValue(), "outbound_") {
t.Fatalf("expected http proxy stat prefix to have outbound, %s", cfg.Fields["stat_prefix"].GetStringValue())
}
}
func TestHttpProxyListenerPerWorkload(t *testing.T) {
listeners := buildListeners(t, TestOptions{}, &model.Proxy{Metadata: &model.NodeMetadata{HTTPProxyPort: "15007"}})
httpProxy := xdstest.ExtractListener("127.0.0.1_15007", listeners)
f := httpProxy.FilterChains[0].Filters[0]
cfg, _ := conversion.MessageToStruct(f.GetTypedConfig())
if httpProxy.Address.GetSocketAddress().GetPortValue() != 15007 {
t.Fatalf("expected http proxy is not listening on %d, but on port %d", 15007,
httpProxy.Address.GetSocketAddress().GetPortValue())
}
if !strings.HasPrefix(cfg.Fields["stat_prefix"].GetStringValue(), "outbound_") {
t.Fatalf("expected http proxy stat prefix to have outbound, %s", cfg.Fields["stat_prefix"].GetStringValue())
}
}
func TestHttpProxyListener_Tracing(t *testing.T) {
customTagsTest := []struct {
name string
in *meshconfig.Tracing
out *hcm.HttpConnectionManager_Tracing
tproxy *model.Proxy
envPilotSampling float64
disableIstioTags bool
}{
{
name: "random-sampling-env",
tproxy: getProxy(),
envPilotSampling: 80.0,
in: &meshconfig.Tracing{
Tracer: nil,
CustomTags: nil,
MaxPathTagLength: 0,
Sampling: 0,
},
out: &hcm.HttpConnectionManager_Tracing{
MaxPathTagLength: nil,
ClientSampling: &xdstype.Percent{
Value: 100.0,
},
RandomSampling: &xdstype.Percent{
Value: 80.0,
},
OverallSampling: &xdstype.Percent{
Value: 100.0,
},
CustomTags: customTracingTags(),
},
},
{
name: "random-sampling-env-without-istio-tags",
disableIstioTags: true,
tproxy: getProxy(),
envPilotSampling: 80.0,
in: &meshconfig.Tracing{
Tracer: nil,
CustomTags: nil,
MaxPathTagLength: 0,
Sampling: 0,
},
out: &hcm.HttpConnectionManager_Tracing{
MaxPathTagLength: nil,
ClientSampling: &xdstype.Percent{
Value: 100.0,
},
RandomSampling: &xdstype.Percent{
Value: 80.0,
},
OverallSampling: &xdstype.Percent{
Value: 100.0,
},
},
},
{
name: "random-sampling-env-and-meshconfig",
tproxy: getProxy(),
envPilotSampling: 80.0,
in: &meshconfig.Tracing{
Tracer: nil,
CustomTags: nil,
MaxPathTagLength: 0,
Sampling: 10,
},
out: &hcm.HttpConnectionManager_Tracing{
MaxPathTagLength: nil,
ClientSampling: &xdstype.Percent{
Value: 100.0,
},
RandomSampling: &xdstype.Percent{
Value: 10.0,
},
OverallSampling: &xdstype.Percent{
Value: 100.0,
},
CustomTags: customTracingTags(),
},
},
{
name: "random-sampling-too-low-env",
tproxy: getProxy(),
envPilotSampling: -1,
in: &meshconfig.Tracing{
Tracer: nil,
CustomTags: nil,
MaxPathTagLength: 0,
Sampling: 300,
},
out: &hcm.HttpConnectionManager_Tracing{
MaxPathTagLength: nil,
ClientSampling: &xdstype.Percent{
Value: 100.0,
},
RandomSampling: &xdstype.Percent{
Value: 1.0,
},
OverallSampling: &xdstype.Percent{
Value: 100.0,
},
CustomTags: customTracingTags(),
},
},
{
name: "random-sampling-too-high-meshconfig",
tproxy: getProxy(),
envPilotSampling: 80.0,
in: &meshconfig.Tracing{
Tracer: nil,
CustomTags: nil,
MaxPathTagLength: 0,
Sampling: 300,
},
out: &hcm.HttpConnectionManager_Tracing{
MaxPathTagLength: nil,
ClientSampling: &xdstype.Percent{
Value: 100.0,
},
RandomSampling: &xdstype.Percent{
Value: 1.0,
},
OverallSampling: &xdstype.Percent{
Value: 100.0,
},
CustomTags: customTracingTags(),
},
},
{
name: "random-sampling-too-high-env",
tproxy: getProxy(),
envPilotSampling: 2000.0,
in: &meshconfig.Tracing{
Tracer: nil,
CustomTags: nil,
MaxPathTagLength: 0,
Sampling: 300,
},
out: &hcm.HttpConnectionManager_Tracing{
MaxPathTagLength: nil,
ClientSampling: &xdstype.Percent{
Value: 100.0,
},
RandomSampling: &xdstype.Percent{
Value: 1.0,
},
OverallSampling: &xdstype.Percent{
Value: 100.0,
},
CustomTags: customTracingTags(),
},
},
{
// upstream will set the default to 256 per
// its documentation
name: "tag-max-path-length-not-set-default",
tproxy: getProxy(),
in: &meshconfig.Tracing{
Tracer: nil,
CustomTags: nil,
MaxPathTagLength: 0,
Sampling: 0,
},
out: &hcm.HttpConnectionManager_Tracing{
MaxPathTagLength: nil,
ClientSampling: &xdstype.Percent{
Value: 100.0,
},
RandomSampling: &xdstype.Percent{
Value: 1.0,
},
OverallSampling: &xdstype.Percent{
Value: 100.0,
},
CustomTags: customTracingTags(),
},
},
{
name: "tag-max-path-length-set-to-1024",
tproxy: getProxy(),
in: &meshconfig.Tracing{
Tracer: nil,
CustomTags: nil,
MaxPathTagLength: 1024,
Sampling: 0,
},
out: &hcm.HttpConnectionManager_Tracing{
MaxPathTagLength: &wrappers.UInt32Value{
Value: 1024,
},
ClientSampling: &xdstype.Percent{
Value: 100.0,
},
RandomSampling: &xdstype.Percent{
Value: 1.0,
},
OverallSampling: &xdstype.Percent{
Value: 100.0,
},
CustomTags: customTracingTags(),
},
},
{
name: "custom-tags-sidecar",
tproxy: getProxy(),
in: &meshconfig.Tracing{
CustomTags: map[string]*meshconfig.Tracing_CustomTag{
"custom_tag_env": {
Type: &meshconfig.Tracing_CustomTag_Environment{
Environment: &meshconfig.Tracing_Environment{
Name: "custom_tag_env-var",
DefaultValue: "custom-tag-env-default",
},
},
},
"custom_tag_request_header": {
Type: &meshconfig.Tracing_CustomTag_Header{
Header: &meshconfig.Tracing_RequestHeader{
Name: "custom_tag_request_header_name",
DefaultValue: "custom-defaulted-value-request-header",
},
},
},
// leave this in non-alphanumeric order to verify
// the stable sorting doing when creating the custom tag filter
"custom_tag_literal": {
Type: &meshconfig.Tracing_CustomTag_Literal{
Literal: &meshconfig.Tracing_Literal{
Value: "literal-value",
},
},
},
},
},
out: &hcm.HttpConnectionManager_Tracing{
ClientSampling: &xdstype.Percent{
Value: 100.0,
},
RandomSampling: &xdstype.Percent{
Value: 1.0,
},
OverallSampling: &xdstype.Percent{
Value: 100.0,
},
CustomTags: append([]*tracing.CustomTag{
{
Tag: "custom_tag_env",
Type: &tracing.CustomTag_Environment_{
Environment: &tracing.CustomTag_Environment{
Name: "custom_tag_env-var",
DefaultValue: "custom-tag-env-default",
},
},
},
{
Tag: "custom_tag_literal",
Type: &tracing.CustomTag_Literal_{
Literal: &tracing.CustomTag_Literal{
Value: "literal-value",
},
},
},
{
Tag: "custom_tag_request_header",
Type: &tracing.CustomTag_RequestHeader{
RequestHeader: &tracing.CustomTag_Header{
Name: "custom_tag_request_header_name",
DefaultValue: "custom-defaulted-value-request-header",
},
},
},
}, customTracingTags()...),
},
},
{
name: "custom-tags-sidecar-without-istio-tags",
disableIstioTags: true,
tproxy: getProxy(),
in: &meshconfig.Tracing{
CustomTags: map[string]*meshconfig.Tracing_CustomTag{
"custom_tag_env": {
Type: &meshconfig.Tracing_CustomTag_Environment{
Environment: &meshconfig.Tracing_Environment{
Name: "custom_tag_env-var",
DefaultValue: "custom-tag-env-default",
},
},
},
"custom_tag_request_header": {
Type: &meshconfig.Tracing_CustomTag_Header{
Header: &meshconfig.Tracing_RequestHeader{
Name: "custom_tag_request_header_name",
DefaultValue: "custom-defaulted-value-request-header",
},
},
},
// leave this in non-alphanumeric order to verify
// the stable sorting doing when creating the custom tag filter
"custom_tag_literal": {
Type: &meshconfig.Tracing_CustomTag_Literal{
Literal: &meshconfig.Tracing_Literal{
Value: "literal-value",
},
},
},
},
},
out: &hcm.HttpConnectionManager_Tracing{
ClientSampling: &xdstype.Percent{
Value: 100.0,
},
RandomSampling: &xdstype.Percent{
Value: 1.0,
},
OverallSampling: &xdstype.Percent{
Value: 100.0,
},
CustomTags: []*tracing.CustomTag{
{
Tag: "custom_tag_env",
Type: &tracing.CustomTag_Environment_{
Environment: &tracing.CustomTag_Environment{
Name: "custom_tag_env-var",
DefaultValue: "custom-tag-env-default",
},
},
},
{
Tag: "custom_tag_literal",
Type: &tracing.CustomTag_Literal_{
Literal: &tracing.CustomTag_Literal{
Value: "literal-value",
},
},
},
{
Tag: "custom_tag_request_header",
Type: &tracing.CustomTag_RequestHeader{
RequestHeader: &tracing.CustomTag_Header{
Name: "custom_tag_request_header_name",
DefaultValue: "custom-defaulted-value-request-header",
},
},
},
},
},
},
{
name: "custom-tracing-gateways",
tproxy: &proxyGateway,
in: &meshconfig.Tracing{
MaxPathTagLength: 100,
CustomTags: map[string]*meshconfig.Tracing_CustomTag{
"custom_tag_request_header": {
Type: &meshconfig.Tracing_CustomTag_Header{
Header: &meshconfig.Tracing_RequestHeader{
Name: "custom_tag_request_header_name",
DefaultValue: "custom-defaulted-value-request-header",
},
},
},
},
},
out: &hcm.HttpConnectionManager_Tracing{
ClientSampling: &xdstype.Percent{
Value: 100.0,
},
RandomSampling: &xdstype.Percent{
Value: 1.0,
},
OverallSampling: &xdstype.Percent{
Value: 100.0,
},
MaxPathTagLength: &wrappers.UInt32Value{
Value: 100,
},
CustomTags: append([]*tracing.CustomTag{
{
Tag: "custom_tag_request_header",
Type: &tracing.CustomTag_RequestHeader{
RequestHeader: &tracing.CustomTag_Header{
Name: "custom_tag_request_header_name",
DefaultValue: "custom-defaulted-value-request-header",
},
},
},
}, customTracingTags()...),
},
},
}
for _, tc := range customTagsTest {
t.Run(tc.name, func(t *testing.T) {
if tc.envPilotSampling != 0.0 {
test.SetFloatForTest(t, &features.TraceSampling, tc.envPilotSampling)
}
test.SetBoolForTest(t, &features.EnableIstioTags, !tc.disableIstioTags)
m := mesh.DefaultMeshConfig()
m.ProxyHttpPort = 15007
m.EnableTracing = true
m.DefaultConfig = &meshconfig.ProxyConfig{
Tracing: &meshconfig.Tracing{
CustomTags: tc.in.CustomTags,
MaxPathTagLength: tc.in.MaxPathTagLength,
Sampling: tc.in.Sampling,
},
}
listeners := buildListeners(t, TestOptions{MeshConfig: m}, nil)
httpProxy := xdstest.ExtractListener("127.0.0.1_15007", listeners)
f := httpProxy.FilterChains[0].Filters[0]
verifyHTTPConnectionManagerFilter(t, f, tc.out, tc.name)
})
}
}
func customTracingTags() []*tracing.CustomTag {
return append(buildOptionalPolicyTags(),
&tracing.CustomTag{
Tag: "istio.canonical_revision",
Type: &tracing.CustomTag_Literal_{
Literal: &tracing.CustomTag_Literal{
Value: "latest",
},
},
},
&tracing.CustomTag{
Tag: "istio.canonical_service",
Type: &tracing.CustomTag_Literal_{
Literal: &tracing.CustomTag_Literal{
Value: "unknown",
},
},
},
&tracing.CustomTag{
Tag: "istio.mesh_id",
Type: &tracing.CustomTag_Literal_{
Literal: &tracing.CustomTag_Literal{
Value: "unknown",
},
},
},
&tracing.CustomTag{
Tag: "istio.namespace",
Type: &tracing.CustomTag_Literal_{
Literal: &tracing.CustomTag_Literal{
Value: "default",
},
},
})
}
func verifyHTTPConnectionManagerFilter(t *testing.T, f *listener.Filter, expected *hcm.HttpConnectionManager_Tracing, name string) {
t.Helper()
if f.Name == wellknown.HTTPConnectionManager {
cmgr := &hcm.HttpConnectionManager{}
err := getFilterConfig(f, cmgr)
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(cmgr.GetTracing(), expected, protocmp.Transform()); diff != "" {
t.Fatalf("Testcase failure: %s custom tags did match not expected output; diff: %v", name, diff)
}
}
}
func TestOutboundListenerConfig_TCPFailThrough(t *testing.T) {
// Add a service and verify it's config
services := []*model.Service{
buildService("test1.com", wildcardIP, protocol.HTTP, tnow),
}
listeners := buildListeners(t, TestOptions{Services: services}, nil)
l := xdstest.ExtractListener("0.0.0.0_8080", listeners)
if l == nil {
t.Fatalf("failed to find listener")
}
if len(l.FilterChains) != 1 {
t.Fatalf("expectd %d filter chains, found %d", 1, len(l.FilterChains))
}
verifyHTTPFilterChainMatch(t, l.FilterChains[0])
verifyPassThroughTCPFilterChain(t, l.DefaultFilterChain)
verifyListenerFilters(t, l.ListenerFilters)
}
func verifyPassThroughTCPFilterChain(t *testing.T, fc *listener.FilterChain) {
t.Helper()
f := fc.Filters[0]
expectedStatPrefix := util.PassthroughCluster
cfg, _ := conversion.MessageToStruct(f.GetTypedConfig())
statPrefix := cfg.Fields["stat_prefix"].GetStringValue()
if statPrefix != expectedStatPrefix {
t.Fatalf("expected listener to contain stat_prefix %s, found %s", expectedStatPrefix, statPrefix)
}
}
func verifyOutboundTCPListenerHostname(t *testing.T, l *listener.Listener, hostname host.Name) {
t.Helper()
if len(l.FilterChains) != 1 {
t.Fatalf("expected %d filter chains, found %d", 1, len(l.FilterChains))
}
fc := l.FilterChains[0]
f := getTCPFilter(fc)
if f == nil {
t.Fatalf("expected TCP filters, found none")
}
expectedStatPrefix := fmt.Sprintf("outbound|8080||%s", hostname)
cfg, _ := conversion.MessageToStruct(f.GetTypedConfig())
statPrefix := cfg.Fields["stat_prefix"].GetStringValue()
if statPrefix != expectedStatPrefix {
t.Fatalf("expected listener to contain stat_prefix %s, found %s", expectedStatPrefix, statPrefix)
}
}
func verifyFilterChainMatch(t *testing.T, listener *listener.Listener) {
httpFilters := []string{
xdsfilters.MxFilterName,
xdsfilters.Fault.Name,
xdsfilters.Cors.Name,
xdsfilters.Router.Name,
}
listenertest.VerifyListener(t, listener, listenertest.ListenerTest{
FilterChains: []listenertest.FilterChainTest{
{
Name: model.VirtualInboundBlackholeFilterChainName,
Port: 15006,
TotalMatch: true,
},
{
Name: model.VirtualInboundCatchAllHTTPFilterChainName,
Type: listenertest.MTLSHTTP,
HTTPFilters: httpFilters,
NetworkFilters: []string{xdsfilters.MxFilterName, wellknown.HTTPConnectionManager},
TotalMatch: true,
},
{
Name: model.VirtualInboundCatchAllHTTPFilterChainName,
Type: listenertest.PlainHTTP,
HTTPFilters: httpFilters,
NetworkFilters: []string{xdsfilters.MxFilterName, wellknown.HTTPConnectionManager},
TotalMatch: true,
},
{
Name: model.VirtualInboundListenerName,
Type: listenertest.MTLSTCP,
HTTPFilters: []string{},
NetworkFilters: []string{xdsfilters.MxFilterName, wellknown.TCPProxy},
TotalMatch: true,
},
{
Name: model.VirtualInboundListenerName,
Type: listenertest.PlainTCP,
HTTPFilters: []string{},
NetworkFilters: []string{xdsfilters.MxFilterName, wellknown.TCPProxy},
TotalMatch: true,
},
{
Name: model.VirtualInboundListenerName,
Type: listenertest.StandardTLS,
HTTPFilters: []string{},
NetworkFilters: []string{xdsfilters.MxFilterName, wellknown.TCPProxy},
TotalMatch: true,
},
},
})
}
func getOldestService(services ...*model.Service) *model.Service {
var oldestService *model.Service
for _, s := range services {
if oldestService == nil || s.CreationTime.Before(oldestService.CreationTime) {
oldestService = s
}
}
return oldestService
}
func getFilterConfig(filter *listener.Filter, out proto.Message) error {
switch c := filter.ConfigType.(type) {
case *listener.Filter_TypedConfig:
if err := c.TypedConfig.UnmarshalTo(out); err != nil {
return err
}
}
return nil
}
func buildOutboundListeners(t *testing.T, proxy *model.Proxy, sidecarConfig *config.Config,
virtualService *config.Config, services ...*model.Service) []*listener.Listener {
t.Helper()
cg := NewConfigGenTest(t, TestOptions{
Services: services,
ConfigPointers: []*config.Config{sidecarConfig, virtualService},
})
listeners := NewListenerBuilder(proxy, cg.env.PushContext).buildSidecarOutboundListeners(cg.SetupProxy(proxy), cg.env.PushContext)
xdstest.ValidateListeners(t, listeners)
return listeners
}
func isHTTPListener(listener *listener.Listener) bool {
for _, fc := range listener.GetFilterChains() {
if isHTTPFilterChain(fc) {
return true
}
}
return false
}
func isMysqlListener(listener *listener.Listener) bool {
if len(listener.FilterChains) > 0 && len(listener.FilterChains[0].Filters) > 0 {
return listener.FilterChains[0].Filters[0].Name == wellknown.MySQLProxy
}
return false
}
func hasListenerOrFilterChainForPort(listeners []*listener.Listener, port uint32) bool {
for _, l := range listeners {
if port == l.Address.GetSocketAddress().GetPortValue() {
return true
}
for _, fc := range l.GetFilterChains() {
if fc.GetFilterChainMatch().GetDestinationPort().GetValue() == port {
return true
}
}
}
return false
}
func findListenerByPort(listeners []*listener.Listener, port uint32) *listener.Listener {
for _, l := range listeners {
if port == l.Address.GetSocketAddress().GetPortValue() {
return l
}
}
return nil
}
func findListenerByAddress(listeners []*listener.Listener, address string) *listener.Listener {
for _, l := range listeners {
if address == l.Address.GetSocketAddress().Address {
return l
}
}
return nil
}
func buildService(hostname string, ip string, protocol protocol.Instance, creationTime time.Time) *model.Service {
return &model.Service{
CreationTime: creationTime,
Hostname: host.Name(hostname),
DefaultAddress: ip,
Ports: model.PortList{
&model.Port{
Name: "default",
Port: 8080,
Protocol: protocol,
},
},
Resolution: model.ClientSideLB,
Attributes: model.ServiceAttributes{
Namespace: "default",
},
}
}
func buildServiceWithPort(hostname string, port int, protocol protocol.Instance, creationTime time.Time) *model.Service {
return &model.Service{
CreationTime: creationTime,
Hostname: host.Name(hostname),
DefaultAddress: wildcardIP,
Ports: model.PortList{
&model.Port{
Name: "default",
Port: port,
Protocol: protocol,
},
},
Resolution: model.ClientSideLB,
Attributes: model.ServiceAttributes{
Namespace: "default",
},
}
}
func buildServiceInstance(service *model.Service, instanceIP string) *model.ServiceInstance {
return &model.ServiceInstance{
Endpoint: &model.IstioEndpoint{
Address: instanceIP,
},
ServicePort: service.Ports[0],
Service: service,
}
}
func TestAppendListenerFallthroughRouteForCompleteListener(t *testing.T) {
tests := []struct {
name string
node *model.Proxy
hostname string
idleTimeout *durationpb.Duration
}{
{
name: "Registry_Only",
node: &model.Proxy{
ID: "foo.bar",
Metadata: &model.NodeMetadata{},
SidecarScope: &model.SidecarScope{
OutboundTrafficPolicy: &networking.OutboundTrafficPolicy{
Mode: networking.OutboundTrafficPolicy_REGISTRY_ONLY,
},
},
},
hostname: util.BlackHoleCluster,
},
{
name: "Allow_Any",
node: &model.Proxy{
ID: "foo.bar",
Metadata: &model.NodeMetadata{},
SidecarScope: &model.SidecarScope{
OutboundTrafficPolicy: &networking.OutboundTrafficPolicy{
Mode: networking.OutboundTrafficPolicy_ALLOW_ANY,
},
},
},
hostname: util.PassthroughCluster,
},
{
name: "idle_timeout",
node: &model.Proxy{
ID: "foo.bar",
Metadata: &model.NodeMetadata{
IdleTimeout: "15s",
},
SidecarScope: &model.SidecarScope{
OutboundTrafficPolicy: &networking.OutboundTrafficPolicy{
Mode: networking.OutboundTrafficPolicy_ALLOW_ANY,
},
},
},
hostname: util.PassthroughCluster,
idleTimeout: durationpb.New(15 * time.Second),
},
{
name: "invalid_idle_timeout",
node: &model.Proxy{
ID: "foo.bar",
Metadata: &model.NodeMetadata{
IdleTimeout: "s15s",
},
SidecarScope: &model.SidecarScope{
OutboundTrafficPolicy: &networking.OutboundTrafficPolicy{
Mode: networking.OutboundTrafficPolicy_ALLOW_ANY,
},
},
},
hostname: util.PassthroughCluster,
idleTimeout: durationpb.New(0 * time.Second),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cg := NewConfigGenTest(t, TestOptions{})
l := &listener.Listener{}
appendListenerFallthroughRouteForCompleteListener(l, tt.node, cg.PushContext())
if len(l.FilterChains) != 0 {
t.Errorf("Expected exactly 0 filter chain")
}
if len(l.DefaultFilterChain.Filters) != 1 {
t.Errorf("Expected exactly 1 network filter in the chain")
}
filter := l.DefaultFilterChain.Filters[0]
var tcpProxy tcp.TcpProxy
cfg := filter.GetTypedConfig()
_ = cfg.UnmarshalTo(&tcpProxy)
if tcpProxy.StatPrefix != tt.hostname {
t.Errorf("Expected stat prefix %s but got %s\n", tt.hostname, tcpProxy.StatPrefix)
}
if tcpProxy.GetCluster() != tt.hostname {
t.Errorf("Expected cluster %s but got %s\n", tt.hostname, tcpProxy.GetCluster())
}
if tt.idleTimeout != nil && !reflect.DeepEqual(tcpProxy.IdleTimeout, tt.idleTimeout) {
t.Errorf("Expected IdleTimeout %s but got %s\n", tt.idleTimeout, tcpProxy.IdleTimeout)
}
})
}
}
func TestMergeTCPFilterChains(t *testing.T) {
cg := NewConfigGenTest(t, TestOptions{})
node := &model.Proxy{
ID: "foo.bar",
Metadata: &model.NodeMetadata{},
SidecarScope: &model.SidecarScope{
OutboundTrafficPolicy: &networking.OutboundTrafficPolicy{
Mode: networking.OutboundTrafficPolicy_ALLOW_ANY,
},
},
}
tcpProxy := &tcp.TcpProxy{
StatPrefix: "outbound|443||foo.com",
ClusterSpecifier: &tcp.TcpProxy_Cluster{Cluster: "outbound|443||foo.com"},
}
tcpProxyFilter := &listener.Filter{
Name: wellknown.TCPProxy,
ConfigType: &listener.Filter_TypedConfig{TypedConfig: util.MessageToAny(tcpProxy)},
}
tcpProxy = &tcp.TcpProxy{
StatPrefix: "outbound|443||bar.com",
ClusterSpecifier: &tcp.TcpProxy_Cluster{Cluster: "outbound|443||bar.com"},
}
tcpProxyFilter2 := &listener.Filter{
Name: wellknown.TCPProxy,
ConfigType: &listener.Filter_TypedConfig{TypedConfig: util.MessageToAny(tcpProxy)},
}
svcPort := &model.Port{
Name: "https",
Port: 443,
Protocol: protocol.HTTPS,
}
var l listener.Listener
filterChains := []*listener.FilterChain{
{
FilterChainMatch: &listener.FilterChainMatch{
PrefixRanges: []*core.CidrRange{
{
AddressPrefix: "10.244.0.18",
PrefixLen: &wrappers.UInt32Value{Value: 32},
},
{
AddressPrefix: "fe80::1c97:c3ff:fed7:5940",
PrefixLen: &wrappers.UInt32Value{Value: 128},
},
},
},
Filters: nil, // This is not a valid config, just for test
},
{
FilterChainMatch: &listener.FilterChainMatch{
ServerNames: []string{"foo.com"},
},
// This is not a valid config, just for test
Filters: []*listener.Filter{tcpProxyFilter},
},
{
FilterChainMatch: &listener.FilterChainMatch{},
// This is not a valid config, just for test
Filters: buildOutboundCatchAllNetworkFiltersOnly(cg.PushContext(), node),
},
}
l.FilterChains = filterChains
listenerMap := map[string]*outboundListenerEntry{
"0.0.0.0_443": {
servicePort: svcPort,
services: []*model.Service{{
CreationTime: tnow,
Hostname: host.Name("foo.com"),
DefaultAddress: "192.168.1.1",
Ports: []*model.Port{svcPort},
Resolution: model.DNSLB,
}},
listener: &l,
},
}
incomingFilterChains := []*listener.FilterChain{
{
FilterChainMatch: &listener.FilterChainMatch{
ServerNames: []string{"bar.com"},
}, // This is not a valid config, just for test
Filters: []*listener.Filter{tcpProxyFilter2},
},
}
svc := model.Service{
Hostname: "bar.com",
}
opts := buildListenerOpts{
proxy: node,
push: cg.PushContext(),
service: &svc,
}
out := mergeTCPFilterChains(incomingFilterChains, opts, "0.0.0.0_443", listenerMap)
if len(out) != 4 {
t.Errorf("Got %d filter chains, expected 3", len(out))
}
if !isMatchAllFilterChain(out[2]) {
t.Errorf("The last filter chain %#v is not wildcard matching", out[2])
}
if !reflect.DeepEqual(out[3].Filters, incomingFilterChains[0].Filters) {
t.Errorf("got %v\nwant %v\ndiff %v", out[2].Filters, incomingFilterChains[0].Filters, cmp.Diff(out[2].Filters, incomingFilterChains[0].Filters))
}
}
func TestFilterChainMatchEqual(t *testing.T) {
cases := []struct {
name string
first *listener.FilterChainMatch
second *listener.FilterChainMatch
want bool
}{
{
name: "both nil",
first: nil,
second: nil,
want: true,
},
{
name: "one of them nil",
first: nil,
second: &listener.FilterChainMatch{},
want: false,
},
{
name: "both empty",
first: &listener.FilterChainMatch{},
second: &listener.FilterChainMatch{},
want: true,
},
{
name: "with equal values",
first: &listener.FilterChainMatch{
TransportProtocol: "TCP",
ApplicationProtocols: mtlsHTTPALPNs,
},
second: &listener.FilterChainMatch{
TransportProtocol: "TCP",
ApplicationProtocols: mtlsHTTPALPNs,
},
want: true,
},
{
name: "with not equal values",
first: &listener.FilterChainMatch{
TransportProtocol: "TCP",
ApplicationProtocols: mtlsHTTPALPNs,
},
second: &listener.FilterChainMatch{
TransportProtocol: "TCP",
ApplicationProtocols: plaintextHTTPALPNs,
},
want: false,
},
{
name: "equal with all values",
first: &listener.FilterChainMatch{
TransportProtocol: "TCP",
ApplicationProtocols: mtlsHTTPALPNs,
DestinationPort: &wrappers.UInt32Value{Value: 1999},
AddressSuffix: "suffix",
SourceType: listener.FilterChainMatch_ANY,
SuffixLen: &wrappers.UInt32Value{Value: 3},
PrefixRanges: []*core.CidrRange{
{
AddressPrefix: "10.244.0.18",
PrefixLen: &wrappers.UInt32Value{Value: 32},
},
{
AddressPrefix: "fe80::1c97:c3ff:fed7:5940",
PrefixLen: &wrappers.UInt32Value{Value: 128},
},
},
SourcePrefixRanges: []*core.CidrRange{
{
AddressPrefix: "10.244.0.18",
PrefixLen: &wrappers.UInt32Value{Value: 32},
},
{
AddressPrefix: "fe80::1c97:c3ff:fed7:5940",
PrefixLen: &wrappers.UInt32Value{Value: 128},
},
},
SourcePorts: []uint32{2000},
ServerNames: []string{"foo"},
},
second: &listener.FilterChainMatch{
TransportProtocol: "TCP",
ApplicationProtocols: plaintextHTTPALPNs,
DestinationPort: &wrappers.UInt32Value{Value: 1999},
AddressSuffix: "suffix",
SourceType: listener.FilterChainMatch_ANY,
SuffixLen: &wrappers.UInt32Value{Value: 3},
PrefixRanges: []*core.CidrRange{
{
AddressPrefix: "10.244.0.18",
PrefixLen: &wrappers.UInt32Value{Value: 32},
},
{
AddressPrefix: "fe80::1c97:c3ff:fed7:5940",
PrefixLen: &wrappers.UInt32Value{Value: 128},
},
},
SourcePrefixRanges: []*core.CidrRange{
{
AddressPrefix: "10.244.0.18",
PrefixLen: &wrappers.UInt32Value{Value: 32},
},
{
AddressPrefix: "fe80::1c97:c3ff:fed7:5940",
PrefixLen: &wrappers.UInt32Value{Value: 128},
},
},
SourcePorts: []uint32{2000},
ServerNames: []string{"foo"},
},
want: false,
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
if got := filterChainMatchEqual(tt.first, tt.second); got != tt.want {
t.Fatalf("Expected filter chain match to return %v, but got %v", tt.want, got)
}
})
}
}