/*
Copyright 2022 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package network

import (
	"context"
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/onsi/ginkgo/v2"

	"k8s.io/kubernetes/test/e2e/framework"
	e2epod "k8s.io/kubernetes/test/e2e/framework/pod"
	e2eoutput "k8s.io/kubernetes/test/e2e/framework/pod/output"
	e2eservice "k8s.io/kubernetes/test/e2e/framework/service"
	e2eskipper "k8s.io/kubernetes/test/e2e/framework/skipper"
	"k8s.io/kubernetes/test/e2e/network/common"

	v1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/intstr"
	"k8s.io/apimachinery/pkg/util/wait"
	clientset "k8s.io/client-go/kubernetes"
	admissionapi "k8s.io/pod-security-admission/api"
	netutils "k8s.io/utils/net"
)

// What are funny IPs:
// The adjective is because of the curl blog that explains the history and the problem of liberal
// parsing of IP addresses and the consequences and security risks caused the lack of normalization,
// mainly due to the use of different notations to abuse parsers misalignment to bypass filters.
// xref: https://daniel.haxx.se/blog/2021/04/19/curl-those-funny-ipv4-addresses/
//
// Since golang 1.17, IPv4 addresses with leading zeros are rejected by the standard library.
// xref: https://github.com/golang/go/issues/30999
//
// Because this change on the parsers can cause that previous valid data become invalid, Kubernetes
// forked the old parsers allowing leading zeros on IPv4 address to not break the compatibility.
//
// Kubernetes interprets leading zeros on IPv4 addresses as decimal, users must not rely on parser
// alignment to not being impacted by the associated security advisory: CVE-2021-29923 golang
// standard library "net" - Improper Input Validation of octal literals in golang 1.16.2 and below
// standard library "net" results in indeterminate SSRF & RFI vulnerabilities. xref:
// https://nvd.nist.gov/vuln/detail/CVE-2021-29923
//
// Kubernetes
// ip := net.ParseIPSloppy("192.168.0.011")
// ip.String() yields 192.168.0.11
//
// BSD stacks
// ping 192.168.0.011
// PING 192.168.0.011 (192.168.0.9) 56(84) bytes of data.

var _ = common.SIGDescribe("CVE-2021-29923", func() {
	var (
		ns string
		cs clientset.Interface
	)

	f := framework.NewDefaultFramework("funny-ips")
	f.NamespacePodSecurityEnforceLevel = admissionapi.LevelBaseline

	ginkgo.BeforeEach(func() {
		if framework.TestContext.ClusterIsIPv6() {
			e2eskipper.Skipf("The test doesn't apply to IPv6 addresses, only IPv4 addresses are affected by CVE-2021-29923")
		}
		ns = f.Namespace.Name
		cs = f.ClientSet
	})

	/*
		Create a ClusterIP service with a ClusterIP with leading zeros and check that it is reachable in the IP interpreted as decimal.
		If the Service is not reachable only FAIL if it is reachable in the IP interpreted as binary, because it will be exposed to CVE-2021-29923.
		IMPORTANT: CoreDNS since version 1.8.5 discard IPs with leading zeros so Services are not resolvable, and is probably that
		most of the ecosystem has done the same, however, Kubernetes doesn't impose any restriction, users should migrate their IPs.
	*/
	ginkgo.It("IPv4 Service Type ClusterIP with leading zeros should work interpreted as decimal", func() {
		serviceName := "funny-ip"
		// Use a very uncommon port to reduce the risk of conflicts with other tests that create services.
		servicePort := 7180
		jig := e2eservice.NewTestJig(cs, ns, serviceName)

		clusterIPZero, clusterIPOctal := getServiceIPWithLeadingZeros(cs)
		if clusterIPZero == "" {
			e2eskipper.Skipf("Couldn't find a free ClusterIP")
		}

		ginkgo.By("creating service " + serviceName + " with type=ClusterIP and ip " + clusterIPZero + " in namespace " + ns)
		_, err := jig.CreateTCPService(func(svc *v1.Service) {
			svc.Spec.ClusterIP = clusterIPZero // IP with a leading zero
			svc.Spec.Type = v1.ServiceTypeClusterIP
			svc.Spec.Ports = []v1.ServicePort{
				{Port: int32(servicePort), Name: "http", Protocol: v1.ProtocolTCP, TargetPort: intstr.FromInt(9376)},
			}
		})
		framework.ExpectNoError(err)

		err = jig.CreateServicePods(1)
		framework.ExpectNoError(err)

		execPod := e2epod.CreateExecPodOrFail(cs, ns, "execpod", nil)
		ip := netutils.ParseIPSloppy(clusterIPZero)
		cmd := fmt.Sprintf("echo hostName | nc -v -t -w 2 %s %v", ip.String(), servicePort)
		err = wait.PollImmediate(1*time.Second, e2eservice.ServiceReachabilityShortPollTimeout, func() (bool, error) {
			stdout, err := e2eoutput.RunHostCmd(execPod.Namespace, execPod.Name, cmd)
			if err != nil {
				framework.Logf("Service reachability failing with error: %v\nRetrying...", err)
				return false, nil
			}
			trimmed := strings.TrimSpace(stdout)
			if trimmed != "" {
				return true, nil
			}
			return false, nil
		})
		// Service is working on the expected IP.
		if err == nil {
			return
		}
		// It may happen that the component implementing Services discard the IP.
		// We have to check that the Service is not reachable in the address interpreted as decimal.
		cmd = fmt.Sprintf("echo hostName | nc -v -t -w 2 %s %v", clusterIPOctal, servicePort)
		err = wait.PollImmediate(1*time.Second, e2eservice.ServiceReachabilityShortPollTimeout, func() (bool, error) {
			stdout, err := e2eoutput.RunHostCmd(execPod.Namespace, execPod.Name, cmd)
			if err != nil {
				framework.Logf("Service reachability failing with error: %v\nRetrying...", err)
				return false, nil
			}
			trimmed := strings.TrimSpace(stdout)
			if trimmed != "" {
				return true, nil
			}
			return false, nil
		})
		// Ouch, Service has worked on IP interpreted as octal.
		if err == nil {
			framework.Failf("WARNING: Your Cluster interprets Service ClusterIP %s as %s, please see https://nvd.nist.gov/vuln/detail/CVE-2021-29923", clusterIPZero, clusterIPOctal)
		}
		framework.Logf("Service reachability failing for Service against ClusterIP %s and %s, most probably leading zeros on IPs are not supported by the cluster proxy", clusterIPZero, clusterIPOctal)
	})

})

