k8s.io/kubernetes@v1.29.3/test/e2e/network/kube_proxy.go (about) 1 /* 2 Copyright 2016 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package network 18 19 import ( 20 "context" 21 "fmt" 22 "math" 23 "net" 24 "strconv" 25 "strings" 26 "time" 27 28 v1 "k8s.io/api/core/v1" 29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 "k8s.io/apimachinery/pkg/util/wait" 31 32 "k8s.io/kubernetes/test/e2e/framework" 33 e2enode "k8s.io/kubernetes/test/e2e/framework/node" 34 e2epod "k8s.io/kubernetes/test/e2e/framework/pod" 35 e2eoutput "k8s.io/kubernetes/test/e2e/framework/pod/output" 36 e2eskipper "k8s.io/kubernetes/test/e2e/framework/skipper" 37 "k8s.io/kubernetes/test/e2e/network/common" 38 imageutils "k8s.io/kubernetes/test/utils/image" 39 admissionapi "k8s.io/pod-security-admission/api" 40 netutils "k8s.io/utils/net" 41 42 "github.com/onsi/ginkgo/v2" 43 "github.com/onsi/gomega" 44 ) 45 46 var kubeProxyE2eImage = imageutils.GetE2EImage(imageutils.Agnhost) 47 48 var _ = common.SIGDescribe("KubeProxy", func() { 49 const ( 50 testDaemonTCPPort = 11302 51 postFinTimeoutSeconds = 30 52 ) 53 54 fr := framework.NewDefaultFramework("kube-proxy") 55 fr.NamespacePodSecurityLevel = admissionapi.LevelPrivileged 56 57 ginkgo.It("should set TCP CLOSE_WAIT timeout [Privileged]", func(ctx context.Context) { 58 nodes, err := e2enode.GetBoundedReadySchedulableNodes(ctx, fr.ClientSet, 2) 59 framework.ExpectNoError(err) 60 if len(nodes.Items) < 2 { 61 e2eskipper.Skipf( 62 "Test requires >= 2 Ready nodes, but there are only %v nodes", 63 len(nodes.Items)) 64 } 65 66 type NodeInfo struct { 67 node *v1.Node 68 name string 69 nodeIP string 70 } 71 72 var family v1.IPFamily 73 if framework.TestContext.ClusterIsIPv6() { 74 family = v1.IPv6Protocol 75 } else { 76 family = v1.IPv4Protocol 77 } 78 79 ips := e2enode.GetAddressesByTypeAndFamily(&nodes.Items[0], v1.NodeInternalIP, family) 80 gomega.Expect(ips).ToNot(gomega.BeEmpty()) 81 82 clientNodeInfo := NodeInfo{ 83 node: &nodes.Items[0], 84 name: nodes.Items[0].Name, 85 nodeIP: ips[0], 86 } 87 88 ips = e2enode.GetAddressesByTypeAndFamily(&nodes.Items[1], v1.NodeInternalIP, family) 89 gomega.Expect(ips).ToNot(gomega.BeEmpty()) 90 91 serverNodeInfo := NodeInfo{ 92 node: &nodes.Items[1], 93 name: nodes.Items[1].Name, 94 nodeIP: ips[0], 95 } 96 97 // Create a pod to check the conntrack entries on the host node 98 privileged := true 99 100 hostExecPod := &v1.Pod{ 101 ObjectMeta: metav1.ObjectMeta{ 102 Name: "e2e-net-exec", 103 Namespace: fr.Namespace.Name, 104 Labels: map[string]string{"app": "e2e-net-exec"}, 105 }, 106 Spec: v1.PodSpec{ 107 HostNetwork: true, 108 NodeName: clientNodeInfo.name, 109 Containers: []v1.Container{ 110 { 111 Name: "e2e-net-exec", 112 Image: imageutils.GetE2EImage(imageutils.DistrolessIptables), 113 ImagePullPolicy: v1.PullIfNotPresent, 114 Command: []string{"sleep", "600"}, 115 SecurityContext: &v1.SecurityContext{ 116 Privileged: &privileged, 117 }, 118 }, 119 }, 120 }, 121 } 122 e2epod.NewPodClient(fr).CreateSync(ctx, hostExecPod) 123 124 // Create the client and server pods 125 clientPodSpec := &v1.Pod{ 126 ObjectMeta: metav1.ObjectMeta{ 127 Name: "e2e-net-client", 128 Namespace: fr.Namespace.Name, 129 Labels: map[string]string{"app": "e2e-net-client"}, 130 }, 131 Spec: v1.PodSpec{ 132 NodeName: clientNodeInfo.name, 133 Containers: []v1.Container{ 134 { 135 Name: "e2e-net-client", 136 Image: kubeProxyE2eImage, 137 ImagePullPolicy: v1.PullIfNotPresent, 138 Args: []string{ 139 "net", 140 "--runner", "nat-closewait-client", 141 "--options", 142 fmt.Sprintf(`{"RemoteAddr":"%v", "PostFinTimeoutSeconds":%v, "TimeoutSeconds":%v, "LeakConnection":true}`, 143 net.JoinHostPort(serverNodeInfo.nodeIP, strconv.Itoa(testDaemonTCPPort)), 144 postFinTimeoutSeconds, 145 0), 146 }, 147 }, 148 }, 149 }, 150 } 151 152 serverPodSpec := &v1.Pod{ 153 ObjectMeta: metav1.ObjectMeta{ 154 Name: "e2e-net-server", 155 Namespace: fr.Namespace.Name, 156 Labels: map[string]string{"app": "e2e-net-server"}, 157 }, 158 Spec: v1.PodSpec{ 159 NodeName: serverNodeInfo.name, 160 Containers: []v1.Container{ 161 { 162 Name: "e2e-net-server", 163 Image: kubeProxyE2eImage, 164 ImagePullPolicy: v1.PullIfNotPresent, 165 Args: []string{ 166 "net", 167 "--runner", "nat-closewait-server", 168 "--options", 169 fmt.Sprintf(`{"LocalAddr":":%v", "PostFinTimeoutSeconds":%v}`, 170 testDaemonTCPPort, 171 postFinTimeoutSeconds), 172 }, 173 Ports: []v1.ContainerPort{ 174 { 175 Name: "tcp", 176 ContainerPort: testDaemonTCPPort, 177 HostPort: testDaemonTCPPort, 178 }, 179 }, 180 }, 181 }, 182 }, 183 } 184 185 ginkgo.By(fmt.Sprintf( 186 "Launching a server daemon on node %v (node ip: %v, image: %v)", 187 serverNodeInfo.name, 188 serverNodeInfo.nodeIP, 189 kubeProxyE2eImage)) 190 e2epod.NewPodClient(fr).CreateSync(ctx, serverPodSpec) 191 192 // The server should be listening before spawning the client pod 193 if readyErr := e2epod.WaitTimeoutForPodReadyInNamespace(ctx, fr.ClientSet, serverPodSpec.Name, fr.Namespace.Name, framework.PodStartTimeout); readyErr != nil { 194 framework.Failf("error waiting for server pod %s to be ready: %v", serverPodSpec.Name, readyErr) 195 } 196 // Connect to the server and leak the connection 197 ginkgo.By(fmt.Sprintf( 198 "Launching a client connection on node %v (node ip: %v, image: %v)", 199 clientNodeInfo.name, 200 clientNodeInfo.nodeIP, 201 kubeProxyE2eImage)) 202 e2epod.NewPodClient(fr).CreateSync(ctx, clientPodSpec) 203 204 ginkgo.By("Checking conntrack entries for the timeout") 205 // These must be synchronized from the default values set in 206 // pkg/apis/../defaults.go ConntrackTCPCloseWaitTimeout. The 207 // current defaults are hidden in the initialization code. 208 const epsilonSeconds = 60 209 const expectedTimeoutSeconds = 60 * 60 210 // the conntrack file uses the IPv6 expanded format 211 ip := serverNodeInfo.nodeIP 212 ipFamily := "ipv4" 213 if netutils.IsIPv6String(ip) { 214 ipFamily = "ipv6" 215 } 216 // Obtain the corresponding conntrack entry on the host checking 217 // the nf_conntrack file from the pod e2e-net-exec. 218 // It retries in a loop if the entry is not found. 219 cmd := fmt.Sprintf("conntrack -L -f %s -d %v "+ 220 "| grep -m 1 'CLOSE_WAIT.*dport=%v' ", 221 ipFamily, ip, testDaemonTCPPort) 222 if err := wait.PollImmediate(2*time.Second, epsilonSeconds*time.Second, func() (bool, error) { 223 result, err := e2eoutput.RunHostCmd(fr.Namespace.Name, "e2e-net-exec", cmd) 224 // retry if we can't obtain the conntrack entry 225 if err != nil { 226 framework.Logf("failed to obtain conntrack entry: %v %v", result, err) 227 return false, nil 228 } 229 framework.Logf("conntrack entry for node %v and port %v: %v", serverNodeInfo.nodeIP, testDaemonTCPPort, result) 230 // Timeout in seconds is available as the third column of the matched entry 231 line := strings.Fields(result) 232 if len(line) < 3 { 233 return false, fmt.Errorf("conntrack entry does not have a timeout field: %v", line) 234 } 235 timeoutSeconds, err := strconv.Atoi(line[2]) 236 if err != nil { 237 return false, fmt.Errorf("failed to convert matched timeout %s to integer: %w", line[2], err) 238 } 239 if math.Abs(float64(timeoutSeconds-expectedTimeoutSeconds)) < epsilonSeconds { 240 return true, nil 241 } 242 return false, fmt.Errorf("wrong TCP CLOSE_WAIT timeout: %v expected: %v", timeoutSeconds, expectedTimeoutSeconds) 243 }); err != nil { 244 // Dump all conntrack entries for debugging 245 result, err2 := e2eoutput.RunHostCmd(fr.Namespace.Name, "e2e-net-exec", "conntrack -L") 246 if err2 != nil { 247 framework.Logf("failed to obtain conntrack entry: %v %v", result, err2) 248 } 249 framework.Logf("conntrack entries for node %v: %v", serverNodeInfo.nodeIP, result) 250 framework.Failf("no valid conntrack entry for port %d on node %s: %v", testDaemonTCPPort, serverNodeInfo.nodeIP, err) 251 } 252 }) 253 254 })