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 }