// Try to get a free IP that has different decimal and binary interpretation with leading zeros.
// Return both IPs, the one interpretad as binary and the one interpreted as decimal.
// Return empty if not IPs are found.
func getServiceIPWithLeadingZeros(cs clientset.Interface) (string, string) {
	clusterIPMap := map[string]struct{}{}
	var clusterIPPrefix string
	// Dump all the IPs and look for the ones we want.
	list, err := cs.CoreV1().Services(metav1.NamespaceAll).List(context.TODO(), metav1.ListOptions{})
	framework.ExpectNoError(err)
	for _, svc := range list.Items {
		if len(svc.Spec.ClusterIP) == 0 || svc.Spec.ClusterIP == v1.ClusterIPNone {
			continue
		}
		// Get the clusterIP prefix, needed later for building the IPs with octal format.
		if clusterIPPrefix == "" {
			// split the IP by "."
			// get the first 3 values
			// join them again using dots, dropping the last field
			clusterIPPrefix = strings.Join(strings.Split(svc.Spec.ClusterIP, ".")[:3], ".")
		}
		// Canonicalize IP.
		ip := netutils.ParseIPSloppy(svc.Spec.ClusterIP)
		clusterIPMap[ip.String()] = struct{}{}
	}

	// Try to get a free IP between x.x.x.11 and x.x.x.31, this assumes a /27 Service IP range that should be safe.
	for i := 11; i < 31; i++ {
		ip := fmt.Sprintf("%s.%d", clusterIPPrefix, i)
		// Check the IP in decimal format is free.
		if _, ok := clusterIPMap[ip]; !ok {
			o, err := strconv.ParseInt(fmt.Sprintf("%d", i), 8, 64)
			// Omit ips without binary representation i.e. x.x.x.018, x.x.x.019 ...
			if err != nil {
				continue
			}
			ipOctal := fmt.Sprintf("%s.%d", clusterIPPrefix, o)
			// Check the same IP in octal format is free.
			if _, ok := clusterIPMap[ipOctal]; !ok {
				// Add a leading zero to the decimal format.
				return fmt.Sprintf("%s.0%d", clusterIPPrefix, i), ipOctal
			}
		}
	}
	return "", ""
}
