no_snat.go 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. /*
  2. Copyright 2017 The Kubernetes Authors.
  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. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package network
  14. import (
  15. "fmt"
  16. "io/ioutil"
  17. "net/http"
  18. "strconv"
  19. "strings"
  20. "time"
  21. "k8s.io/api/core/v1"
  22. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  23. "k8s.io/apimachinery/pkg/util/wait"
  24. "k8s.io/kubernetes/test/e2e/framework"
  25. "github.com/onsi/ginkgo"
  26. imageutils "k8s.io/kubernetes/test/utils/image"
  27. )
  28. const (
  29. testPodPort = 8080
  30. testProxyPort = 31235 // Firewall rule allows external traffic on ports 30000-32767. I just picked a random one.
  31. )
  32. var testPodImage = imageutils.GetE2EImage(imageutils.NoSnatTest)
  33. var testProxyImage = imageutils.GetE2EImage(imageutils.NoSnatTestProxy)
  34. var (
  35. testPod = v1.Pod{
  36. ObjectMeta: metav1.ObjectMeta{
  37. GenerateName: "no-snat-test",
  38. Labels: map[string]string{
  39. "no-snat-test": "",
  40. },
  41. },
  42. Spec: v1.PodSpec{
  43. Containers: []v1.Container{
  44. {
  45. Name: "no-snat-test",
  46. Image: testPodImage,
  47. Args: []string{"--port", strconv.Itoa(testPodPort)},
  48. Env: []v1.EnvVar{
  49. {
  50. Name: "POD_IP",
  51. ValueFrom: &v1.EnvVarSource{FieldRef: &v1.ObjectFieldSelector{FieldPath: "status.podIP"}},
  52. },
  53. },
  54. },
  55. },
  56. },
  57. }
  58. testProxyPod = v1.Pod{
  59. ObjectMeta: metav1.ObjectMeta{
  60. Name: "no-snat-test-proxy",
  61. },
  62. Spec: v1.PodSpec{
  63. HostNetwork: true,
  64. Containers: []v1.Container{
  65. {
  66. Name: "no-snat-test-proxy",
  67. Image: testProxyImage,
  68. Args: []string{"--port", strconv.Itoa(testProxyPort)},
  69. Ports: []v1.ContainerPort{
  70. {
  71. ContainerPort: testProxyPort,
  72. HostPort: testProxyPort,
  73. },
  74. },
  75. },
  76. },
  77. },
  78. }
  79. )
  80. // Produces a pod spec that passes nip as NODE_IP env var using downward API
  81. func newTestPod(nodename string, nip string) *v1.Pod {
  82. pod := testPod
  83. nodeIP := v1.EnvVar{
  84. Name: "NODE_IP",
  85. Value: nip,
  86. }
  87. pod.Spec.Containers[0].Env = append(pod.Spec.Containers[0].Env, nodeIP)
  88. pod.Spec.NodeName = nodename
  89. return &pod
  90. }
  91. func newTestProxyPod(nodename string) *v1.Pod {
  92. pod := testProxyPod
  93. pod.Spec.NodeName = nodename
  94. return &pod
  95. }
  96. func getIP(iptype v1.NodeAddressType, node *v1.Node) (string, error) {
  97. for _, addr := range node.Status.Addresses {
  98. if addr.Type == iptype {
  99. return addr.Address, nil
  100. }
  101. }
  102. return "", fmt.Errorf("did not find %s on Node", iptype)
  103. }
  104. func getSchedulable(nodes []v1.Node) (*v1.Node, error) {
  105. for _, node := range nodes {
  106. if node.Spec.Unschedulable == false {
  107. return &node, nil
  108. }
  109. }
  110. return nil, fmt.Errorf("all Nodes were unschedulable")
  111. }
  112. func checknosnatURL(proxy, pip string, ips []string) string {
  113. return fmt.Sprintf("http://%s/checknosnat?target=%s&ips=%s", proxy, pip, strings.Join(ips, ","))
  114. }
  115. // This test verifies that a Pod on each node in a cluster can talk to Pods on every other node without SNAT.
  116. // We use the [Feature:NoSNAT] tag so that most jobs will skip this test by default.
  117. var _ = SIGDescribe("NoSNAT [Feature:NoSNAT] [Slow]", func() {
  118. f := framework.NewDefaultFramework("no-snat-test")
  119. ginkgo.It("Should be able to send traffic between Pods without SNAT", func() {
  120. cs := f.ClientSet
  121. pc := cs.CoreV1().Pods(f.Namespace.Name)
  122. nc := cs.CoreV1().Nodes()
  123. ginkgo.By("creating a test pod on each Node")
  124. nodes, err := nc.List(metav1.ListOptions{})
  125. framework.ExpectNoError(err)
  126. if len(nodes.Items) == 0 {
  127. framework.ExpectNoError(fmt.Errorf("no Nodes in the cluster"))
  128. }
  129. for _, node := range nodes.Items {
  130. // find the Node's internal ip address to feed to the Pod
  131. inIP, err := getIP(v1.NodeInternalIP, &node)
  132. framework.ExpectNoError(err)
  133. // target Pod at Node and feed Pod Node's InternalIP
  134. pod := newTestPod(node.Name, inIP)
  135. _, err = pc.Create(pod)
  136. framework.ExpectNoError(err)
  137. }
  138. // In some (most?) scenarios, the test harness doesn't run in the same network as the Pods,
  139. // which means it can't query Pods using their cluster-internal IPs. To get around this,
  140. // we create a Pod in a Node's host network, and have that Pod serve on a specific port of that Node.
  141. // We can then ask this proxy Pod to query the internal endpoints served by the test Pods.
  142. // Find the first schedulable node; masters are marked unschedulable. We don't put the proxy on the master
  143. // because in some (most?) deployments firewall rules don't allow external traffic to hit ports 30000-32767
  144. // on the master, but do allow this on the nodes.
  145. node, err := getSchedulable(nodes.Items)
  146. framework.ExpectNoError(err)
  147. ginkgo.By("creating a no-snat-test-proxy Pod on Node " + node.Name + " port " + strconv.Itoa(testProxyPort) +
  148. " so we can target our test Pods through this Node's ExternalIP")
  149. extIP, err := getIP(v1.NodeExternalIP, node)
  150. framework.ExpectNoError(err)
  151. proxyNodeIP := extIP + ":" + strconv.Itoa(testProxyPort)
  152. _, err = pc.Create(newTestProxyPod(node.Name))
  153. framework.ExpectNoError(err)
  154. ginkgo.By("waiting for all of the no-snat-test pods to be scheduled and running")
  155. err = wait.PollImmediate(10*time.Second, 1*time.Minute, func() (bool, error) {
  156. pods, err := pc.List(metav1.ListOptions{LabelSelector: "no-snat-test"})
  157. if err != nil {
  158. return false, err
  159. }
  160. // check all pods are running
  161. for _, pod := range pods.Items {
  162. if pod.Status.Phase != v1.PodRunning {
  163. if pod.Status.Phase != v1.PodPending {
  164. return false, fmt.Errorf("expected pod to be in phase \"Pending\" or \"Running\"")
  165. }
  166. return false, nil // pod is still pending
  167. }
  168. }
  169. return true, nil // all pods are running
  170. })
  171. framework.ExpectNoError(err)
  172. ginkgo.By("waiting for the no-snat-test-proxy Pod to be scheduled and running")
  173. err = wait.PollImmediate(10*time.Second, 1*time.Minute, func() (bool, error) {
  174. pod, err := pc.Get("no-snat-test-proxy", metav1.GetOptions{})
  175. if err != nil {
  176. return false, err
  177. }
  178. if pod.Status.Phase != v1.PodRunning {
  179. if pod.Status.Phase != v1.PodPending {
  180. return false, fmt.Errorf("expected pod to be in phase \"Pending\" or \"Running\"")
  181. }
  182. return false, nil // pod is still pending
  183. }
  184. return true, nil // pod is running
  185. })
  186. framework.ExpectNoError(err)
  187. ginkgo.By("sending traffic from each pod to the others and checking that SNAT does not occur")
  188. pods, err := pc.List(metav1.ListOptions{LabelSelector: "no-snat-test"})
  189. framework.ExpectNoError(err)
  190. // collect pod IPs
  191. podIPs := []string{}
  192. for _, pod := range pods.Items {
  193. podIPs = append(podIPs, pod.Status.PodIP+":"+strconv.Itoa(testPodPort))
  194. }
  195. // hit the /checknosnat endpoint on each Pod, tell each Pod to check all the other Pods
  196. // this test is O(n^2) but it doesn't matter because we only run this test on small clusters (~3 nodes)
  197. errs := []string{}
  198. client := http.Client{
  199. Timeout: 5 * time.Minute,
  200. }
  201. for _, pip := range podIPs {
  202. ips := []string{}
  203. for _, ip := range podIPs {
  204. if ip == pip {
  205. continue
  206. }
  207. ips = append(ips, ip)
  208. }
  209. // hit /checknosnat on pip, via proxy
  210. resp, err := client.Get(checknosnatURL(proxyNodeIP, pip, ips))
  211. framework.ExpectNoError(err)
  212. // check error code on the response, if 500 record the body, which will describe the error
  213. if resp.StatusCode == 500 {
  214. body, err := ioutil.ReadAll(resp.Body)
  215. framework.ExpectNoError(err)
  216. errs = append(errs, string(body))
  217. }
  218. resp.Body.Close()
  219. }
  220. // report the errors all at the end
  221. if len(errs) > 0 {
  222. str := strings.Join(errs, "\n")
  223. err := fmt.Errorf("/checknosnat failed in the following cases:\n%s", str)
  224. framework.ExpectNoError(err)
  225. }
  226. })
  227. })