istio.io/istio@v0.0.0-20240520182934-d79c90f27776/cni/pkg/plugin/plugin_test.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package plugin
    16  
    17  import (
    18  	"fmt"
    19  	"net/http"
    20  	"net/http/httptest"
    21  	"reflect"
    22  	"testing"
    23  
    24  	"github.com/containernetworking/cni/pkg/skel"
    25  	corev1 "k8s.io/api/core/v1"
    26  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  	"k8s.io/apimachinery/pkg/runtime"
    28  
    29  	"istio.io/api/annotation"
    30  	"istio.io/api/label"
    31  	"istio.io/istio/pkg/config/constants"
    32  	"istio.io/istio/pkg/kube"
    33  	"istio.io/istio/pkg/test/util/assert"
    34  )
    35  
    36  const (
    37  	testPodName          = "testPod"
    38  	testNSName           = "testNS"
    39  	testSandboxDirectory = "/tmp"
    40  	invalidVersion       = "0.1.0"
    41  	preVersion           = "0.2.0"
    42  )
    43  
    44  var mockConfTmpl = `{
    45      "cniVersion": "%s",
    46  	"name": "istio-plugin-sample-test",
    47  	"type": "sample",
    48      "capabilities": {
    49          "testCapability": false
    50      },
    51      "ipam": {
    52          "type": "testIPAM"
    53      },
    54      "dns": {
    55          "nameservers": ["testNameServer"],
    56          "domain": "testDomain",
    57          "search": ["testSearch"],
    58          "options": ["testOption"]
    59      },
    60      "prevResult": {
    61          "cniversion": "%s",
    62          "interfaces": [
    63              {
    64                  "name": "%s",
    65                  "sandbox": "%s"
    66              }
    67          ],
    68          "ips": [
    69              {
    70                  "version": "4",
    71                  "address": "10.0.0.2/24",
    72                  "gateway": "10.0.0.1",
    73                  "interface": 0
    74              }
    75          ],
    76          "routes": []
    77  
    78      },
    79      "log_level": "debug",
    80      "cni_event_address": "%s",
    81      "ambient_enabled": %t,
    82      "kubernetes": {
    83          "k8s_api_root": "APIRoot",
    84          "kubeconfig": "testK8sConfig",
    85  		"intercept_type": "%s",
    86          "node_name": "testNodeName",
    87          "exclude_namespaces": ["testExcludeNS"],
    88          "cni_bin_dir": "/testDirectory"
    89      }
    90  }`
    91  
    92  type mockInterceptRuleMgr struct {
    93  	lastRedirect []*Redirect
    94  }
    95  
    96  func buildMockConf(ambientEnabled bool, eventURL string) string {
    97  	return fmt.Sprintf(
    98  		mockConfTmpl,
    99  		"1.0.0",
   100  		"1.0.0",
   101  		"eth0",
   102  		testSandboxDirectory,
   103  		eventURL,
   104  		ambientEnabled,
   105  		"mock",
   106  	)
   107  }
   108  
   109  func buildFakePodAndNSForClient() (*corev1.Pod, *corev1.Namespace) {
   110  	proxy := corev1.Container{Name: "mockContainer"}
   111  	app := corev1.Container{Name: "foo-init"}
   112  	fakePod := &corev1.Pod{
   113  		TypeMeta: metav1.TypeMeta{
   114  			APIVersion: "core/v1",
   115  			Kind:       "Pod",
   116  		},
   117  		ObjectMeta: metav1.ObjectMeta{
   118  			Name:        testPodName,
   119  			Namespace:   testNSName,
   120  			Annotations: map[string]string{},
   121  		},
   122  		Spec: corev1.PodSpec{
   123  			Containers: []corev1.Container{app, proxy},
   124  		},
   125  	}
   126  
   127  	fakeNS := &corev1.Namespace{
   128  		TypeMeta: metav1.TypeMeta{
   129  			APIVersion: "core/v1",
   130  			Kind:       "Namespace",
   131  		},
   132  		ObjectMeta: metav1.ObjectMeta{
   133  			Name:      testNSName,
   134  			Namespace: "",
   135  			Labels:    map[string]string{},
   136  		},
   137  	}
   138  
   139  	return fakePod, fakeNS
   140  }
   141  
   142  func (mrdir *mockInterceptRuleMgr) Program(podName, netns string, redirect *Redirect) error {
   143  	mrdir.lastRedirect = append(mrdir.lastRedirect, redirect)
   144  	return nil
   145  }
   146  
   147  // returns the test server URL and a dispose func for the test server
   148  func setupCNIEventClientWithMockServer(serverErr bool) (string, func() bool) {
   149  	cniAddServerCalled := false
   150  	// replace the global CNI client with mock
   151  	newCNIClient = func(address, path string) CNIEventClient {
   152  		c := http.DefaultClient
   153  
   154  		eventC := CNIEventClient{
   155  			client: c,
   156  			url:    address + path,
   157  		}
   158  		return eventC
   159  	}
   160  
   161  	testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
   162  		cniAddServerCalled = true
   163  		if serverErr {
   164  			res.WriteHeader(http.StatusInternalServerError)
   165  			res.Write([]byte("server not happy"))
   166  			return
   167  		}
   168  		res.WriteHeader(http.StatusOK)
   169  		res.Write([]byte("server happy"))
   170  	}))
   171  
   172  	return testServer.URL, func() bool {
   173  		testServer.Close()
   174  		return cniAddServerCalled
   175  	}
   176  }
   177  
   178  func buildCmdArgs(stdinData, podName, podNamespace string) *skel.CmdArgs {
   179  	return &skel.CmdArgs{
   180  		ContainerID: "testContainerID",
   181  		Netns:       testSandboxDirectory,
   182  		IfName:      "eth0",
   183  		Args:        fmt.Sprintf("K8S_POD_NAMESPACE=%s;K8S_POD_NAME=%s", podNamespace, podName),
   184  		Path:        "/tmp",
   185  		StdinData:   []byte(stdinData),
   186  	}
   187  }
   188  
   189  func testCmdAddExpectFail(t *testing.T, stdinData string, objects ...runtime.Object) *mockInterceptRuleMgr {
   190  	args := buildCmdArgs(stdinData, testPodName, testNSName)
   191  
   192  	conf, err := parseConfig(args.StdinData)
   193  	if err != nil {
   194  		t.Fatalf("config parse failed with error: %v", err)
   195  	}
   196  
   197  	// Create a kube client
   198  	client := kube.NewFakeClient(objects...)
   199  
   200  	mockRedir := &mockInterceptRuleMgr{}
   201  	err = doAddRun(args, conf, client.Kube(), mockRedir)
   202  	if err == nil {
   203  		t.Fatal("expected to fail, but did not!")
   204  	}
   205  
   206  	return mockRedir
   207  }
   208  
   209  func testDoAddRun(t *testing.T, stdinData, nsName string, objects ...runtime.Object) *mockInterceptRuleMgr {
   210  	args := buildCmdArgs(stdinData, testPodName, nsName)
   211  
   212  	conf, err := parseConfig(args.StdinData)
   213  	if err != nil {
   214  		t.Fatalf("config parse failed with error: %v", err)
   215  	}
   216  
   217  	// Create a kube client
   218  	client := kube.NewFakeClient(objects...)
   219  
   220  	mockRedir := &mockInterceptRuleMgr{}
   221  	err = doAddRun(args, conf, client.Kube(), mockRedir)
   222  	if err != nil {
   223  		t.Fatalf("failed with error: %v", err)
   224  	}
   225  
   226  	return mockRedir
   227  }
   228  
   229  func TestCmdAddAmbientEnabledOnNS(t *testing.T) {
   230  	url, serverClose := setupCNIEventClientWithMockServer(false)
   231  
   232  	cniConf := buildMockConf(true, url)
   233  
   234  	pod, ns := buildFakePodAndNSForClient()
   235  	ns.ObjectMeta.Labels = map[string]string{constants.DataplaneModeLabel: constants.DataplaneModeAmbient}
   236  
   237  	testDoAddRun(t, cniConf, testNSName, pod, ns)
   238  
   239  	wasCalled := serverClose()
   240  	// Pod in namespace with enabled ambient label, should be added to mesh
   241  	assert.Equal(t, wasCalled, true)
   242  }
   243  
   244  func TestCmdAddAmbientEnabledOnNSServerFails(t *testing.T) {
   245  	url, serverClose := setupCNIEventClientWithMockServer(true)
   246  
   247  	cniConf := buildMockConf(true, url)
   248  
   249  	pod, ns := buildFakePodAndNSForClient()
   250  	ns.ObjectMeta.Labels = map[string]string{constants.DataplaneModeLabel: constants.DataplaneModeAmbient}
   251  
   252  	testCmdAddExpectFail(t, cniConf, pod, ns)
   253  
   254  	wasCalled := serverClose()
   255  	// server called, but errored
   256  	assert.Equal(t, wasCalled, true)
   257  }
   258  
   259  func TestCmdAddPodWithProxySidecarAmbientEnabledNS(t *testing.T) {
   260  	url, serverClose := setupCNIEventClientWithMockServer(false)
   261  
   262  	cniConf := buildMockConf(true, url)
   263  
   264  	pod, ns := buildFakePodAndNSForClient()
   265  
   266  	proxy := corev1.Container{Name: "istio-proxy"}
   267  	app := corev1.Container{Name: "app"}
   268  
   269  	pod.Spec.Containers = []corev1.Container{app, proxy}
   270  	pod.ObjectMeta.Annotations = map[string]string{annotation.SidecarStatus.Name: "true"}
   271  	ns.ObjectMeta.Labels = map[string]string{constants.DataplaneModeLabel: constants.DataplaneModeAmbient}
   272  
   273  	testDoAddRun(t, cniConf, testNSName, pod, ns)
   274  
   275  	wasCalled := serverClose()
   276  	// Pod has sidecar annotation from injector, should not be added to mesh
   277  	assert.Equal(t, wasCalled, false)
   278  }
   279  
   280  func TestCmdAddPodWithGenericSidecar(t *testing.T) {
   281  	url, serverClose := setupCNIEventClientWithMockServer(false)
   282  
   283  	cniConf := buildMockConf(true, url)
   284  
   285  	pod, ns := buildFakePodAndNSForClient()
   286  
   287  	proxy := corev1.Container{Name: "istio-proxy"}
   288  	app := corev1.Container{Name: "app"}
   289  
   290  	pod.Spec.Containers = []corev1.Container{app, proxy}
   291  	ns.ObjectMeta.Labels = map[string]string{constants.DataplaneModeLabel: constants.DataplaneModeAmbient}
   292  
   293  	testDoAddRun(t, cniConf, testNSName, pod, ns)
   294  
   295  	wasCalled := serverClose()
   296  	// Pod should be added to ambient mesh
   297  	assert.Equal(t, wasCalled, true)
   298  }
   299  
   300  func TestCmdAddPodDisabledLabel(t *testing.T) {
   301  	url, serverClose := setupCNIEventClientWithMockServer(false)
   302  
   303  	cniConf := buildMockConf(true, url)
   304  
   305  	pod, ns := buildFakePodAndNSForClient()
   306  
   307  	app := corev1.Container{Name: "app"}
   308  	ns.ObjectMeta.Labels = map[string]string{constants.DataplaneModeLabel: constants.DataplaneModeAmbient}
   309  	pod.ObjectMeta.Labels = map[string]string{constants.DataplaneModeLabel: constants.DataplaneModeNone}
   310  	pod.Spec.Containers = []corev1.Container{app}
   311  
   312  	testDoAddRun(t, cniConf, testNSName, pod, ns)
   313  
   314  	wasCalled := serverClose()
   315  	// Pod has an explicit opt-out label, should not be added to ambient mesh
   316  	assert.Equal(t, wasCalled, false)
   317  }
   318  
   319  func TestCmdAddPodEnabledNamespaceDisabled(t *testing.T) {
   320  	url, serverClose := setupCNIEventClientWithMockServer(false)
   321  
   322  	cniConf := buildMockConf(true, url)
   323  
   324  	pod, ns := buildFakePodAndNSForClient()
   325  
   326  	app := corev1.Container{Name: "app"}
   327  	pod.ObjectMeta.Labels = map[string]string{constants.DataplaneModeLabel: constants.DataplaneModeAmbient}
   328  	pod.Spec.Containers = []corev1.Container{app}
   329  
   330  	testDoAddRun(t, cniConf, testNSName, pod, ns)
   331  
   332  	wasCalled := serverClose()
   333  	assert.Equal(t, wasCalled, true)
   334  }
   335  
   336  func TestCmdAddPodInExcludedNamespace(t *testing.T) {
   337  	url, serverClose := setupCNIEventClientWithMockServer(false)
   338  
   339  	cniConf := buildMockConf(true, url)
   340  
   341  	excludedNS := "testExcludeNS"
   342  	pod, ns := buildFakePodAndNSForClient()
   343  
   344  	app := corev1.Container{Name: "app"}
   345  	ns.ObjectMeta.Name = excludedNS
   346  	ns.ObjectMeta.Labels = map[string]string{constants.DataplaneModeLabel: constants.AmbientRedirectionEnabled}
   347  
   348  	pod.ObjectMeta.Namespace = excludedNS
   349  	pod.Spec.Containers = []corev1.Container{app}
   350  
   351  	testDoAddRun(t, cniConf, excludedNS, pod, ns)
   352  
   353  	wasCalled := serverClose()
   354  	// If the pod is being added to a namespace that is explicitly excluded by plugin config denylist
   355  	// it should never be added, even if the namespace has the annotation
   356  	assert.Equal(t, wasCalled, false)
   357  }
   358  
   359  func TestCmdAdd(t *testing.T) {
   360  	pod, ns := buildFakePodAndNSForClient()
   361  	testDoAddRun(t, buildMockConf(true, ""), testNSName, pod, ns)
   362  }
   363  
   364  func TestCmdAddTwoContainersWithAnnotation(t *testing.T) {
   365  	pod, ns := buildFakePodAndNSForClient()
   366  
   367  	pod.Spec.Containers[0].Name = "mockContainer"
   368  	pod.Spec.Containers[1].Name = "istio-proxy"
   369  	pod.ObjectMeta.Annotations[injectAnnotationKey] = "false"
   370  
   371  	testDoAddRun(t, buildMockConf(true, ""), testNSName, pod, ns)
   372  }
   373  
   374  func TestCmdAddTwoContainersWithLabel(t *testing.T) {
   375  	pod, ns := buildFakePodAndNSForClient()
   376  	pod.Spec.Containers[0].Name = "mockContainer"
   377  	pod.Spec.Containers[1].Name = "istio-proxy"
   378  	pod.ObjectMeta.Annotations[label.SidecarInject.Name] = "false"
   379  
   380  	testDoAddRun(t, buildMockConf(true, ""), testNSName, pod, ns)
   381  }
   382  
   383  func TestCmdAddTwoContainers(t *testing.T) {
   384  	pod, ns := buildFakePodAndNSForClient()
   385  
   386  	pod.Spec.Containers[0].Name = "mockContainer"
   387  	pod.Spec.Containers[1].Name = "istio-proxy"
   388  	pod.ObjectMeta.Annotations[sidecarStatusKey] = "true"
   389  
   390  	mockIntercept := testDoAddRun(t, buildMockConf(false, ""), testNSName, pod, ns)
   391  
   392  	if len(mockIntercept.lastRedirect) == 0 {
   393  		t.Fatalf("expected nsenterFunc to be called")
   394  	}
   395  	r := mockIntercept.lastRedirect[len(mockIntercept.lastRedirect)-1]
   396  	if r.includeInboundPorts != "*" {
   397  		t.Fatalf("expect includeInboundPorts has value '*' set by istio, actual %v", r.includeInboundPorts)
   398  	}
   399  }
   400  
   401  func TestCmdAddTwoContainersWithStarInboundPort(t *testing.T) {
   402  	pod, ns := buildFakePodAndNSForClient()
   403  
   404  	pod.Spec.Containers[0].Name = "mockContainer"
   405  	pod.Spec.Containers[1].Name = "istio-proxy"
   406  	pod.ObjectMeta.Annotations[sidecarStatusKey] = "true"
   407  	pod.ObjectMeta.Annotations[includeInboundPortsKey] = "*"
   408  
   409  	mockIntercept := testDoAddRun(t, buildMockConf(true, ""), testNSName, pod, ns)
   410  
   411  	if len(mockIntercept.lastRedirect) != 1 {
   412  		t.Fatalf("expected nsenterFunc to be called")
   413  	}
   414  	r := mockIntercept.lastRedirect[len(mockIntercept.lastRedirect)-1]
   415  	if r.includeInboundPorts != "*" {
   416  		t.Fatalf("expect includeInboundPorts is '*', actual %v", r.includeInboundPorts)
   417  	}
   418  }
   419  
   420  func TestCmdAddTwoContainersWithEmptyInboundPort(t *testing.T) {
   421  	pod, ns := buildFakePodAndNSForClient()
   422  
   423  	pod.Spec.Containers[0].Name = "mockContainer"
   424  	pod.Spec.Containers[1].Name = "istio-proxy"
   425  	pod.ObjectMeta.Annotations[sidecarStatusKey] = "true"
   426  	pod.ObjectMeta.Annotations[includeInboundPortsKey] = ""
   427  
   428  	mockIntercept := testDoAddRun(t, buildMockConf(true, ""), testNSName, pod, ns)
   429  
   430  	if len(mockIntercept.lastRedirect) != 1 {
   431  		t.Fatalf("expected nsenterFunc to be called")
   432  	}
   433  	r := mockIntercept.lastRedirect[len(mockIntercept.lastRedirect)-1]
   434  	if r.includeInboundPorts != "" {
   435  		t.Fatalf("expect includeInboundPorts is \"\", actual %v", r.includeInboundPorts)
   436  	}
   437  }
   438  
   439  func TestCmdAddTwoContainersWithEmptyExcludeInboundPort(t *testing.T) {
   440  	pod, ns := buildFakePodAndNSForClient()
   441  	pod.Spec.Containers[0].Name = "mockContainer"
   442  	pod.Spec.Containers[1].Name = "istio-proxy"
   443  	pod.ObjectMeta.Annotations[sidecarStatusKey] = "true"
   444  	pod.ObjectMeta.Annotations[excludeInboundPortsKey] = ""
   445  
   446  	mockIntercept := testDoAddRun(t, buildMockConf(true, ""), testNSName, pod, ns)
   447  
   448  	if len(mockIntercept.lastRedirect) != 1 {
   449  		t.Fatalf("expected nsenterFunc to be called")
   450  	}
   451  	r := mockIntercept.lastRedirect[len(mockIntercept.lastRedirect)-1]
   452  	if r.excludeInboundPorts != "15020,15021,15090" {
   453  		t.Fatalf("expect excludeInboundPorts is \"15090\", actual %v", r.excludeInboundPorts)
   454  	}
   455  }
   456  
   457  func TestCmdAddTwoContainersWithExplictExcludeInboundPort(t *testing.T) {
   458  	pod, ns := buildFakePodAndNSForClient()
   459  	pod.Spec.Containers[0].Name = "mockContainer"
   460  	pod.Spec.Containers[1].Name = "istio-proxy"
   461  	pod.ObjectMeta.Annotations[sidecarStatusKey] = "true"
   462  	pod.ObjectMeta.Annotations[excludeInboundPortsKey] = "3306"
   463  
   464  	mockIntercept := testDoAddRun(t, buildMockConf(true, ""), testNSName, pod, ns)
   465  
   466  	if len(mockIntercept.lastRedirect) == 0 {
   467  		t.Fatalf("expected nsenterFunc to be called")
   468  	}
   469  	r := mockIntercept.lastRedirect[len(mockIntercept.lastRedirect)-1]
   470  	if r.excludeInboundPorts != "3306,15020,15021,15090" {
   471  		t.Fatalf("expect excludeInboundPorts is \"3306,15090\", actual %v", r.excludeInboundPorts)
   472  	}
   473  }
   474  
   475  func TestCmdAddTwoContainersWithoutSideCar(t *testing.T) {
   476  	pod, ns := buildFakePodAndNSForClient()
   477  	pod.Spec.Containers[0].Name = "mockContainer"
   478  	pod.Spec.Containers[1].Name = "istio-proxy"
   479  
   480  	mockIntercept := testDoAddRun(t, buildMockConf(true, ""), testNSName, pod, ns)
   481  
   482  	if len(mockIntercept.lastRedirect) != 0 {
   483  		t.Fatalf("Didnt Expect nsenterFunc to be called because this pod does not contain a sidecar")
   484  	}
   485  }
   486  
   487  func TestCmdAddExcludePod(t *testing.T) {
   488  	pod, ns := buildFakePodAndNSForClient()
   489  
   490  	mockIntercept := testDoAddRun(t, buildMockConf(true, ""), "testExcludeNS", pod, ns)
   491  	if len(mockIntercept.lastRedirect) != 0 {
   492  		t.Fatalf("failed to exclude pod")
   493  	}
   494  }
   495  
   496  func TestCmdAddExcludePodWithIstioInitContainer(t *testing.T) {
   497  	pod, ns := buildFakePodAndNSForClient()
   498  	pod.ObjectMeta.Annotations[sidecarStatusKey] = "true"
   499  	pod.Spec.Containers = append(pod.Spec.Containers, corev1.Container{Name: "istio-init"})
   500  
   501  	mockIntercept := testDoAddRun(t, buildMockConf(true, ""), testNSName, pod, ns)
   502  
   503  	if len(mockIntercept.lastRedirect) != 0 {
   504  		t.Fatalf("failed to exclude pod")
   505  	}
   506  }
   507  
   508  func TestCmdAddExcludePodWithEnvoyDisableEnv(t *testing.T) {
   509  	pod, ns := buildFakePodAndNSForClient()
   510  	pod.ObjectMeta.Annotations[sidecarStatusKey] = "true"
   511  	pod.Spec.Containers = append(pod.Spec.Containers, corev1.Container{
   512  		Name: "istio-init",
   513  		Env:  []corev1.EnvVar{{Name: "DISABLE_ENVOY", Value: "true"}},
   514  	})
   515  
   516  	mockIntercept := testDoAddRun(t, buildMockConf(true, ""), testNSName, pod, ns)
   517  
   518  	if len(mockIntercept.lastRedirect) != 0 {
   519  		t.Fatalf("failed to exclude pod")
   520  	}
   521  }
   522  
   523  func TestCmdAddNoPrevResult(t *testing.T) {
   524  	confNoPrevResult := `{
   525      "cniVersion": "1.0.0",
   526  	"name": "istio-plugin-sample-test",
   527  	"type": "sample",
   528      "runtimeconfig": {
   529           "sampleconfig": []
   530      },
   531      "loglevel": "debug",
   532  	"ambient_enabled": %t,
   533      "kubernetes": {
   534          "k8sapiroot": "APIRoot",
   535          "kubeconfig": "testK8sConfig",
   536          "nodename": "testNodeName",
   537          "excludenamespaces": "testNS",
   538          "cnibindir": "/testDirectory"
   539      }
   540      }`
   541  
   542  	pod, ns := buildFakePodAndNSForClient()
   543  	testDoAddRun(t, fmt.Sprintf(confNoPrevResult, false), testNSName, pod, ns)
   544  	testDoAddRun(t, fmt.Sprintf(confNoPrevResult, true), testNSName, pod, ns)
   545  }
   546  
   547  func TestCmdAddEnableDualStack(t *testing.T) {
   548  	pod, ns := buildFakePodAndNSForClient()
   549  	pod.ObjectMeta.Annotations[sidecarStatusKey] = "true"
   550  	pod.Spec.Containers = []corev1.Container{
   551  		{
   552  			Name: "istio-proxy",
   553  			Env:  []corev1.EnvVar{{Name: "ISTIO_DUAL_STACK", Value: "true"}},
   554  		}, {Name: "mockContainer"},
   555  	}
   556  
   557  	mockIntercept := testDoAddRun(t, buildMockConf(true, ""), testNSName, pod, ns)
   558  
   559  	if len(mockIntercept.lastRedirect) == 0 {
   560  		t.Fatalf("expected nsenterFunc to be called")
   561  	}
   562  	r := mockIntercept.lastRedirect[len(mockIntercept.lastRedirect)-1]
   563  	if !r.dualStack {
   564  		t.Fatalf("expect dualStack is true, actual %v", r.dualStack)
   565  	}
   566  }
   567  
   568  func Test_dedupPorts(t *testing.T) {
   569  	type args struct {
   570  		ports []string
   571  	}
   572  	tests := []struct {
   573  		name string
   574  		args args
   575  		want []string
   576  	}{
   577  		{
   578  			name: "No duplicates",
   579  			args: args{ports: []string{"1234", "2345"}},
   580  			want: []string{"1234", "2345"},
   581  		},
   582  		{
   583  			name: "Sequential Duplicates",
   584  			args: args{ports: []string{"1234", "1234", "2345", "2345"}},
   585  			want: []string{"1234", "2345"},
   586  		},
   587  		{
   588  			name: "Mixed Duplicates",
   589  			args: args{ports: []string{"1234", "2345", "1234", "2345"}},
   590  			want: []string{"1234", "2345"},
   591  		},
   592  		{
   593  			name: "Empty",
   594  			args: args{ports: []string{}},
   595  			want: []string{},
   596  		},
   597  		{
   598  			name: "Non-parseable",
   599  			args: args{ports: []string{"abcd", "2345", "abcd"}},
   600  			want: []string{"abcd", "2345"},
   601  		},
   602  	}
   603  	for _, tt := range tests {
   604  		t.Run(tt.name, func(t *testing.T) {
   605  			if got := dedupPorts(tt.args.ports); !reflect.DeepEqual(got, tt.want) {
   606  				t.Errorf("dedupPorts() = %v, want %v", got, tt.want)
   607  			}
   608  		})
   609  	}
   610  }