@@ -18,22 +18,22 @@ package network
18
18
19
19
import (
20
20
"context"
21
- "encoding/json "
21
+ "encoding/hex "
22
22
"fmt"
23
23
"math"
24
+ "net"
24
25
"strconv"
25
26
"strings"
26
27
"time"
27
28
28
29
v1 "k8s.io/api/core/v1"
29
30
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
31
+ "k8s.io/apimachinery/pkg/util/wait"
30
32
31
33
"k8s.io/kubernetes/test/e2e/framework"
32
34
e2enode "k8s.io/kubernetes/test/e2e/framework/node"
33
35
e2epod "k8s.io/kubernetes/test/e2e/framework/pod"
34
36
e2eskipper "k8s.io/kubernetes/test/e2e/framework/skipper"
35
- e2essh "k8s.io/kubernetes/test/e2e/framework/ssh"
36
- "k8s.io/kubernetes/test/images/agnhost/net/nat"
37
37
imageutils "k8s.io/kubernetes/test/utils/image"
38
38
39
39
"github.com/onsi/ginkgo"
@@ -44,15 +44,15 @@ var kubeProxyE2eImage = imageutils.GetE2EImage(imageutils.Agnhost)
44
44
45
45
var _ = SIGDescribe ("Network" , func () {
46
46
const (
47
- testDaemonHTTPPort = 11301
48
- testDaemonTCPPort = 11302
49
- timeoutSeconds = 10
50
- postFinTimeoutSeconds = 5
47
+ testDaemonHTTPPort = 11301
48
+ testDaemonTCPPort = 11302
49
+ deadlineTimeoutSeconds = 5
50
+ postFinTimeoutSeconds = 15
51
51
)
52
52
53
53
fr := framework .NewDefaultFramework ("network" )
54
54
55
- ginkgo .It ("should set TCP CLOSE_WAIT timeout" , func () {
55
+ ginkgo .It ("should set TCP CLOSE_WAIT timeout [Privileged] " , func () {
56
56
nodes , err := e2enode .GetBoundedReadySchedulableNodes (fr .ClientSet , 2 )
57
57
framework .ExpectNoError (err )
58
58
if len (nodes .Items ) < 2 {
@@ -83,16 +83,63 @@ var _ = SIGDescribe("Network", func() {
83
83
84
84
zero := int64 (0 )
85
85
86
+ // Create a pod to check the conntrack entries on the host node
87
+ // It mounts the host /proc/net folder to be able to access
88
+ // the nf_conntrack file with the host conntrack entries
89
+ privileged := true
90
+
91
+ hostExecPod := & v1.Pod {
92
+ ObjectMeta : metav1.ObjectMeta {
93
+ Name : "e2e-net-exec" ,
94
+ Namespace : fr .Namespace .Name ,
95
+ Labels : map [string ]string {"app" : "e2e-net-exec" },
96
+ },
97
+ Spec : v1.PodSpec {
98
+ HostNetwork : true ,
99
+ NodeName : clientNodeInfo .name ,
100
+ Containers : []v1.Container {
101
+ {
102
+ Name : "e2e-net-exec" ,
103
+ Image : kubeProxyE2eImage ,
104
+ ImagePullPolicy : "Always" ,
105
+ Args : []string {"pause" },
106
+ VolumeMounts : []v1.VolumeMount {
107
+ {
108
+ Name : "proc-net" ,
109
+ MountPath : "/rootfs/proc/net" ,
110
+ ReadOnly : true ,
111
+ },
112
+ },
113
+ SecurityContext : & v1.SecurityContext {
114
+ Privileged : & privileged ,
115
+ },
116
+ },
117
+ },
118
+ Volumes : []v1.Volume {
119
+ {
120
+ Name : "proc-net" ,
121
+ VolumeSource : v1.VolumeSource {
122
+ HostPath : & v1.HostPathVolumeSource {
123
+ Path : "/proc/net" ,
124
+ },
125
+ },
126
+ },
127
+ },
128
+ TerminationGracePeriodSeconds : & zero ,
129
+ },
130
+ }
131
+ fr .PodClient ().CreateSync (hostExecPod )
132
+ defer fr .PodClient ().DeleteSync (hostExecPod .Name , metav1.DeleteOptions {}, framework .DefaultPodDeletionTimeout )
133
+
86
134
// Some distributions (Ubuntu 16.04 etc.) don't support the proc file.
87
- _ , err = e2essh .IssueSSHCommandWithResult (
88
- "ls /proc/net/nf_conntrack" ,
89
- framework .TestContext .Provider ,
90
- clientNodeInfo .node )
135
+ _ , err = framework .RunHostCmd (fr .Namespace .Name , "e2e-net-exec" ,
136
+ "ls /rootfs/proc/net/nf_conntrack" )
91
137
if err != nil && strings .Contains (err .Error (), "No such file or directory" ) {
92
138
e2eskipper .Skipf ("The node %s does not support /proc/net/nf_conntrack" , clientNodeInfo .name )
93
139
}
94
140
framework .ExpectNoError (err )
95
141
142
+ // Create the client and server pods
96
143
clientPodSpec := & v1.Pod {
97
144
ObjectMeta : metav1.ObjectMeta {
98
145
Name : "e2e-net-client" ,
@@ -107,7 +154,13 @@ var _ = SIGDescribe("Network", func() {
107
154
Image : kubeProxyE2eImage ,
108
155
ImagePullPolicy : "Always" ,
109
156
Args : []string {
110
- "net" , "--serve" , fmt .Sprintf ("0.0.0.0:%d" , testDaemonHTTPPort ),
157
+ "net" ,
158
+ "--runner" , "nat-closewait-client" ,
159
+ "--options" ,
160
+ fmt .Sprintf (`{"RemoteAddr":"%v", "PostFinTimeoutSeconds":%v, "TimeoutSeconds":%v, "LeakConnection":true}` ,
161
+ net .JoinHostPort (serverNodeInfo .nodeIP , strconv .Itoa (testDaemonTCPPort )),
162
+ postFinTimeoutSeconds ,
163
+ deadlineTimeoutSeconds ),
111
164
},
112
165
},
113
166
},
@@ -132,7 +185,7 @@ var _ = SIGDescribe("Network", func() {
132
185
"net" ,
133
186
"--runner" , "nat-closewait-server" ,
134
187
"--options" ,
135
- fmt .Sprintf (`{"LocalAddr":"0.0.0.0 :%v", "PostFindTimeoutSeconds ":%v}` ,
188
+ fmt .Sprintf (`{"LocalAddr":":%v", "PostFinTimeoutSeconds ":%v}` ,
136
189
testDaemonTCPPort ,
137
190
postFinTimeoutSeconds ),
138
191
},
@@ -156,72 +209,55 @@ var _ = SIGDescribe("Network", func() {
156
209
kubeProxyE2eImage ))
157
210
fr .PodClient ().CreateSync (serverPodSpec )
158
211
212
+ // The server should be listening before spawning the client pod
213
+ <- time .After (time .Duration (2 ) * time .Second )
214
+ // Connect to the server and leak the connection
159
215
ginkgo .By (fmt .Sprintf (
160
- "Launching a client daemon on node %v (node ip: %v, image: %v)" ,
216
+ "Launching a client connection on node %v (node ip: %v, image: %v)" ,
161
217
clientNodeInfo .name ,
162
218
clientNodeInfo .nodeIP ,
163
219
kubeProxyE2eImage ))
164
220
fr .PodClient ().CreateSync (clientPodSpec )
165
221
166
- ginkgo .By ("Make client connect" )
167
-
168
- options := nat.CloseWaitClientOptions {
169
- RemoteAddr : fmt .Sprintf ("%v:%v" ,
170
- serverNodeInfo .nodeIP , testDaemonTCPPort ),
171
- TimeoutSeconds : timeoutSeconds ,
172
- PostFinTimeoutSeconds : 0 ,
173
- LeakConnection : true ,
174
- }
175
-
176
- jsonBytes , err := json .Marshal (options )
177
- framework .ExpectNoError (err , "could not marshal" )
178
-
179
- cmd := fmt .Sprintf (
180
- `curl -X POST http://localhost:%v/run/nat-closewait-client -d ` +
181
- `'%v' 2>/dev/null` ,
182
- testDaemonHTTPPort ,
183
- string (jsonBytes ))
184
- framework .RunHostCmdOrDie (fr .Namespace .Name , "e2e-net-client" , cmd )
185
-
186
- <- time .After (time .Duration (1 ) * time .Second )
187
-
188
222
ginkgo .By ("Checking /proc/net/nf_conntrack for the timeout" )
189
- // If test flakes occur here, then this check should be performed
190
- // in a loop as there may be a race with the client connecting.
191
- e2essh .IssueSSHCommandWithResult (
192
- fmt .Sprintf ("sudo cat /proc/net/nf_conntrack | grep 'dport=%v'" ,
193
- testDaemonTCPPort ),
194
- framework .TestContext .Provider ,
195
- clientNodeInfo .node )
196
-
197
- // Timeout in seconds is available as the fifth column from
198
- // /proc/net/nf_conntrack.
199
- result , err := e2essh .IssueSSHCommandWithResult (
200
- fmt .Sprintf (
201
- "sudo cat /proc/net/nf_conntrack " +
202
- "| grep 'CLOSE_WAIT.*dst=%v.*dport=%v' " +
203
- "| tail -n 1" +
204
- "| awk '{print $5}' " ,
205
- serverNodeInfo .nodeIP ,
206
- testDaemonTCPPort ),
207
- framework .TestContext .Provider ,
208
- clientNodeInfo .node )
209
- framework .ExpectNoError (err )
210
-
211
- timeoutSeconds , err := strconv .Atoi (strings .TrimSpace (result .Stdout ))
212
- framework .ExpectNoError (err )
213
-
214
223
// These must be synchronized from the default values set in
215
224
// pkg/apis/../defaults.go ConntrackTCPCloseWaitTimeout. The
216
225
// current defaults are hidden in the initialization code.
217
226
const epsilonSeconds = 60
218
227
const expectedTimeoutSeconds = 60 * 60
219
-
220
- framework .Logf ("conntrack entry timeout was: %v, expected: %v" ,
221
- timeoutSeconds , expectedTimeoutSeconds )
222
-
223
- gomega .Expect (math .Abs (float64 (timeoutSeconds - expectedTimeoutSeconds ))).Should (
224
- gomega .BeNumerically ("<" , (epsilonSeconds )))
228
+ // the conntrack file uses the IPv6 expanded format
229
+ ip := fullIPv6 (net .ParseIP (serverNodeInfo .nodeIP ))
230
+ // Obtain the corresponding conntrack entry on the host checking
231
+ // the nf_conntrack file from the pod e2e-net-exec.
232
+ // It retries in a loop if the entry is not found.
233
+ cmd := fmt .Sprintf ("cat /rootfs/proc/net/nf_conntrack " +
234
+ "| grep -m 1 'CLOSE_WAIT.*dst=%v.*dport=%v' " ,
235
+ ip , testDaemonTCPPort )
236
+ if err := wait .PollImmediate (deadlineTimeoutSeconds , postFinTimeoutSeconds , func () (bool , error ) {
237
+ result , err := framework .RunHostCmd (fr .Namespace .Name , "e2e-net-exec" , cmd )
238
+ // retry if we can't obtain the conntrack entry
239
+ if err != nil {
240
+ framework .Logf ("failed to obtain conntrack entry: %v %v" , result , err )
241
+ return false , nil
242
+ }
243
+ framework .Logf ("conntrack entry for node %v and port %v: %v" , serverNodeInfo .nodeIP , testDaemonTCPPort , result )
244
+ // Timeout in seconds is available as the fifth column of
245
+ // the matched entry in /proc/net/nf_conntrack.
246
+ line := strings .Fields (result )
247
+ if len (line ) < 5 {
248
+ return false , fmt .Errorf ("conntrack entry does not have a timeout field: %v" , line )
249
+ }
250
+ timeoutSeconds , err := strconv .Atoi (line [4 ])
251
+ if err != nil {
252
+ return false , fmt .Errorf ("failed to convert matched timeout %s to integer: %v" , line [4 ], err )
253
+ }
254
+ if math .Abs (float64 (timeoutSeconds - expectedTimeoutSeconds )) < epsilonSeconds {
255
+ return true , nil
256
+ }
257
+ return false , fmt .Errorf ("wrong TCP CLOSE_WAIT timeout: %v expected: %v" , timeoutSeconds , expectedTimeoutSeconds )
258
+ }); err != nil {
259
+ framework .Failf ("no conntrack entry for port %d on node %s" , testDaemonTCPPort , serverNodeInfo .nodeIP )
260
+ }
225
261
})
226
262
227
263
// Regression test for #74839, where:
@@ -338,3 +374,22 @@ var _ = SIGDescribe("Network", func() {
338
374
}
339
375
})
340
376
})
377
+
378
+ // fullIPv6 returns a string with the IP representation
379
+ // if IPv6 it returns the expanded address format
380
+ // credit https://stackoverflow.com/a/52003106/4532704
381
+ func fullIPv6 (ip net.IP ) string {
382
+ if ip .To4 () == nil {
383
+ dst := make ([]byte , hex .EncodedLen (len (ip )))
384
+ _ = hex .Encode (dst , ip )
385
+ return string (dst [0 :4 ]) + ":" +
386
+ string (dst [4 :8 ]) + ":" +
387
+ string (dst [8 :12 ]) + ":" +
388
+ string (dst [12 :16 ]) + ":" +
389
+ string (dst [16 :20 ]) + ":" +
390
+ string (dst [20 :24 ]) + ":" +
391
+ string (dst [24 :28 ]) + ":" +
392
+ string (dst [28 :])
393
+ }
394
+ return ip .String ()
395
+ }
0 commit comments