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 }