k8s.io/kubernetes@v1.29.3/test/e2e/network/firewall.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  	"strings"
    23  	"time"
    24  
    25  	v1 "k8s.io/api/core/v1"
    26  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  	"k8s.io/apimachinery/pkg/util/sets"
    28  	"k8s.io/apimachinery/pkg/util/wait"
    29  	clientset "k8s.io/client-go/kubernetes"
    30  	cloudprovider "k8s.io/cloud-provider"
    31  	"k8s.io/kubernetes/pkg/cluster/ports"
    32  	kubeschedulerconfig "k8s.io/kubernetes/pkg/scheduler/apis/config"
    33  	"k8s.io/kubernetes/test/e2e/framework"
    34  	e2enetwork "k8s.io/kubernetes/test/e2e/framework/network"
    35  	e2enode "k8s.io/kubernetes/test/e2e/framework/node"
    36  	e2epod "k8s.io/kubernetes/test/e2e/framework/pod"
    37  	"k8s.io/kubernetes/test/e2e/framework/providers/gce"
    38  	e2eservice "k8s.io/kubernetes/test/e2e/framework/service"
    39  	e2eskipper "k8s.io/kubernetes/test/e2e/framework/skipper"
    40  	"k8s.io/kubernetes/test/e2e/network/common"
    41  	gcecloud "k8s.io/legacy-cloud-providers/gce"
    42  	admissionapi "k8s.io/pod-security-admission/api"
    43  
    44  	"github.com/onsi/ginkgo/v2"
    45  )
    46  
    47  const (
    48  	firewallTestTCPTimeout = time.Duration(1 * time.Second)
    49  	// Set ports outside of 30000-32767, 80 and 8080 to avoid being allowlisted by the e2e cluster
    50  	firewallTestHTTPPort = int32(29999)
    51  	firewallTestUDPPort  = int32(29998)
    52  )
    53  
    54  var _ = common.SIGDescribe("Firewall rule", func() {
    55  	var firewallTestName = "firewall-test"
    56  	f := framework.NewDefaultFramework(firewallTestName)
    57  	f.NamespacePodSecurityLevel = admissionapi.LevelPrivileged
    58  
    59  	var cs clientset.Interface
    60  	var cloudConfig framework.CloudConfig
    61  	var gceCloud *gcecloud.Cloud
    62  
    63  	ginkgo.BeforeEach(func() {
    64  		e2eskipper.SkipUnlessProviderIs("gce")
    65  
    66  		var err error
    67  		cs = f.ClientSet
    68  		cloudConfig = framework.TestContext.CloudConfig
    69  		gceCloud, err = gce.GetGCECloud()
    70  		framework.ExpectNoError(err)
    71  	})
    72  
    73  	// This test takes around 6 minutes to run
    74  	f.It(f.WithSlow(), f.WithSerial(), "should create valid firewall rules for LoadBalancer type service", func(ctx context.Context) {
    75  		ns := f.Namespace.Name
    76  		// This source ranges is just used to examine we have exact same things on LB firewall rules
    77  		firewallTestSourceRanges := []string{"0.0.0.0/1", "128.0.0.0/1"}
    78  		serviceName := "firewall-test-loadbalancer"
    79  
    80  		ginkgo.By("Getting cluster ID")
    81  		clusterID, err := gce.GetClusterID(ctx, cs)
    82  		framework.ExpectNoError(err)
    83  		framework.Logf("Got cluster ID: %v", clusterID)
    84  
    85  		jig := e2eservice.NewTestJig(cs, ns, serviceName)
    86  		nodeList, err := e2enode.GetBoundedReadySchedulableNodes(ctx, cs, e2eservice.MaxNodesForEndpointsTests)
    87  		framework.ExpectNoError(err)
    88  
    89  		nodesNames := []string{}
    90  		for _, node := range nodeList.Items {
    91  			nodesNames = append(nodesNames, node.Name)
    92  		}
    93  		nodesSet := sets.NewString(nodesNames...)
    94  
    95  		ginkgo.By("Creating a LoadBalancer type service with ExternalTrafficPolicy=Global")
    96  		svc, err := jig.CreateLoadBalancerService(ctx, e2eservice.GetServiceLoadBalancerCreationTimeout(ctx, cs), func(svc *v1.Service) {
    97  			svc.Spec.Ports = []v1.ServicePort{{Protocol: v1.ProtocolTCP, Port: firewallTestHTTPPort}}
    98  			svc.Spec.LoadBalancerSourceRanges = firewallTestSourceRanges
    99  		})
   100  		framework.ExpectNoError(err)
   101  		defer func() {
   102  			_, err = jig.UpdateService(ctx, func(svc *v1.Service) {
   103  				svc.Spec.Type = v1.ServiceTypeNodePort
   104  				svc.Spec.LoadBalancerSourceRanges = nil
   105  			})
   106  			framework.ExpectNoError(err)
   107  			err = cs.CoreV1().Services(svc.Namespace).Delete(ctx, svc.Name, metav1.DeleteOptions{})
   108  			framework.ExpectNoError(err)
   109  			ginkgo.By("Waiting for the local traffic health check firewall rule to be deleted")
   110  			localHCFwName := gce.MakeHealthCheckFirewallNameForLBService(clusterID, cloudprovider.DefaultLoadBalancerName(svc), false)
   111  			_, err := gce.WaitForFirewallRule(ctx, gceCloud, localHCFwName, false, e2eservice.LoadBalancerCleanupTimeout)
   112  			framework.ExpectNoError(err)
   113  		}()
   114  		svcExternalIP := svc.Status.LoadBalancer.Ingress[0].IP
   115  
   116  		ginkgo.By("Checking if service's firewall rule is correct")
   117  		lbFw := gce.ConstructFirewallForLBService(svc, cloudConfig.NodeTag)
   118  		fw, err := gceCloud.GetFirewall(lbFw.Name)
   119  		framework.ExpectNoError(err)
   120  		err = gce.VerifyFirewallRule(fw, lbFw, cloudConfig.Network, false)
   121  		framework.ExpectNoError(err)
   122  
   123  		ginkgo.By("Checking if service's nodes health check firewall rule is correct")
   124  		nodesHCFw := gce.ConstructHealthCheckFirewallForLBService(clusterID, svc, cloudConfig.NodeTag, true)
   125  		fw, err = gceCloud.GetFirewall(nodesHCFw.Name)
   126  		framework.ExpectNoError(err)
   127  		err = gce.VerifyFirewallRule(fw, nodesHCFw, cloudConfig.Network, false)
   128  		framework.ExpectNoError(err)
   129  
   130  		// OnlyLocal service is needed to examine which exact nodes the requests are being forwarded to by the Load Balancer on GCE
   131  		ginkgo.By("Updating LoadBalancer service to ExternalTrafficPolicy=Local")
   132  		svc, err = jig.UpdateService(ctx, func(svc *v1.Service) {
   133  			svc.Spec.ExternalTrafficPolicy = v1.ServiceExternalTrafficPolicyLocal
   134  		})
   135  		framework.ExpectNoError(err)
   136  
   137  		ginkgo.By("Waiting for the nodes health check firewall rule to be deleted")
   138  		_, err = gce.WaitForFirewallRule(ctx, gceCloud, nodesHCFw.Name, false, e2eservice.LoadBalancerCleanupTimeout)
   139  		framework.ExpectNoError(err)
   140  
   141  		ginkgo.By("Waiting for the correct local traffic health check firewall rule to be created")
   142  		localHCFw := gce.ConstructHealthCheckFirewallForLBService(clusterID, svc, cloudConfig.NodeTag, false)
   143  		fw, err = gce.WaitForFirewallRule(ctx, gceCloud, localHCFw.Name, true, e2eservice.GetServiceLoadBalancerCreationTimeout(ctx, cs))
   144  		framework.ExpectNoError(err)
   145  		err = gce.VerifyFirewallRule(fw, localHCFw, cloudConfig.Network, false)
   146  		framework.ExpectNoError(err)
   147  
   148  		ginkgo.By(fmt.Sprintf("Creating netexec pods on at most %v nodes", e2eservice.MaxNodesForEndpointsTests))
   149  		for i, nodeName := range nodesNames {
   150  			podName := fmt.Sprintf("netexec%v", i)
   151  
   152  			framework.Logf("Creating netexec pod %q on node %v in namespace %q", podName, nodeName, ns)
   153  			pod := e2epod.NewAgnhostPod(ns, podName, nil, nil, nil,
   154  				"netexec",
   155  				fmt.Sprintf("--http-port=%d", firewallTestHTTPPort),
   156  				fmt.Sprintf("--udp-port=%d", firewallTestUDPPort))
   157  			pod.ObjectMeta.Labels = jig.Labels
   158  			nodeSelection := e2epod.NodeSelection{Name: nodeName}
   159  			e2epod.SetNodeSelection(&pod.Spec, nodeSelection)
   160  			pod.Spec.HostNetwork = true
   161  			_, err := cs.CoreV1().Pods(ns).Create(ctx, pod, metav1.CreateOptions{})
   162  			framework.ExpectNoError(err)
   163  			framework.ExpectNoError(e2epod.WaitTimeoutForPodReadyInNamespace(ctx, f.ClientSet, podName, f.Namespace.Name, framework.PodStartTimeout))
   164  			framework.Logf("Netexec pod %q in namespace %q running", podName, ns)
   165  
   166  			defer func() {
   167  				framework.Logf("Cleaning up the netexec pod: %v", podName)
   168  				err = cs.CoreV1().Pods(ns).Delete(ctx, podName, metav1.DeleteOptions{})
   169  				framework.ExpectNoError(err)
   170  			}()
   171  		}
   172  
   173  		// Send requests from outside of the cluster because internal traffic is allowlisted
   174  		ginkgo.By("Accessing the external service ip from outside, all non-master nodes should be reached")
   175  		err = testHitNodesFromOutside(svcExternalIP, firewallTestHTTPPort, e2eservice.GetServiceLoadBalancerPropagationTimeout(ctx, cs), nodesSet)
   176  		framework.ExpectNoError(err)
   177  
   178  		// Check if there are overlapping tags on the firewall that extend beyond just the vms in our cluster
   179  		// by removing the tag on one vm and make sure it doesn't get any traffic. This is an imperfect
   180  		// simulation, we really want to check that traffic doesn't reach a vm outside the GKE cluster, but
   181  		// that's much harder to do in the current e2e framework.
   182  		ginkgo.By(fmt.Sprintf("Removing tags from one of the nodes: %v", nodesNames[0]))
   183  		nodesSet.Delete(nodesNames[0])
   184  		// Instance could run in a different zone in multi-zone test. Figure out which zone
   185  		// it is in before proceeding.
   186  		zone := cloudConfig.Zone
   187  		if zoneInLabel, ok := nodeList.Items[0].Labels[v1.LabelFailureDomainBetaZone]; ok {
   188  			zone = zoneInLabel
   189  		} else if zoneInLabel, ok := nodeList.Items[0].Labels[v1.LabelTopologyZone]; ok {
   190  			zone = zoneInLabel
   191  		}
   192  		removedTags := gce.SetInstanceTags(cloudConfig, nodesNames[0], zone, []string{})
   193  		defer func() {
   194  			ginkgo.By("Adding tags back to the node and wait till the traffic is recovered")
   195  			nodesSet.Insert(nodesNames[0])
   196  			gce.SetInstanceTags(cloudConfig, nodesNames[0], zone, removedTags)
   197  			// Make sure traffic is recovered before exit
   198  			err = testHitNodesFromOutside(svcExternalIP, firewallTestHTTPPort, e2eservice.GetServiceLoadBalancerPropagationTimeout(ctx, cs), nodesSet)
   199  			framework.ExpectNoError(err)
   200  		}()
   201  
   202  		ginkgo.By("Accessing service through the external ip and examine got no response from the node without tags")
   203  		err = testHitNodesFromOutsideWithCount(svcExternalIP, firewallTestHTTPPort, e2eservice.GetServiceLoadBalancerPropagationTimeout(ctx, cs), nodesSet, 15)
   204  		framework.ExpectNoError(err)
   205  	})
   206  
   207  	ginkgo.It("control plane should not expose well-known ports", func(ctx context.Context) {
   208  		nodes, err := e2enode.GetReadySchedulableNodes(ctx, cs)
   209  		framework.ExpectNoError(err)
   210  
   211  		ginkgo.By("Checking well known ports on master and nodes are not exposed externally")
   212  		nodeAddr := e2enode.FirstAddress(nodes, v1.NodeExternalIP)
   213  		if nodeAddr != "" {
   214  			assertNotReachableHTTPTimeout(nodeAddr, "/", ports.KubeletPort, firewallTestTCPTimeout, false)
   215  			assertNotReachableHTTPTimeout(nodeAddr, "/", ports.KubeletReadOnlyPort, firewallTestTCPTimeout, false)
   216  			assertNotReachableHTTPTimeout(nodeAddr, "/", ports.ProxyStatusPort, firewallTestTCPTimeout, false)
   217  		}
   218  
   219  		controlPlaneAddresses := framework.GetControlPlaneAddresses(ctx, cs)
   220  		for _, instanceAddress := range controlPlaneAddresses {
   221  			assertNotReachableHTTPTimeout(instanceAddress, "/healthz", ports.KubeControllerManagerPort, firewallTestTCPTimeout, true)
   222  			assertNotReachableHTTPTimeout(instanceAddress, "/healthz", kubeschedulerconfig.DefaultKubeSchedulerPort, firewallTestTCPTimeout, true)
   223  		}
   224  	})
   225  })
   226  
   227  func assertNotReachableHTTPTimeout(ip, path string, port int, timeout time.Duration, enableHTTPS bool) {
   228  	result := e2enetwork.PokeHTTP(ip, port, path, &e2enetwork.HTTPPokeParams{Timeout: timeout, EnableHTTPS: enableHTTPS})
   229  	if result.Status == e2enetwork.HTTPError {
   230  		framework.Failf("Unexpected error checking for reachability of %s:%d: %v", ip, port, result.Error)
   231  	}
   232  	if result.Code != 0 {
   233  		framework.Failf("Was unexpectedly able to reach %s:%d", ip, port)
   234  	}
   235  }
   236  
   237  // testHitNodesFromOutside checks HTTP connectivity from outside.
   238  func testHitNodesFromOutside(externalIP string, httpPort int32, timeout time.Duration, expectedHosts sets.String) error {
   239  	return testHitNodesFromOutsideWithCount(externalIP, httpPort, timeout, expectedHosts, 1)
   240  }
   241  
   242  // testHitNodesFromOutsideWithCount checks HTTP connectivity from outside with count.
   243  func testHitNodesFromOutsideWithCount(externalIP string, httpPort int32, timeout time.Duration, expectedHosts sets.String,
   244  	countToSucceed int) error {
   245  	framework.Logf("Waiting up to %v for satisfying expectedHosts for %v times", timeout, countToSucceed)
   246  	hittedHosts := sets.NewString()
   247  	count := 0
   248  	condition := func() (bool, error) {
   249  		result := e2enetwork.PokeHTTP(externalIP, int(httpPort), "/hostname", &e2enetwork.HTTPPokeParams{Timeout: 1 * time.Second})
   250  		if result.Status != e2enetwork.HTTPSuccess {
   251  			return false, nil
   252  		}
   253  
   254  		hittedHost := strings.TrimSpace(string(result.Body))
   255  		if !expectedHosts.Has(hittedHost) {
   256  			framework.Logf("Error hitting unexpected host: %v, reset counter: %v", hittedHost, count)
   257  			count = 0
   258  			return false, nil
   259  		}
   260  		if !hittedHosts.Has(hittedHost) {
   261  			hittedHosts.Insert(hittedHost)
   262  			framework.Logf("Missing %+v, got %+v", expectedHosts.Difference(hittedHosts), hittedHosts)
   263  		}
   264  		if hittedHosts.Equal(expectedHosts) {
   265  			count++
   266  			if count >= countToSucceed {
   267  				return true, nil
   268  			}
   269  		}
   270  		return false, nil
   271  	}
   272  
   273  	if err := wait.Poll(time.Second, timeout, condition); err != nil {
   274  		return fmt.Errorf("error waiting for expectedHosts: %v, hittedHosts: %v, count: %v, expected count: %v",
   275  			expectedHosts, hittedHosts, count, countToSucceed)
   276  	}
   277  	return nil
   278  }