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

     1  //go:build linux
     2  // +build linux
     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 plugin
    19  
    20  import (
    21  	"fmt"
    22  	"log"
    23  	"os"
    24  	"path/filepath"
    25  	"reflect"
    26  	"strings"
    27  	"testing"
    28  
    29  	"github.com/containernetworking/plugins/pkg/ns"
    30  	corev1 "k8s.io/api/core/v1"
    31  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  	"k8s.io/apimachinery/pkg/runtime"
    33  
    34  	"istio.io/api/annotation"
    35  	"istio.io/istio/pilot/cmd/pilot-agent/options"
    36  	diff "istio.io/istio/pilot/test/util"
    37  	"istio.io/istio/pkg/kube"
    38  	"istio.io/istio/pkg/maps"
    39  	"istio.io/istio/pkg/slices"
    40  	"istio.io/istio/pkg/test/env"
    41  	"istio.io/istio/tools/istio-iptables/pkg/cmd"
    42  	"istio.io/istio/tools/istio-iptables/pkg/dependencies"
    43  )
    44  
    45  type mockNetNs struct {
    46  	path string
    47  }
    48  
    49  func (ns *mockNetNs) Do(toRun func(ns.NetNS) error) error {
    50  	return toRun(ns)
    51  }
    52  
    53  func (*mockNetNs) Set() error {
    54  	return nil
    55  }
    56  
    57  func (ns *mockNetNs) Path() string {
    58  	return ns.path
    59  }
    60  
    61  func (*mockNetNs) Fd() uintptr {
    62  	return 0
    63  }
    64  
    65  func (*mockNetNs) Close() error {
    66  	return nil
    67  }
    68  
    69  type netNsFunc func(nspath string) (ns.NetNS, error)
    70  
    71  func generateMockGetNsFunc(netNs string) netNsFunc {
    72  	return func(nspath string) (ns.NetNS, error) {
    73  		return &mockNetNs{path: netNs}, nil
    74  	}
    75  }
    76  
    77  func buildDryrunConf() string {
    78  	return fmt.Sprintf(
    79  		mockConfTmpl,
    80  		"1.0.0",
    81  		"1.0.0",
    82  		"eth0",
    83  		testSandboxDirectory,
    84  		"",
    85  		false,
    86  		"iptables",
    87  	)
    88  }
    89  
    90  func TestIPTablesRuleGeneration(t *testing.T) {
    91  	cniConf := buildDryrunConf()
    92  
    93  	customUID := int64(1000670000)
    94  	customGID := int64(1000670001)
    95  	zero := int64(0)
    96  
    97  	tests := []struct {
    98  		name        string
    99  		annotations map[string]string
   100  		proxyEnv    []corev1.EnvVar
   101  		customUID   *int64
   102  		customGID   *int64
   103  		golden      string
   104  	}{
   105  		{
   106  			name:        "basic",
   107  			annotations: map[string]string{annotation.SidecarStatus.Name: "true"},
   108  			proxyEnv:    []corev1.EnvVar{},
   109  			golden:      filepath.Join(env.IstioSrc, "cni/pkg/plugin/testdata/basic.txt.golden"),
   110  		},
   111  		{
   112  			name: "include-exclude-ip",
   113  			annotations: map[string]string{
   114  				annotation.SidecarStatus.Name:                         "true",
   115  				annotation.SidecarTrafficIncludeOutboundIPRanges.Name: "127.0.0.0/8",
   116  				annotation.SidecarTrafficExcludeOutboundIPRanges.Name: "10.0.0.0/8",
   117  			},
   118  			proxyEnv: []corev1.EnvVar{},
   119  			golden:   filepath.Join(env.IstioSrc, "cni/pkg/plugin/testdata/include-exclude-ip.txt.golden"),
   120  		},
   121  		{
   122  			name: "include-exclude-ports",
   123  			annotations: map[string]string{
   124  				annotation.SidecarStatus.Name:                      "true",
   125  				annotation.SidecarTrafficIncludeInboundPorts.Name:  "1111,2222",
   126  				annotation.SidecarTrafficExcludeInboundPorts.Name:  "3333,4444",
   127  				annotation.SidecarTrafficExcludeOutboundPorts.Name: "5555,6666",
   128  			},
   129  			proxyEnv: []corev1.EnvVar{},
   130  			golden:   filepath.Join(env.IstioSrc, "cni/pkg/plugin/testdata/include-exclude-ports.txt.golden"),
   131  		},
   132  		{
   133  			name: "tproxy",
   134  			annotations: map[string]string{
   135  				annotation.SidecarStatus.Name:           "true",
   136  				annotation.SidecarInterceptionMode.Name: redirectModeTPROXY,
   137  			},
   138  			proxyEnv: []corev1.EnvVar{},
   139  			golden:   filepath.Join(env.IstioSrc, "cni/pkg/plugin/testdata/tproxy.txt.golden"),
   140  		},
   141  		{
   142  			name:        "DNS",
   143  			annotations: map[string]string{annotation.SidecarStatus.Name: "true"},
   144  			proxyEnv:    []corev1.EnvVar{{Name: options.DNSCaptureByAgent.Name, Value: "true"}},
   145  			golden:      filepath.Join(env.IstioSrc, "cni/pkg/plugin/testdata/dns.txt.golden"),
   146  		},
   147  		{
   148  			name:        "invalid-drop",
   149  			annotations: map[string]string{annotation.SidecarStatus.Name: "true"},
   150  			proxyEnv:    []corev1.EnvVar{{Name: cmd.InvalidDropByIptables, Value: "true"}},
   151  			golden:      filepath.Join(env.IstioSrc, "cni/pkg/plugin/testdata/invalid-drop.txt.golden"),
   152  		},
   153  		{
   154  			name:        "custom-uid",
   155  			annotations: map[string]string{annotation.SidecarStatus.Name: "true"},
   156  			customUID:   &customUID,
   157  			customGID:   &customGID,
   158  			golden:      filepath.Join(env.IstioSrc, "cni/pkg/plugin/testdata/custom-uid.txt.golden"),
   159  		},
   160  		{
   161  			name:        "custom-uid-zero",
   162  			annotations: map[string]string{annotation.SidecarStatus.Name: "true"},
   163  			customUID:   &zero,
   164  			golden:      filepath.Join(env.IstioSrc, "cni/pkg/plugin/testdata/basic.txt.golden"),
   165  		},
   166  		{
   167  			name: "custom-uid-tproxy",
   168  			annotations: map[string]string{
   169  				annotation.SidecarStatus.Name:           "true",
   170  				annotation.SidecarInterceptionMode.Name: redirectModeTPROXY,
   171  			},
   172  			customUID: &customUID,
   173  			customGID: &customGID,
   174  			golden:    filepath.Join(env.IstioSrc, "cni/pkg/plugin/testdata/custom-uid-tproxy.txt.golden"),
   175  		},
   176  	}
   177  
   178  	for _, tt := range tests {
   179  		t.Run(tt.name, func(t *testing.T) {
   180  			// TODO(bianpengyuan): How do we test ipv6 rules?
   181  			getNs = generateMockGetNsFunc(testSandboxDirectory)
   182  			tmpDir := t.TempDir()
   183  			outputFilePath := filepath.Join(tmpDir, "output.txt")
   184  			if _, err := os.Create(outputFilePath); err != nil {
   185  				t.Fatalf("Failed to create temp file for IPTables rule output: %v", err)
   186  			}
   187  			t.Setenv(dependencies.DryRunFilePath.Name, outputFilePath)
   188  
   189  			pod := buildFakeDryRunPod()
   190  			pod.ObjectMeta.Annotations = tt.annotations
   191  			pod.Spec.Containers[1].Env = tt.proxyEnv
   192  
   193  			pod.Spec.Containers[1].SecurityContext = &corev1.SecurityContext{}
   194  
   195  			if tt.customGID != nil {
   196  				pod.Spec.Containers[1].SecurityContext.RunAsGroup = tt.customGID
   197  			}
   198  
   199  			if tt.customUID != nil {
   200  				pod.Spec.Containers[1].SecurityContext.RunAsUser = tt.customUID
   201  			}
   202  
   203  			testdoAddRunWithIptablesIntercept(t, cniConf, testPodName, testNSName, pod)
   204  
   205  			generated, err := os.ReadFile(outputFilePath)
   206  			if err != nil {
   207  				log.Fatalf("Cannot read generated IPTables rule file: %v", err)
   208  			}
   209  			generatedRules := getRules(generated)
   210  
   211  			refreshGoldens(t, tt.golden, generatedRules)
   212  
   213  			// Compare generated iptables rule with golden files.
   214  			golden, err := os.ReadFile(tt.golden)
   215  			if err != nil {
   216  				log.Fatalf("Cannot read golden rule file: %v", err)
   217  			}
   218  			goldenRules := getRules(golden)
   219  
   220  			if len(generatedRules) == 0 {
   221  				t.Error("Got empty generated rules")
   222  			}
   223  			if !reflect.DeepEqual(generatedRules, goldenRules) {
   224  				t.Errorf("Unexpected IPtables rules generated, want \n%v \ngot \n%v", goldenRules, generatedRules)
   225  			}
   226  		})
   227  	}
   228  }
   229  
   230  func getRules(b []byte) map[string]string {
   231  	// Separate content with "COMMIT"
   232  	parts := strings.Split(string(b), "COMMIT")
   233  	tables := make(map[string]string)
   234  	for _, table := range parts {
   235  		// If table is not empty, get table name from the first line
   236  		lines := strings.Split(strings.Trim(table, "\n"), "\n")
   237  		if len(lines) >= 1 && strings.HasPrefix(lines[0], "* ") {
   238  			tableName := lines[0][2:]
   239  			lines = append(lines, "COMMIT")
   240  			tables[tableName] = strings.Join(lines, "\n")
   241  		}
   242  	}
   243  	return tables
   244  }
   245  
   246  func refreshGoldens(t *testing.T, goldenFileName string, generatedRules map[string]string) {
   247  	tables := slices.Sort(maps.Keys(generatedRules))
   248  	goldenFileContent := ""
   249  	for _, t := range tables {
   250  		goldenFileContent += generatedRules[t] + "\n"
   251  	}
   252  	diff.RefreshGoldenFile(t, []byte(goldenFileContent), goldenFileName)
   253  }
   254  
   255  func testdoAddRunWithIptablesIntercept(t *testing.T, stdinData, testPodName, testNSName string, objects ...runtime.Object) {
   256  	args := buildCmdArgs(stdinData, testPodName, testNSName)
   257  
   258  	conf, err := parseConfig(args.StdinData)
   259  	if err != nil {
   260  		t.Fatalf("config parse failed with error: %v", err)
   261  	}
   262  
   263  	// Create a kube client
   264  	client := kube.NewFakeClient(objects...)
   265  
   266  	err = doAddRun(args, conf, client.Kube(), IptablesInterceptRuleMgr())
   267  	if err != nil {
   268  		t.Fatalf("failed with error: %v", err)
   269  	}
   270  }
   271  
   272  func buildFakeDryRunPod() *corev1.Pod {
   273  	app := corev1.Container{Name: "test"}
   274  	proxy := corev1.Container{Name: "istio-proxy"}
   275  	validate := corev1.Container{Name: "istio-validate"}
   276  	fakePod := &corev1.Pod{
   277  		TypeMeta: metav1.TypeMeta{
   278  			APIVersion: "core/v1",
   279  			Kind:       "Pod",
   280  		},
   281  		ObjectMeta: metav1.ObjectMeta{
   282  			Name:        testPodName,
   283  			Namespace:   testNSName,
   284  			Annotations: map[string]string{},
   285  		},
   286  		Spec: corev1.PodSpec{
   287  			Containers: []corev1.Container{app, proxy, validate},
   288  		},
   289  	}
   290  
   291  	return fakePod
   292  }