// Copyright 2023 CNI 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 main

import (
	"fmt"
	"strings"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"sigs.k8s.io/knftables"

	"github.com/containernetworking/cni/pkg/types"
)

var _ = Describe("portmapping configuration (nftables)", func() {
	containerID := "icee6giejonei6so"
	configTmpl := `{
		"name": "test",
		"type": "portmap",
		"cniVersion": "%s",
		"backend": "nftables",
		"runtimeConfig": {
			"portMappings": [
				{ "hostPort": 8080, "containerPort": 80, "protocol": "tcp"},
				{ "hostPort": 8081, "containerPort": 80, "protocol": "tcp"},
				{ "hostPort": 8080, "containerPort": 81, "protocol": "udp"},
				{ "hostPort": 8082, "containerPort": 82, "protocol": "udp"},
				{ "hostPort": 8083, "containerPort": 83, "protocol": "tcp", "hostIP": "192.168.0.2"},
				{ "hostPort": 8084, "containerPort": 84, "protocol": "tcp", "hostIP": "0.0.0.0"},
				{ "hostPort": 8085, "containerPort": 85, "protocol": "tcp", "hostIP": "2001:db8:a::1"},
				{ "hostPort": 8086, "containerPort": 86, "protocol": "tcp", "hostIP": "::"}
			]
		},
		"snat": true,
		"conditionsV4": ["a", "b"],
		"conditionsV6": ["c", "d"]
	}`
	containerNet4, err := types.ParseCIDR("10.0.0.2/24")
	Expect(err).NotTo(HaveOccurred())
	containerNet6, err := types.ParseCIDR("2001:db8::2/64")
	Expect(err).NotTo(HaveOccurred())

	for _, ver := range []string{"0.3.0", "0.3.1", "0.4.0", "1.0.0"} {
		// Redefine ver inside for scope so real value is picked up by each dynamically defined It()
		// See Gingkgo's "Patterns for dynamically generating tests" documentation.
		ver := ver

		Describe("nftables rules", func() {
			var pmNFT *portMapperNFTables
			var ipv4Fake, ipv6Fake *knftables.Fake
			BeforeEach(func() {
				ipv4Fake = knftables.NewFake(knftables.IPv4Family, tableName)
				ipv6Fake = knftables.NewFake(knftables.IPv6Family, tableName)
				pmNFT = &portMapperNFTables{
					ipv4: ipv4Fake,
					ipv6: ipv6Fake,
				}
			})

			It(fmt.Sprintf("[%s] generates correct rules", ver), func() {
				configBytes := []byte(fmt.Sprintf(configTmpl, ver))

				conf, _, err := parseConfig(configBytes, "foo")
				Expect(err).NotTo(HaveOccurred())
				conf.ContainerID = containerID

				err = pmNFT.forwardPorts(conf, *containerNet4)
				Expect(err).NotTo(HaveOccurred())

				expectedRules := strings.TrimSpace(`
add table ip cni_hostport { comment "CNI portmap plugin" ; }
add chain ip cni_hostport hostip_hostports
add chain ip cni_hostport hostports
add chain ip cni_hostport hostports_all
add chain ip cni_hostport masquerading { type nat hook postrouting priority 100 ; }
add chain ip cni_hostport output { type nat hook output priority -100 ; }
add chain ip cni_hostport prerouting { type nat hook prerouting priority -100 ; }
add rule ip cni_hostport hostports tcp dport 8080 dnat to 10.0.0.2:80 comment "icee6giejonei6so"
add rule ip cni_hostport hostports tcp dport 8081 dnat to 10.0.0.2:80 comment "icee6giejonei6so"
add rule ip cni_hostport hostports udp dport 8080 dnat to 10.0.0.2:81 comment "icee6giejonei6so"
add rule ip cni_hostport hostports udp dport 8082 dnat to 10.0.0.2:82 comment "icee6giejonei6so"
add rule ip cni_hostport hostports ip daddr 192.168.0.2 tcp dport 8083 dnat to 10.0.0.2:83 comment "icee6giejonei6so"
add rule ip cni_hostport hostports tcp dport 8084 dnat to 10.0.0.2:84 comment "icee6giejonei6so"
add rule ip cni_hostport hostports_all jump hostip_hostports
add rule ip cni_hostport hostports_all jump hostports
add rule ip cni_hostport masquerading ip saddr 10.0.0.2 ip daddr 10.0.0.2 masquerade comment "icee6giejonei6so"
add rule ip cni_hostport masquerading ip saddr 127.0.0.1 ip daddr 10.0.0.2 masquerade comment "icee6giejonei6so"
add rule ip cni_hostport output a b fib daddr type local jump hostports_all
add rule ip cni_hostport prerouting a b fib daddr type local jump hostports_all
`)
				actualRules := strings.TrimSpace(ipv4Fake.Dump())
				Expect(actualRules).To(Equal(expectedRules))

				// Disable snat, generate IPv6 rules
				*conf.SNAT = false

				err = pmNFT.forwardPorts(conf, *containerNet6)
				Expect(err).NotTo(HaveOccurred())

				expectedRules = strings.TrimSpace(`
add table ip6 cni_hostport { comment "CNI portmap plugin" ; }
add chain ip6 cni_hostport hostip_hostports
add chain ip6 cni_hostport hostports
add chain ip6 cni_hostport hostports_all
add chain ip6 cni_hostport output { type nat hook output priority -100 ; }
add chain ip6 cni_hostport prerouting { type nat hook prerouting priority -100 ; }
add rule ip6 cni_hostport hostports tcp dport 8080 dnat to [2001:db8::2]:80 comment "icee6giejonei6so"
add rule ip6 cni_hostport hostports tcp dport 8081 dnat to [2001:db8::2]:80 comment "icee6giejonei6so"
add rule ip6 cni_hostport hostports udp dport 8080 dnat to [2001:db8::2]:81 comment "icee6giejonei6so"
add rule ip6 cni_hostport hostports udp dport 8082 dnat to [2001:db8::2]:82 comment "icee6giejonei6so"
add rule ip6 cni_hostport hostports ip6 daddr 2001:db8:a::1 tcp dport 8085 dnat to [2001:db8::2]:85 comment "icee6giejonei6so"
add rule ip6 cni_hostport hostports tcp dport 8086 dnat to [2001:db8::2]:86 comment "icee6giejonei6so"
add rule ip6 cni_hostport hostports_all jump hostip_hostports
add rule ip6 cni_hostport hostports_all jump hostports
add rule ip6 cni_hostport output c d fib daddr type local jump hostports_all
add rule ip6 cni_hostport prerouting c d fib daddr type local jump hostports_all
`)
				actualRules = strings.TrimSpace(ipv6Fake.Dump())
				Expect(actualRules).To(Equal(expectedRules))

				err = pmNFT.unforwardPorts(conf)
				Expect(err).NotTo(HaveOccurred())

				expectedRules = strings.TrimSpace(`
add table ip cni_hostport { comment "CNI portmap plugin" ; }
add chain ip cni_hostport hostip_hostports
add chain ip cni_hostport hostports
add chain ip cni_hostport hostports_all
add chain ip cni_hostport masquerading { type nat hook postrouting priority 100 ; }
add chain ip cni_hostport output { type nat hook output priority -100 ; }
add chain ip cni_hostport prerouting { type nat hook prerouting priority -100 ; }
add rule ip cni_hostport hostports_all jump hostip_hostports
add rule ip cni_hostport hostports_all jump hostports
add rule ip cni_hostport output a b fib daddr type local jump hostports_all
add rule ip cni_hostport prerouting a b fib daddr type local jump hostports_all
`)
				actualRules = strings.TrimSpace(ipv4Fake.Dump())
				Expect(actualRules).To(Equal(expectedRules))

				expectedRules = strings.TrimSpace(`
add table ip6 cni_hostport { comment "CNI portmap plugin" ; }
add chain ip6 cni_hostport hostip_hostports
add chain ip6 cni_hostport hostports
add chain ip6 cni_hostport hostports_all
add chain ip6 cni_hostport output { type nat hook output priority -100 ; }
add chain ip6 cni_hostport prerouting { type nat hook prerouting priority -100 ; }
add rule ip6 cni_hostport hostports_all jump hostip_hostports
add rule ip6 cni_hostport hostports_all jump hostports
add rule ip6 cni_hostport output c d fib daddr type local jump hostports_all
add rule ip6 cni_hostport prerouting c d fib daddr type local jump hostports_all
`)
				actualRules = strings.TrimSpace(ipv6Fake.Dump())
				Expect(actualRules).To(Equal(expectedRules))
			})

			It(fmt.Sprintf("[%s] has working CHECK", ver), func() {
				configBytes := []byte(fmt.Sprintf(configTmpl, ver))

				conf, _, err := parseConfig(configBytes, "foo")
				Expect(err).NotTo(HaveOccurred())
				conf.ContainerID = containerID

				// CHECK KO: Missing all rules
				err = pmNFT.checkPorts(conf, *containerNet4)
				Expect(err).To(HaveOccurred())
				err = pmNFT.checkPorts(conf, *containerNet6)
				Expect(err).To(HaveOccurred())

				*conf.SNAT = false

				// ADD with SNAT=false
				err = pmNFT.forwardPorts(conf, *containerNet4)
				Expect(err).NotTo(HaveOccurred())
				err = pmNFT.forwardPorts(conf, *containerNet6)
				Expect(err).NotTo(HaveOccurred())

				// CHECK OK: no SNAT rules
				err = pmNFT.checkPorts(conf, *containerNet4)
				Expect(err).NotTo(HaveOccurred())
				err = pmNFT.checkPorts(conf, *containerNet6)
				Expect(err).NotTo(HaveOccurred())

				*conf.SNAT = true

				// CHECK KO: missing SNAT rules
				err = pmNFT.checkPorts(conf, *containerNet4)
				Expect(err).To(HaveOccurred())
				err = pmNFT.checkPorts(conf, *containerNet6)
				Expect(err).To(HaveOccurred())

				// ADD with SNAT=true
				err = pmNFT.forwardPorts(conf, *containerNet4)
				Expect(err).NotTo(HaveOccurred())
				err = pmNFT.forwardPorts(conf, *containerNet6)
				Expect(err).NotTo(HaveOccurred())

				// CHECK OK: All rules present
				err = pmNFT.checkPorts(conf, *containerNet4)
				Expect(err).NotTo(HaveOccurred())
				err = pmNFT.checkPorts(conf, *containerNet6)
				Expect(err).NotTo(HaveOccurred())

				// DELETE
				err = pmNFT.unforwardPorts(conf)
				Expect(err).NotTo(HaveOccurred())

				// CHECK KO: Missing all rules
				err = pmNFT.checkPorts(conf, *containerNet4)
				Expect(err).To(HaveOccurred())
				err = pmNFT.checkPorts(conf, *containerNet6)
				Expect(err).To(HaveOccurred())
			})
		})
	}
})
