istio.io/istio@v0.0.0-20240520182934-d79c90f27776/tests/integration/pilot/tunneling_test.go (about)

     1  //go:build integ
     2  // +build integ
     3  
     4  // Copyright Istio Authors
     5  //
     6  // Licensed under the Apache License, Version 2.0 (the "License");
     7  // you may not use this file except in compliance with the License.
     8  // You may obtain a copy of the License at
     9  //
    10  //     http://www.apache.org/licenses/LICENSE-2.0
    11  //
    12  // Unless required by applicable law or agreed to in writing, software
    13  // distributed under the License is distributed on an "AS IS" BASIS,
    14  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    15  // See the License for the specific language governing permissions and
    16  // limitations under the License.
    17  
    18  package pilot
    19  
    20  import (
    21  	"context"
    22  	"fmt"
    23  	"os"
    24  	"strings"
    25  	"sync"
    26  	"testing"
    27  	"time"
    28  
    29  	corev1 "k8s.io/api/core/v1"
    30  	"k8s.io/apimachinery/pkg/util/intstr"
    31  
    32  	"istio.io/istio/pkg/config/protocol"
    33  	"istio.io/istio/pkg/test/framework"
    34  	"istio.io/istio/pkg/test/framework/components/echo"
    35  	"istio.io/istio/pkg/test/framework/components/echo/common/ports"
    36  	"istio.io/istio/pkg/test/framework/components/istioctl"
    37  	"istio.io/istio/pkg/test/util/retry"
    38  	"istio.io/istio/tests/integration/pilot/forwardproxy"
    39  )
    40  
    41  const (
    42  	forwardProxyConfigMapFile    = "testdata/forward-proxy/configmap.tmpl.yaml"
    43  	forwardProxyServiceFile      = "testdata/forward-proxy/service.tmpl.yaml"
    44  	tunnelingDestinationRuleFile = "testdata/tunneling/destination-rule.tmpl.yaml"
    45  )
    46  
    47  type tunnelingTestCase struct {
    48  	// configDir is a directory with Istio configuration files for a particular test case
    49  	configDir string
    50  }
    51  
    52  type testRequestSpec struct {
    53  	protocol protocol.Instance
    54  	port     echo.Port
    55  }
    56  
    57  var forwardProxyConfigurations = []forwardproxy.ListenerSettings{
    58  	{
    59  		Port:        3128,
    60  		HTTPVersion: forwardproxy.HTTP1,
    61  		TLSEnabled:  false,
    62  	},
    63  	{
    64  		Port:        4128,
    65  		HTTPVersion: forwardproxy.HTTP1,
    66  		TLSEnabled:  true,
    67  	},
    68  	{
    69  		Port:        5128,
    70  		HTTPVersion: forwardproxy.HTTP2,
    71  		TLSEnabled:  false,
    72  	},
    73  	{
    74  		Port:        6128,
    75  		HTTPVersion: forwardproxy.HTTP2,
    76  		TLSEnabled:  true,
    77  	},
    78  }
    79  
    80  var requestsSpec = []testRequestSpec{
    81  	{
    82  		protocol: protocol.HTTP,
    83  		port:     ports.TCPForHTTP,
    84  	},
    85  	{
    86  		protocol: protocol.HTTPS,
    87  		port:     ports.HTTPS,
    88  	},
    89  }
    90  
    91  var testCases = []tunnelingTestCase{
    92  	{
    93  		configDir: "sidecar",
    94  	},
    95  	{
    96  		configDir: "gateway/tcp",
    97  	},
    98  	{
    99  		configDir: "gateway/tls/istio-mutual",
   100  	},
   101  	{
   102  		configDir: "gateway/tls/passthrough",
   103  	},
   104  }
   105  
   106  func TestTunnelingOutboundTraffic(t *testing.T) {
   107  	framework.
   108  		NewTest(t).
   109  		RequireIstioVersion("1.15.0").
   110  		Run(func(ctx framework.TestContext) {
   111  			meshNs := apps.A.NamespaceName()
   112  			externalNs := apps.External.Namespace.Name()
   113  
   114  			applyForwardProxyConfigMaps(ctx, externalNs)
   115  			ctx.ConfigIstio().File(externalNs, "testdata/external-forward-proxy-deployment.yaml").ApplyOrFail(ctx)
   116  			applyForwardProxyService(ctx, externalNs)
   117  			externalForwardProxyIPs, err := i.PodIPsFor(ctx.Clusters().Default(), externalNs, "app=external-forward-proxy")
   118  			if err != nil {
   119  				t.Fatalf("error getting external forward proxy ips: %v", err)
   120  			}
   121  
   122  			for _, proxyConfig := range forwardProxyConfigurations {
   123  				templateParams := map[string]any{
   124  					"externalNamespace":             externalNs,
   125  					"forwardProxyPort":              proxyConfig.Port,
   126  					"tlsEnabled":                    proxyConfig.TLSEnabled,
   127  					"externalSvcTcpPort":            ports.TCPForHTTP.ServicePort,
   128  					"externalSvcTlsPort":            ports.HTTPS.ServicePort,
   129  					"EgressGatewayIstioLabel":       i.Settings().EgressGatewayIstioLabel,
   130  					"EgressGatewayServiceName":      i.Settings().EgressGatewayServiceName,
   131  					"EgressGatewayServiceNamespace": i.Settings().EgressGatewayServiceNamespace,
   132  				}
   133  				ctx.ConfigIstio().EvalFile(externalNs, templateParams, tunnelingDestinationRuleFile).ApplyOrFail(ctx)
   134  
   135  				for _, tc := range testCases {
   136  					for _, file := range listFilesInDirectory(ctx, tc.configDir) {
   137  						ctx.ConfigIstio().EvalFile(meshNs, templateParams, file).ApplyOrFail(ctx)
   138  					}
   139  
   140  					for _, spec := range requestsSpec {
   141  						testName := fmt.Sprintf("%s/%s/%s/%s-request",
   142  							proxyConfig.HTTPVersion, proxyConfig.TLSEnabledStr(), tc.configDir, spec.protocol)
   143  						ctx.NewSubTest(testName).Run(func(ctx framework.TestContext) {
   144  							// requests will fail until istio-proxy gets the Envoy configuration from istiod, so retries are necessary
   145  							retry.UntilSuccessOrFail(ctx, func() error {
   146  								client := apps.A[0]
   147  								target := apps.External.All[0]
   148  								if err := testConnectivity(client, target, spec.protocol, spec.port, testName); err != nil {
   149  									return err
   150  								}
   151  								if err := verifyThatRequestWasTunneled(target, externalForwardProxyIPs, testName); err != nil {
   152  									return err
   153  								}
   154  								return nil
   155  							}, retry.Timeout(10*time.Second))
   156  						})
   157  					}
   158  
   159  					for _, file := range listFilesInDirectory(ctx, tc.configDir) {
   160  						ctx.ConfigIstio().EvalFile(meshNs, templateParams, file).DeleteOrFail(ctx)
   161  					}
   162  
   163  					// Make sure that configuration changes were pushed to istio-proxies.
   164  					// Otherwise, test results could be false-positive,
   165  					// because subsequent test cases could work thanks to previous configurations.
   166  
   167  					waitUntilTunnelingConfigurationIsRemovedOrFail(ctx, meshNs, i.Settings().EgressGatewayServiceNamespace, i.Settings().EgressGatewayServiceName)
   168  				}
   169  
   170  				ctx.ConfigIstio().EvalFile(externalNs, templateParams, tunnelingDestinationRuleFile).DeleteOrFail(ctx)
   171  			}
   172  		})
   173  }
   174  
   175  func testConnectivity(from, to echo.Instance, p protocol.Instance, port echo.Port, testName string) error {
   176  	res, err := from.Call(echo.CallOptions{
   177  		Address: to.ClusterLocalFQDN(),
   178  		Port: echo.Port{
   179  			Protocol:    p,
   180  			ServicePort: port.ServicePort,
   181  		},
   182  		HTTP: echo.HTTP{
   183  			Path: "/" + testName,
   184  		},
   185  	})
   186  	if err != nil {
   187  		return fmt.Errorf("failed to request to external service: %s", err)
   188  	}
   189  	if res.Responses[0].Code != "200" {
   190  		return fmt.Errorf("expected to get 200 status code, got: %s", res.Responses[0].Code)
   191  	}
   192  	return nil
   193  }
   194  
   195  func verifyThatRequestWasTunneled(target echo.Instance, expectedSourceIPs []corev1.PodIP, expectedPath string) error {
   196  	workloads, err := target.Workloads()
   197  	if err != nil {
   198  		return fmt.Errorf("failed to get workloads of %s: %s", target.ServiceName(), err)
   199  	}
   200  	var logs strings.Builder
   201  	for _, w := range workloads {
   202  		workloadLogs, err := w.Logs()
   203  		if err != nil {
   204  			return fmt.Errorf("failed to get logs of workload %s: %s", w.PodName(), err)
   205  		}
   206  		logs.WriteString(workloadLogs)
   207  	}
   208  
   209  	expectedTunnelLogFound := false
   210  	for _, expectedSourceIP := range expectedSourceIPs {
   211  		expectedLog := fmt.Sprintf("remoteAddr=%s method=GET url=/%s", expectedSourceIP.IP, expectedPath)
   212  		if strings.Contains(logs.String(), expectedLog) {
   213  			expectedTunnelLogFound = true
   214  			break
   215  		}
   216  	}
   217  	if !expectedTunnelLogFound {
   218  		return fmt.Errorf("failed to find expected tunnel log in logs of %s", target.ServiceName())
   219  	}
   220  	return nil
   221  }
   222  
   223  func applyForwardProxyConfigMaps(ctx framework.TestContext, externalNs string) {
   224  	bootstrapYaml, err := forwardproxy.GenerateForwardProxyBootstrapConfig(forwardProxyConfigurations)
   225  	if err != nil {
   226  		ctx.Fatalf("failed to generate bootstrap configuration for external-forward-proxy: %s", err)
   227  	}
   228  
   229  	subject := fmt.Sprintf("external-forward-proxy.%s.svc.cluster.local", externalNs)
   230  	key, crt, err := forwardproxy.GenerateKeyAndCertificate(subject, ctx.TempDir())
   231  	if err != nil {
   232  		ctx.Fatalf("failed to generate private key and certificate: %s", err)
   233  	}
   234  
   235  	templateParams := map[string]any{
   236  		"envoyYaml": bootstrapYaml,
   237  		"keyPem":    key,
   238  		"certPem":   crt,
   239  	}
   240  	ctx.ConfigIstio().EvalFile(externalNs, templateParams, forwardProxyConfigMapFile).ApplyOrFail(ctx)
   241  }
   242  
   243  func applyForwardProxyService(ctx framework.TestContext, externalNs string) {
   244  	var servicePorts []corev1.ServicePort
   245  	for i, cfg := range forwardProxyConfigurations {
   246  		servicePorts = append(servicePorts, corev1.ServicePort{
   247  			Name:       fmt.Sprintf("%s-%d", selectPortName(cfg.HTTPVersion), i),
   248  			Port:       int32(cfg.Port),
   249  			TargetPort: intstr.FromInt32(int32(cfg.Port)),
   250  		})
   251  	}
   252  	templateParams := map[string]any{
   253  		"ports": servicePorts,
   254  	}
   255  	ctx.ConfigIstio().EvalFile(externalNs, templateParams, forwardProxyServiceFile).ApplyOrFail(ctx)
   256  }
   257  
   258  func listFilesInDirectory(ctx framework.TestContext, dir string) []string {
   259  	files, err := os.ReadDir("testdata/tunneling/" + dir)
   260  	if err != nil {
   261  		ctx.Fatalf("failed to read files in directory: %s", err)
   262  	}
   263  	filesList := make([]string, 0, len(files))
   264  	for _, file := range files {
   265  		filesList = append(filesList, fmt.Sprintf("testdata/tunneling/%s/%s", dir, file.Name()))
   266  	}
   267  	return filesList
   268  }
   269  
   270  func selectPortName(httpVersion string) string {
   271  	if httpVersion == forwardproxy.HTTP1 {
   272  		return "http-connect"
   273  	}
   274  	return "http2-connect"
   275  }
   276  
   277  func getPodName(ctx framework.TestContext, ns, appSelector string) string {
   278  	return getPodStringProperty(ctx, ns, appSelector, func(pod corev1.Pod) string {
   279  		return pod.Name
   280  	})
   281  }
   282  
   283  func getPodStringProperty(ctx framework.TestContext, ns, selector string, getPodProperty func(pod corev1.Pod) string) string {
   284  	var podProperty string
   285  	kubeClient := ctx.Clusters().Default()
   286  	retry.UntilSuccessOrFail(ctx, func() error {
   287  		pods, err := kubeClient.PodsForSelector(context.TODO(), ns, fmt.Sprintf("app=%s", selector))
   288  		if err != nil {
   289  			return fmt.Errorf("failed to get pods for selector app=%s: %v", selector, err)
   290  		}
   291  		if len(pods.Items) == 0 {
   292  			return fmt.Errorf("no pods for selector app=%s", selector)
   293  		}
   294  		podProperty = getPodProperty(pods.Items[0])
   295  		return nil
   296  	}, retry.Timeout(30*time.Second))
   297  	return podProperty
   298  }
   299  
   300  func waitUntilTunnelingConfigurationIsRemovedOrFail(ctx framework.TestContext, meshNs string, egressNs string, egressLabel string) {
   301  	var wg sync.WaitGroup
   302  	wg.Add(1)
   303  	go func() {
   304  		defer wg.Done()
   305  		waitForTunnelingRemovedOrFail(ctx, meshNs, "a")
   306  	}()
   307  	wg.Add(1)
   308  	go func() {
   309  		defer wg.Done()
   310  		waitForTunnelingRemovedOrFail(ctx, egressNs, egressLabel)
   311  	}()
   312  	wg.Wait()
   313  }
   314  
   315  func waitForTunnelingRemovedOrFail(ctx framework.TestContext, ns, app string) {
   316  	istioCtl := istioctl.NewOrFail(ctx, ctx, istioctl.Config{Cluster: ctx.Clusters().Default()})
   317  	podName := getPodName(ctx, ns, app)
   318  	args := []string{"proxy-config", "listeners", "-n", ns, podName, "-o", "json"}
   319  	retry.UntilSuccessOrFail(ctx, func() error {
   320  		out, _, err := istioCtl.Invoke(args)
   321  		if err != nil {
   322  			return fmt.Errorf("failed to get listeners of %s/%s: %s", app, ns, err)
   323  		}
   324  		if strings.Contains(out, "tunnelingConfig") {
   325  			return fmt.Errorf("tunnelingConfig was not removed from istio-proxy configuration in %s/%s", app, ns)
   326  		}
   327  		return nil
   328  	}, retry.Timeout(10*time.Second))
   329  }