// Copyright (c) 2020-2022 Tigera, Inc. All rights reserved.
//
// 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 ut_test

import (
	"encoding/binary"
	"encoding/json"
	"fmt"
	"net"
	"os"
	"os/exec"
	"path"
	"reflect"
	"runtime"
	"strings"
	"sync"
	"syscall"
	"testing"

	"github.com/google/gopacket"
	"github.com/google/gopacket/layers"
	. "github.com/onsi/gomega"
	. "github.com/onsi/gomega/gstruct"
	"github.com/onsi/gomega/types"
	"github.com/pkg/errors"
	log "github.com/sirupsen/logrus"
	"golang.org/x/sys/unix"

	"github.com/projectcalico/calico/felix/bpf"
	"github.com/projectcalico/calico/felix/bpf/arp"
	"github.com/projectcalico/calico/felix/bpf/conntrack"
	"github.com/projectcalico/calico/felix/bpf/counters"
	"github.com/projectcalico/calico/felix/bpf/failsafes"
	"github.com/projectcalico/calico/felix/bpf/hook"
	"github.com/projectcalico/calico/felix/bpf/ifstate"
	"github.com/projectcalico/calico/felix/bpf/ipsets"
	"github.com/projectcalico/calico/felix/bpf/jump"
	"github.com/projectcalico/calico/felix/bpf/libbpf"
	"github.com/projectcalico/calico/felix/bpf/maps"
	"github.com/projectcalico/calico/felix/bpf/nat"
	"github.com/projectcalico/calico/felix/bpf/polprog"
	"github.com/projectcalico/calico/felix/bpf/routes"
	"github.com/projectcalico/calico/felix/bpf/state"
	"github.com/projectcalico/calico/felix/bpf/tc"
	tcdefs "github.com/projectcalico/calico/felix/bpf/tc/defs"
	"github.com/projectcalico/calico/felix/bpf/xdp"
	"github.com/projectcalico/calico/felix/environment"
	"github.com/projectcalico/calico/felix/idalloc"
	"github.com/projectcalico/calico/felix/ip"
	"github.com/projectcalico/calico/felix/logutils"
	"github.com/projectcalico/calico/felix/proto"
)

var canTestMarks bool

func init() {
	logutils.ConfigureEarlyLogging()
	log.SetLevel(log.DebugLevel)

	fd := environment.NewFeatureDetector(make(map[string]string))
	if ok, err := fd.KernelIsAtLeast("5.9.0"); err == nil && ok {
		canTestMarks = true
	}
}

// Constants that are shared with the UT binaries that we build.
const (
	natTunnelMTU  = uint16(700)
	testVxlanPort = uint16(5665)
)

var (
	rulesDefaultAllow = &polprog.Rules{
		Tiers: []polprog.Tier{{
			Name: "base tier",
			Policies: []polprog.Policy{{
				Name:  "allow all",
				Rules: []polprog.Rule{{Rule: &proto.Rule{Action: "Allow"}}},
			}},
		}},
	}
	node1ip    = net.IPv4(10, 10, 0, 1).To4()
	node1ip2   = net.IPv4(10, 10, 2, 1).To4()
	node1tunIP = net.IPv4(11, 11, 0, 1).To4()
	node2ip    = net.IPv4(10, 10, 0, 2).To4()
	intfIP     = net.IPv4(10, 10, 0, 3).To4()
	node1CIDR  = net.IPNet{
		IP:   node1ip,
		Mask: net.IPv4Mask(255, 255, 255, 255),
	}
	node2CIDR = net.IPNet{
		IP:   node2ip,
		Mask: net.IPv4Mask(255, 255, 255, 255),
	}

	node1ipV6    = net.ParseIP("abcd::ffff:0a0a:0001").To16()
	node1ip2V6   = net.ParseIP("abcd::ffff:0a0a:0201").To16()
	node1tunIPV6 = net.ParseIP("abcd::ffff:0b0b:0001").To16()
	node2ipV6    = net.ParseIP("abcd::ffff:0a0a:0002").To16()
	intfIPV6     = net.ParseIP("abcd::ffff:0a0a:0003").To16()
	node1CIDRV6  = net.IPNet{
		IP:   node1ipV6,
		Mask: net.CIDRMask(128, 128),
	}
	node2CIDRV6 = net.IPNet{
		IP:   node2ipV6,
		Mask: net.CIDRMask(128, 128),
	}
)

// Globals that we use to configure the next test run.
var (
	hostIP       = node1ip
	skbMark      uint32
	bpfIfaceName string
)

const (
	resTC_ACT_OK int = iota
	resTC_ACT_RECLASSIFY
	resTC_ACT_SHOT
	resTC_ACT_PIPE
	resTC_ACT_STOLEN
	resTC_ACT_QUEUED
	resTC_ACT_REPEAT
	resTC_ACT_REDIRECT
	resTC_ACT_UNSPEC = (1 << 32) - 1
)

var retvalToStr = map[int]string{
	resTC_ACT_OK:         "TC_ACT_OK",
	resTC_ACT_RECLASSIFY: "TC_ACT_RECLASSIFY",
	resTC_ACT_SHOT:       "TC_ACT_SHOT",
	resTC_ACT_PIPE:       "TC_ACT_PIPE",
	resTC_ACT_STOLEN:     "TC_ACT_STOLEN",
	resTC_ACT_QUEUED:     "TC_ACT_QUEUED",
	resTC_ACT_REPEAT:     "TC_ACT_REPEAT",
	resTC_ACT_REDIRECT:   "TC_ACT_REDIRECT",
	resTC_ACT_UNSPEC:     "TC_ACT_UNSPEC",
}

const (
	resXDP_ABORTED int = iota
	resXDP_DROP
	resXDP_PASS
)

var retvalToStrXDP = map[int]string{
	resXDP_ABORTED: "XDP_ABORTED",
	resXDP_PASS:    "XDP_PASS",
	resXDP_DROP:    "XDP_DROP",
}

func expectMark(expect int) {
	if canTestMarks {
		ExpectWithOffset(1, skbMark).To(Equal(uint32(expect)),
			fmt.Sprintf("skbMark 0x%08x should be 0x%08x at %s", skbMark, expect, caller(2)))
	} else {
		// If we cannot verify the mark, set it to the expected value as the
		// next stage expects it to be set.
		skbMark = uint32(expect)
	}
}

var xdpJumpMapIndexes = map[string]map[int]string{
	"IPv4": map[int]string{
		tcdefs.ProgIndexMain:    "calico_xdp_main",
		tcdefs.ProgIndexPolicy:  "calico_xdp_norm_pol_tail",
		tcdefs.ProgIndexAllowed: "calico_xdp_accepted_entrypoint",
		tcdefs.ProgIndexDrop:    "calico_xdp_drop",
	},
	"IPv4 debug": map[int]string{
		tcdefs.ProgIndexMain:    "calico_xdp_main",
		tcdefs.ProgIndexPolicy:  "calico_xdp_norm_pol_tail",
		tcdefs.ProgIndexAllowed: "calico_xdp_accepted_entrypoint",
		tcdefs.ProgIndexDrop:    "calico_xdp_drop",
	},
	"IPv6": map[int]string{
		tcdefs.ProgIndexMain:    "calico_xdp_main",
		tcdefs.ProgIndexPolicy:  "calico_xdp_norm_pol_tail",
		tcdefs.ProgIndexAllowed: "calico_xdp_accepted_entrypoint",
		tcdefs.ProgIndexDrop:    "calico_xdp_drop",
	},
	"IPv6 debug": map[int]string{
		tcdefs.ProgIndexMain:    "calico_xdp_main",
		tcdefs.ProgIndexPolicy:  "calico_xdp_norm_pol_tail",
		tcdefs.ProgIndexAllowed: "calico_xdp_accepted_entrypoint",
		tcdefs.ProgIndexDrop:    "calico_xdp_drop",
	},
}

var tcJumpMapIndexes = map[string][]int{
	"IPv4": []int{
		tcdefs.ProgIndexMain,
		tcdefs.ProgIndexPolicy,
		tcdefs.ProgIndexAllowed,
		tcdefs.ProgIndexIcmp,
		tcdefs.ProgIndexDrop,
		tcdefs.ProgIndexHostCtConflict,
		tcdefs.ProgIndexIcmpInnerNat,
		tcdefs.ProgIndexNewFlow,
	},
	"IPv4 debug": []int{
		tcdefs.ProgIndexMainDebug,
		tcdefs.ProgIndexPolicyDebug,
		tcdefs.ProgIndexAllowedDebug,
		tcdefs.ProgIndexIcmpDebug,
		tcdefs.ProgIndexDropDebug,
		tcdefs.ProgIndexHostCtConflictDebug,
		tcdefs.ProgIndexIcmpInnerNatDebug,
		tcdefs.ProgIndexNewFlowDebug,
	},
	"IPv6": []int{
		tcdefs.ProgIndexMain,
		tcdefs.ProgIndexPolicy,
		tcdefs.ProgIndexAllowed,
		tcdefs.ProgIndexIcmp,
		tcdefs.ProgIndexDrop,
		tcdefs.ProgIndexHostCtConflict,
		tcdefs.ProgIndexIcmpInnerNat,
		tcdefs.ProgIndexNewFlow,
	},
	"IPv6 debug": []int{
		tcdefs.ProgIndexMainDebug,
		tcdefs.ProgIndexPolicyDebug,
		tcdefs.ProgIndexAllowedDebug,
		tcdefs.ProgIndexIcmpDebug,
		tcdefs.ProgIndexDropDebug,
		tcdefs.ProgIndexHostCtConflictDebug,
		tcdefs.ProgIndexIcmpInnerNatDebug,
		tcdefs.ProgIndexNewFlowDebug,
	},
}

func TestCompileTemplateRun(t *testing.T) {
	skbMark = tcdefs.MarkSeen
	runBpfTest(t, "calico_to_workload_ep", &polprog.Rules{}, func(bpfrun bpfProgRunFn) {
		_, _, _, _, pktBytes, err := testPacketUDPDefault()
		Expect(err).NotTo(HaveOccurred())

		res, err := bpfrun(pktBytes)
		Expect(err).NotTo(HaveOccurred())

		// Implicitly denied by normal policy: DROP
		Expect(res.Retval).To(Equal(resTC_ACT_SHOT))
	})
}

func TestLoadZeroProgram(t *testing.T) {
	RegisterTestingT(t)
	fd, err := bpf.LoadBPFProgramFromInsns(nil, "calico_policy", "Apache-2.0", unix.BPF_PROG_TYPE_SCHED_CLS)
	if err == nil {
		_ = fd.Close()
	}
	Expect(err).To(Equal(unix.E2BIG))
}

type testLogger interface {
	Log(args ...interface{})
	Logf(format string, args ...interface{})
}

func startBPFLogging() *exec.Cmd {
	cmd := exec.Command("/usr/bin/bpftool", "prog", "tracelog")
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	err := cmd.Start()
	if err != nil {
		log.WithError(err).Warn("Failed to start bpf log collection")
		return nil
	}
	return cmd
}

func stopBPFLogging(cmd *exec.Cmd) {
	if cmd == nil {
		return
	}
	err := cmd.Process.Signal(syscall.SIGTERM)
	if err != nil {
		log.WithError(err).Warn("Failed to send SIGTERM to bpftool")
		return
	}
	err = cmd.Wait()
	if err != nil {
		log.WithError(err).Warn("Failed to wait for bpftool")
	}
}

func setupAndRun(logger testLogger, loglevel, section string, rules *polprog.Rules,
	runFn func(progName string), opts ...testOption) {
	topts := testOpts{
		subtests:  true,
		logLevel:  log.DebugLevel,
		psnaStart: 20000,
		psnatEnd:  30000,
	}

	for _, o := range opts {
		o(&topts)
	}

	tempDir, err := os.MkdirTemp("", "calico-bpf-")
	Expect(err).NotTo(HaveOccurred())
	defer os.RemoveAll(tempDir)

	unique := path.Base(tempDir)
	bpfFsDir := "/sys/fs/bpf/" + unique

	err = os.Mkdir(bpfFsDir, os.ModePerm)
	Expect(err).NotTo(HaveOccurred())
	defer os.RemoveAll(bpfFsDir)

	err = os.Mkdir(bpfFsDir+"_v6", os.ModePerm)
	Expect(err).NotTo(HaveOccurred())
	defer os.RemoveAll(bpfFsDir + "v6")

	obj := "../../bpf-gpl/bin/test_xdp_debug"
	if !topts.xdp {
		obj = "../../bpf-gpl/bin/test_"
		if strings.Contains(section, "from") {
			obj += "from_"
		} else {
			obj += "to_"
		}

		if strings.Contains(section, "host") {
			obj += "hep_"
			topts.progLog = "HEP"
		} else if strings.Contains(section, "nat") {
			obj += "nat_"
			topts.progLog = "NAT"
		} else if strings.Contains(section, "wireguard") {
			obj += "wg_"
			topts.progLog = "WG"
		} else {
			obj += "wep_"
			topts.progLog = "WEP"
		}

		if topts.ipv6 {
			log.WithField("hostIP", hostIP).Info("Host IP")
			log.WithField("intfIP", intfIPV6).Info("Intf IP")
		} else {
			log.WithField("hostIP", hostIP).Info("Host IP")
			log.WithField("intfIP", intfIP).Info("Intf IP")
		}
		obj += fmt.Sprintf("fib_%s", loglevel)

		if strings.Contains(section, "_dsr") {
			obj += "_dsr"
		}
	}

	ipFamily := "IPv4"
	policyIdx := tcdefs.ProgIndexPolicy
	if topts.ipv6 {
		ipFamily = "IPv6"
	}

	if topts.objname != "" {
		obj = topts.objname
	} else if topts.ipv6 {
		if topts.xdp {
			obj += "_co-re_v6"
		} else {
			obj += "_v6"
		}

	}

	if topts.xdp {
		o, err := objLoad("../../bpf-gpl/bin/xdp_preamble.o", bpfFsDir, "preamble", topts, false, false)
		Expect(err).NotTo(HaveOccurred())
		defer o.Close()
	} else {
		o, err := objLoad("../../bpf-gpl/bin/tc_preamble.o", bpfFsDir, "preamble", topts, false, false)
		Expect(err).NotTo(HaveOccurred())
		defer o.Close()
	}

	log.Infof("Patching binary %s", obj+".o")

	bin, err := bpf.BinaryFromFile(obj + ".o")
	Expect(err).NotTo(HaveOccurred())
	// XXX for now we both path the mark here and include it in the context as
	// well. This needs to be done for as long as we want to run the tests on
	// older kernels.
	bin.PatchSkbMark(skbMark)
	tempObj := tempDir + "bpf.o"
	err = bin.WriteToFile(tempObj)
	Expect(err).NotTo(HaveOccurred())

	if loglevel == "debug" {
		ipFamily += " debug"
	}

	var o *libbpf.Obj

	o, err = objLoad(tempObj, bpfFsDir, ipFamily, topts, rules != nil, true)
	Expect(err).NotTo(HaveOccurred())
	defer o.Close()

	if rules != nil {
		staticProgMap := progMap
		polMap := policyJumpMap
		popts := []polprog.Option{}
		stride := jump.TCMaxEntryPoints
		if topts.xdp {
			staticProgMap = progMapXDP
			polMap = policyJumpMapXDP
			popts = append(popts,
				polprog.WithAllowDenyJumps(tcdefs.ProgIndexAllowed, tcdefs.ProgIndexDrop),
				polprog.WithPolicyMapIndexAndStride(policyIdx, jump.XDPMaxEntryPoints),
			)
			if topts.ipv6 {
				popts = append(popts, polprog.WithIPv6())
			}
			stride = jump.XDPMaxEntryPoints
		} else {
			popts = append(popts,
				polprog.WithPolicyMapIndexAndStride(policyIdx, jump.TCMaxEntryPoints),
			)
		}
		alloc := &forceAllocator{alloc: idalloc.New()}
		ipsMapFD := ipsMap.MapFD()
		Expect(ipsMapFD).NotTo(BeZero())
		stateMapFD := stateMap.MapFD()
		Expect(stateMapFD).NotTo(BeZero())
		pg := polprog.NewBuilder(alloc, ipsMapFD, stateMapFD, staticProgMap.MapFD(), polMap.MapFD(), popts...)
		insns, err := pg.Instructions(*rules)
		Expect(err).NotTo(HaveOccurred())

		var polProgFDs []bpf.ProgFD
		defer func() {
			var errs []error
			for _, polProgFD := range polProgFDs {
				err := polProgFD.Close()
				if err != nil {
					errs = append(errs, err)
				}
			}
			Expect(errs).To(BeEmpty())
		}()
		var progType uint32
		if topts.xdp {
			progType = unix.BPF_PROG_TYPE_XDP
		} else {
			progType = unix.BPF_PROG_TYPE_SCHED_CLS
		}
		for i, p := range insns {
			polProgFD, err := bpf.LoadBPFProgramFromInsns(p, "calico_policy", "Apache-2.0", progType)
			Expect(err).NotTo(HaveOccurred(), "failed to load program into the kernel")
			Expect(polProgFD).NotTo(BeZero())
			polProgFDs = append(polProgFDs, polProgFD)
			err = polMap.Update(
				jump.Key(polprog.SubProgramJumpIdx(policyIdx, i, stride)),
				jump.Value(polProgFD.FD()),
			)
			Expect(err).NotTo(HaveOccurred())
		}
		log.WithField("rules", rules).Debug("set policy")
	}

	if !topts.xdp {
		runFn(bpfFsDir + "/cali_tc_preamble")
	} else {
		runFn(bpfFsDir + "/cali_xdp_preamble")
	}
}

func caller(skip int) string {
	_, f, l, ok := runtime.Caller(skip)
	if ok {
		return fmt.Sprintf("%s:%d", f, l)
	}

	return "<unknown>"
}

// runBpfTest runs a specific section of the entire bpf program in isolation
func runBpfTest(t *testing.T, section string, rules *polprog.Rules, testFn func(bpfProgRunFn), opts ...testOption) {
	RegisterTestingT(t)
	xdp := false
	if strings.Contains(section, "xdp") {
		xdp = true
	}

	ctxIn := make([]byte, 18*4)
	binary.LittleEndian.PutUint32(ctxIn[2*4:3*4], skbMark)
	if xdp {
		// XDP tests cannot take context and would fail.
		ctxIn = nil
	}

	topts := testOpts{}

	for _, o := range opts {
		o(&topts)
	}

	cllr := caller(2)

	setupAndRun(t, "debug", section, rules, func(progName string) {
		label := section
		if topts.description != "" {
			label = topts.description + " - " + section
		}
		t.Run(label, func(_ *testing.T) {
			if strings.Contains(section, "calico_from_") {
				ExpectWithOffset(2, skbMark).To(Equal(uint32(0)),
					fmt.Sprintf("skb mark 0x%08x should be zero at %s", skbMark, cllr))
			}
			if !topts.hostNetworked && strings.Contains(section, "calico_to_") {
				ExpectWithOffset(2, skbMark&uint32(tcdefs.MarkSeen) != 0).
					To(BeTrue(), fmt.Sprintf("skb mark 0x%08x does not have tcdefs.MarkSeen 0x%08x set before tc at %s",
						skbMark, tcdefs.MarkSeen, cllr))
			}

			testFn(func(dataIn []byte) (bpfRunResult, error) {
				res, err := bpftoolProgRun(progName, dataIn, ctxIn)
				log.Debugf("dataIn  = %+v", dataIn)
				if err == nil {
					log.Debugf("dataOut = %+v", res.dataOut)
				}

				if res.Retval != resTC_ACT_SHOT && canTestMarks && strings.Contains(section, "calico_from_") {
					ExpectWithOffset(3, skbMark&uint32(tcdefs.MarkSeen) != 0).
						To(BeTrue(), fmt.Sprintf("skb mark 0x%08x does not have tcdefs.MarkSeen 0x%08x set after tc at %s",
							skbMark, tcdefs.MarkSeen, cllr))
				}

				return res, err
			})
		})
	}, opts...)
}

type forceAllocator struct {
	alloc *idalloc.IDAllocator
}

func (a *forceAllocator) GetNoAlloc(id string) uint64 {
	return a.alloc.GetOrAlloc(id)
}

func bpftool(args ...string) ([]byte, error) {
	args = append([]string{"--json", "--pretty"}, args...)
	cmd := exec.Command("bpftool", args...)
	log.WithField("cmd", cmd.String()).Debugf("executing")
	out, err := cmd.Output()
	if err != nil {
		if e, ok := err.(*exec.ExitError); ok {
			log.WithField("stderr", string(e.Stderr)).Errorf("bpftool %s failed: %v out=\n%v", args, err, string(out))
			// to make the output reflect the new lines, logrus ignores it
			fmt.Print(fmt.Sprint(string(e.Stderr)))
		}
	}

	return out, err
}

var (
	mapInitOnce sync.Once

	natMap, natBEMap, ctMap, rtMap, ipsMap, testStateMap, affinityMap, arpMap, fsafeMap     maps.Map
	natMapV6, natBEMapV6, ctMapV6, rtMapV6, ipsMapV6, affinityMapV6, arpMapV6, fsafeMapV6   maps.Map
	stateMap, countersMap, ifstateMap, progMap, progMapXDP, policyJumpMap, policyJumpMapXDP maps.Map
	allMaps                                                                                 []maps.Map
)

func initMapsOnce() {
	mapInitOnce.Do(func() {
		natMap = nat.FrontendMap()
		natBEMap = nat.BackendMap()
		natMapV6 = nat.FrontendMapV6()
		natBEMapV6 = nat.BackendMapV6()
		ctMap = conntrack.Map()
		ctMapV6 = conntrack.MapV6()
		rtMap = routes.Map()
		rtMapV6 = routes.MapV6()
		ipsMap = ipsets.Map()
		ipsMapV6 = ipsets.MapV6()
		stateMap = state.Map()
		testStateMap = state.MapForTest()
		affinityMap = nat.AffinityMap()
		affinityMapV6 = nat.AffinityMapV6()
		arpMap = arp.Map()
		arpMapV6 = arp.MapV6()
		fsafeMap = failsafes.Map()
		fsafeMapV6 = failsafes.MapV6()
		countersMap = counters.Map()
		ifstateMap = ifstate.Map()
		policyJumpMap = jump.Map()
		policyJumpMapXDP = jump.XDPMap()

		allMaps = []maps.Map{natMap, natBEMap, natMapV6, natBEMapV6, ctMap, ctMapV6, rtMap, rtMapV6, ipsMap, ipsMapV6,
			stateMap, testStateMap, affinityMap, affinityMapV6, arpMap, arpMapV6, fsafeMap, fsafeMapV6,
			countersMap, ifstateMap,
			policyJumpMap, policyJumpMapXDP}
		for _, m := range allMaps {
			err := m.EnsureExists()
			if err != nil {
				log.WithError(err).Panic("Failed to initialise maps")
			}
		}
	})
}

func cleanUpMaps() {
	log.Info("Cleaning up all maps")

	logLevel := log.GetLevel()
	log.SetLevel(log.InfoLevel)
	defer log.SetLevel(logLevel)

	for _, m := range allMaps {
		if m == stateMap || m == testStateMap || m == progMap || m == countersMap {
			continue // Can't clean up array maps
		}
		log.WithField("map", m.GetName()).Info("Cleaning")
		err := m.Iter(func(_, _ []byte) maps.IteratorAction {
			return maps.IterDelete
		})
		if err != nil {
			if errors.Is(err, maps.ErrNotSupported) {
				continue
			}
			log.WithError(err).Panic("Failed to walk map")
		}
	}
	log.Info("Cleaned up all maps")
}

func jumpMapUpdatePinned(jm maps.Map, idx int, val string) error {
	out, err := bpftool("map", "update", "pinned", jm.Path(),
		"key", fmt.Sprintf("%d", idx), "0", "0", "0", "value", "pinned", val)

	if err != nil {
		return fmt.Errorf("%s\n%w", string(out), err)
	}

	return nil
}

func jumpMapUpdate(jm maps.Map, idx int, val int) error {
	var k, v [4]byte

	binary.LittleEndian.PutUint32(k[:], uint32(idx))
	binary.LittleEndian.PutUint32(v[:], uint32(val))

	return jm.Update(k[:], v[:])
}

func jumpMapDelete(jm maps.Map, idx int) error {
	var k [4]byte

	binary.LittleEndian.PutUint32(k[:], uint32(idx))

	return jm.Delete(k[:])
}

func ipToU32(ip net.IP) uint32 {
	ip = ip.To4()
	return binary.LittleEndian.Uint32([]byte(ip[:]))
}

func tcUpdateJumpMap(obj *libbpf.Obj, progs []int, hasPolicyProg, hasHostConflictProg bool) error {
	for _, idx := range progs {
		switch idx {
		case
			tcdefs.ProgIndexPolicy,
			tcdefs.ProgIndexPolicyDebug:

			if !hasPolicyProg {
				continue
			}
		case
			tcdefs.ProgIndexHostCtConflict,
			tcdefs.ProgIndexHostCtConflictDebug:
			if !hasHostConflictProg {
				continue
			}
		}
		log.WithField("prog", tcdefs.ProgramNames[idx]).WithField("idx", idx).Debug("UpdateJumpMap")
		err := obj.UpdateJumpMap(progMap.GetName(), tcdefs.ProgramNames[idx], idx)
		if err != nil {
			return fmt.Errorf("error updating %s program: %w", tcdefs.ProgramNames[idx], err)
		}
	}

	return nil
}

func objLoad(fname, bpfFsDir, ipFamily string, topts testOpts, polProg, hasHostConflictProg bool) (*libbpf.Obj, error) {
	log.WithField("program", fname).Debug("Loading BPF program")

	forXDP := topts.xdp

	// XXX we do not need to create both sets of maps, but, well, who cares here ;-)
	progMap = hook.NewProgramsMap()
	policyJumpMap = jump.Map()
	progMapXDP = hook.NewXDPProgramsMap()
	policyJumpMapXDP = jump.XDPMap()
	if ipFamily == "preamble" {
		_ = unix.Unlink(progMap.Path())
		_ = unix.Unlink(policyJumpMap.Path())
		_ = unix.Unlink(progMapXDP.Path())
		_ = unix.Unlink(policyJumpMapXDP.Path())
	}
	err := progMap.EnsureExists()
	Expect(err).NotTo(HaveOccurred())
	err = policyJumpMap.EnsureExists()
	Expect(err).NotTo(HaveOccurred())
	err = progMapXDP.EnsureExists()
	Expect(err).NotTo(HaveOccurred())
	err = policyJumpMapXDP.EnsureExists()
	Expect(err).NotTo(HaveOccurred())

	obj, err := libbpf.OpenObject(fname)
	if err != nil {
		return nil, fmt.Errorf("open object %s: %w", fname, err)
	}

	for m, err := obj.FirstMap(); m != nil && err == nil; m, err = m.NextMap() {
		if m.IsMapInternal() {
			if strings.HasPrefix(m.Name(), ".rodata") {
				continue
			}
			if forXDP {
				var globals libbpf.XDPGlobalData
				for i := 0; i < 16; i++ {
					globals.Jumps[i] = uint32(i)
				}
				if topts.ipv6 {
					for i := 0; i < 16; i++ {
						globals.JumpsV6[i] = uint32(i)
					}
				}

				if err := xdp.ConfigureProgram(m, bpfIfaceName, &globals); err != nil {
					return nil, err
				}
			} else if topts.ipv6 {
				ifaceLog := topts.progLog + "-" + bpfIfaceName
				globals := libbpf.TcGlobalData{
					Tmtu:         natTunnelMTU,
					VxlanPort:    testVxlanPort,
					PSNatStart:   uint16(topts.psnaStart),
					PSNatLen:     uint16(topts.psnatEnd-topts.psnaStart) + 1,
					Flags:        libbpf.GlobalsNoDSRCidrs | libbpf.GlobalsRPFOptionStrict,
					LogFilterJmp: 0xffffffff,
				}

				copy(globals.HostTunnelIPv6[:], node1tunIPV6.To16())
				copy(globals.HostIPv6[:], hostIP.To16())
				copy(globals.IntfIPv6[:], intfIPV6.To16())

				for i := 0; i < tcdefs.ProgIndexEnd; i++ {
					globals.JumpsV6[i] = uint32(i)
				}

				log.WithField("globals", globals).Debugf("configure program v6")

				if err := tc.ConfigureProgram(m, ifaceLog, &globals); err != nil {
					return nil, fmt.Errorf("failed to configure tc program: %w", err)
				}
				log.WithField("program", fname).Debugf("Configured BPF program iface \"%s\"", ifaceLog)
			} else {
				ifaceLog := topts.progLog + "-" + bpfIfaceName
				globals := libbpf.TcGlobalData{
					Tmtu:         natTunnelMTU,
					VxlanPort:    testVxlanPort,
					PSNatStart:   uint16(topts.psnaStart),
					PSNatLen:     uint16(topts.psnatEnd-topts.psnaStart) + 1,
					Flags:        libbpf.GlobalsNoDSRCidrs,
					LogFilterJmp: 0xffffffff,
				}

				copy(globals.HostIPv4[0:4], hostIP)
				copy(globals.IntfIPv4[0:4], intfIP)
				copy(globals.HostTunnelIPv4[0:4], node1tunIP.To4())

				for i := 0; i < tcdefs.ProgIndexEnd; i++ {
					globals.Jumps[i] = uint32(i)
				}

				log.WithField("globals", globals).Debugf("configure program")

				if err := tc.ConfigureProgram(m, ifaceLog, &globals); err != nil {
					return nil, fmt.Errorf("failed to configure tc program: %w", err)
				}
				log.WithField("program", fname).Debugf("Configured BPF program iface \"%s\"", ifaceLog)
			}
			continue
		}
		pin := "/sys/fs/bpf/tc/globals/" + m.Name()
		log.WithFields(log.Fields{
			"pin":        pin,
			"key size":   m.KeySize(),
			"value size": m.ValueSize(),
		}).Debug("Pinning map")
		cmd := exec.Command("bpftool", "map", "show", "pinned", pin)
		log.WithField("cmd", cmd.String()).Debugf("executing")
		out, _ := cmd.Output()
		log.WithField("output", string(out)).Debug("map")
		log.WithField("size", m.MaxEntries()).Debug("libbpf map")
		if err := m.SetPinPath(pin); err != nil {
			obj.Close()
			return nil, fmt.Errorf("error pinning map %s: %w", m.Name(), err)
		}
	}

	if err := obj.Load(); err != nil {
		return nil, fmt.Errorf("load object: %w", err)
	}

	progDir := bpfFsDir
	policyIdx := tcdefs.ProgIndexPolicy

	err = obj.PinPrograms(progDir)
	if err != nil {
		obj.Close()
		return nil, fmt.Errorf("pin %s programs to %s: %w", ipFamily, progDir, err)
	}

	if polProg {
		polProgPath := "xdp_policy"
		if !forXDP {
			polProgPath = "classifier_tc_policy"
		}
		polProgPath = path.Join(bpfFsDir, polProgPath)
		_, err = os.Stat(polProgPath)
		if err == nil {
			m := policyJumpMap
			if forXDP {
				m = policyJumpMapXDP
			}
			err = jumpMapUpdatePinned(m, policyIdx, polProgPath)
			if err != nil {
				err = errors.Wrap(err, "failed to update jump map (policy program)")
				goto out
			}
			log.Debug("set default policy")
		}
	}

	if !forXDP {
		log.WithField("ipFamily", ipFamily).Debug("Updating jump map")
		err = tcUpdateJumpMap(obj, tcJumpMapIndexes[ipFamily], false, hasHostConflictProg)
		if err != nil && !strings.Contains(err.Error(), "error updating calico_tc_host_ct_conflict program") {
			goto out
		}
		err = tcUpdateJumpMap(obj, tcJumpMapIndexes[ipFamily], false, false)
	} else {
		if err = xdpUpdateJumpMap(obj, xdpJumpMapIndexes[ipFamily]); err != nil {
			goto out
		}
	}

out:
	if err != nil {
		_ = obj.UnpinPrograms(bpfFsDir)
		obj.Close()
		return nil, fmt.Errorf("%s: %w", ipFamily, err)
	}

	log.WithField("program", fname).Debug("Loaded BPF program")
	return obj, nil
}

func objUTLoad(fname, bpfFsDir, ipFamily string, topts testOpts, polProg, hasHostConflictProg bool) (*libbpf.Obj, error) {
	log.WithField("program", fname).Debug("Loading BPF UT program")

	obj, err := libbpf.OpenObject(fname)
	if err != nil {
		return nil, fmt.Errorf("open object %s: %w", fname, err)
	}

	for m, err := obj.FirstMap(); m != nil && err == nil; m, err = m.NextMap() {
		if m.IsMapInternal() {
			ifaceLog := topts.progLog + "-" + bpfIfaceName
			if topts.ipv6 {
				globals := libbpf.TcGlobalData{
					Tmtu:       natTunnelMTU,
					VxlanPort:  testVxlanPort,
					PSNatStart: uint16(topts.psnaStart),
					PSNatLen:   uint16(topts.psnatEnd-topts.psnaStart) + 1,
					Flags:      libbpf.GlobalsNoDSRCidrs,
				}

				copy(globals.HostTunnelIPv6[:], node1tunIPV6.To16())
				copy(globals.HostIPv6[:], hostIP.To16())
				copy(globals.IntfIPv6[:], intfIPV6.To16())

				if err := tc.ConfigureProgram(m, ifaceLog, &globals); err != nil {
					return nil, fmt.Errorf("failed to configure v6 tc program: %w", err)
				}
			} else {
				globals := libbpf.TcGlobalData{
					Tmtu:       natTunnelMTU,
					VxlanPort:  testVxlanPort,
					PSNatStart: uint16(topts.psnaStart),
					PSNatLen:   uint16(topts.psnatEnd-topts.psnaStart) + 1,
					Flags:      libbpf.GlobalsNoDSRCidrs,
				}
				copy(globals.HostTunnelIPv4[0:4], node1tunIP.To4())
				copy(globals.HostIPv4[0:4], hostIP.To4())
				copy(globals.IntfIPv4[0:4], intfIP.To4())

				if err := tc.ConfigureProgram(m, ifaceLog, &globals); err != nil {
					return nil, fmt.Errorf("failed to configure tc program: %w", err)
				}
			}
			break
		}
	}

	if err := obj.Load(); err != nil {
		return nil, fmt.Errorf("load object: %w", err)
	}

	progDir := bpfFsDir

	err = obj.PinPrograms(progDir)
	if err != nil {
		obj.Close()
		return nil, fmt.Errorf("pin %s programs to %s: %w", ipFamily, progDir, err)
	}

	log.WithField("program", fname).Debug("Loaded BPF UT program")
	return obj, nil
}

func xdpUpdateJumpMap(obj *libbpf.Obj, progs map[int]string) error {
	for idx, name := range progs {
		err := obj.UpdateJumpMap(hook.NewXDPProgramsMap().GetName(), name, idx)
		if err != nil {
			return fmt.Errorf("failed to update program '%s' at index %d: %w", name, idx, err)
		}
		log.Debugf("xdp set program '%s' at index %d", name, idx)
	}

	return nil
}

type bpfRunResult struct {
	Retval   int
	Duration int
	dataOut  []byte
}

func (r bpfRunResult) RetvalStr() string {
	s := retvalToStr[r.Retval]
	if s == "" {
		return fmt.Sprint(r.Retval)
	}
	return s
}

func (r bpfRunResult) RetvalStrXDP() string {
	s := retvalToStrXDP[r.Retval]
	if s == "" {
		return fmt.Sprint(r.Retval)
	}
	return s
}

func bpftoolProgRun(progName string, dataIn, ctxIn []byte) (bpfRunResult, error) {
	return bpftoolProgRunN(progName, dataIn, ctxIn, 1)
}

func bpftoolProgRunN(progName string, dataIn, ctxIn []byte, N int) (bpfRunResult, error) {
	var res bpfRunResult

	tempDir, err := os.MkdirTemp("", "bpftool-data-")
	Expect(err).NotTo(HaveOccurred())

	defer os.RemoveAll(tempDir)

	dataInFname := tempDir + "/data_in"
	dataOutFname := tempDir + "/data_out"

	ctxInFname := tempDir + "/ctx_in"
	ctxOutFname := tempDir + "/ctx_out"

	if err := os.WriteFile(dataInFname, dataIn, 0644); err != nil {
		return res, errors.Errorf("failed to write input data in file: %s", err)
	}

	if ctxIn != nil {
		if err := os.WriteFile(ctxInFname, ctxIn, 0644); err != nil {
			return res, errors.Errorf("failed to write input ctx in file: %s", err)
		}
	}

	args := []string{"prog", "run", "pinned", progName, "data_in", dataInFname, "data_out", dataOutFname}
	if ctxIn != nil {
		args = append(args, "ctx_in", ctxInFname, "ctx_out", ctxOutFname)
	}
	if N > 1 {
		args = append(args, "repeat", fmt.Sprintf("%d", N))
	}

	out, err := bpftool(args...)
	if err != nil {
		return res, err
	}

	if err := json.Unmarshal(out, &res); err != nil {
		return res, errors.Errorf("failed to unmarshall json: %s", err)
	}

	res.dataOut, err = os.ReadFile(dataOutFname)
	if err != nil {
		return res, errors.Errorf("failed to read output data from file: %s", err)
	}

	if ctxIn != nil {
		ctxOut, err := os.ReadFile(ctxOutFname)
		if err != nil {
			return res, errors.Errorf("failed to read output ctx from file: %s", err)
		}
		skbMark = binary.LittleEndian.Uint32(ctxOut[2*4 : 3*4])
	}

	return res, nil
}

type bpfProgRunFn func(data []byte) (bpfRunResult, error)

// runBpfUnitTest runs a small unit in isolation. It requires a small .c file
// that wraps the unit and compiles into a calico_unittest section.
func runBpfUnitTest(t *testing.T, source string, testFn func(bpfProgRunFn), opts ...testOption) {
	RegisterTestingT(t)

	topts := testOpts{
		subtests: true,
		logLevel: log.DebugLevel,
	}

	for _, o := range opts {
		o(&topts)
	}

	loglevel := log.GetLevel()
	if topts.logLevel != loglevel {
		defer log.SetLevel(loglevel)
		log.SetLevel(topts.logLevel)
	}

	tempDir, err := os.MkdirTemp("", "calico-bpf-")
	Expect(err).NotTo(HaveOccurred())
	defer os.RemoveAll(tempDir)

	unique := path.Base(tempDir)
	bpfFsDir := "/sys/fs/bpf/" + unique

	err = os.Mkdir(bpfFsDir, os.ModePerm)
	Expect(err).NotTo(HaveOccurred())
	defer os.RemoveAll(bpfFsDir)

	vExt := ""
	if topts.ipv6 {
		vExt = "_v6"
	}

	objFname := "../../bpf-gpl/ut/" + strings.TrimSuffix(source, path.Ext(source)) + vExt + ".o"
	if topts.objname != "" {
		objFname = "../../bpf-gpl/ut/" + topts.objname
	}

	obj, err := objUTLoad(objFname, bpfFsDir, "IPv4", topts, true, false)
	Expect(err).NotTo(HaveOccurred())
	defer func() { _ = obj.UnpinPrograms(bpfFsDir) }()
	defer obj.Close()

	ctxIn := make([]byte, 18*4)

	runTest := func() {
		testFn(func(dataIn []byte) (bpfRunResult, error) {
			res, err := bpftoolProgRun(bpfFsDir+"/unittest", dataIn, ctxIn)
			log.Debugf("dataIn  = %+v", dataIn)
			if err == nil {
				log.Debugf("dataOut = %+v", res.dataOut)
			}
			return res, err
		})
	}

	if topts.subtests {
		t.Run(source, func(_ *testing.T) {
			runTest()
		})
	} else {
		runTest()
	}
}

type testOpts struct {
	description   string
	subtests      bool
	logLevel      log.Level
	xdp           bool
	psnaStart     uint32
	psnatEnd      uint32
	hostNetworked bool
	progLog       string
	ipv6          bool
	objname       string
}

type testOption func(opts *testOpts)

func withSubtests(v bool) testOption {
	return func(o *testOpts) {
		o.subtests = v
	}
}

var _ = withSubtests

func withLogLevel(l log.Level) testOption {
	return func(o *testOpts) {
		o.logLevel = l
	}
}

var _ = withLogLevel

func withXDP() testOption {
	return func(o *testOpts) {
		o.xdp = true
	}
}

func withPSNATPorts(start, end uint16) testOption {
	return func(o *testOpts) {
		o.psnaStart = uint32(start)
		o.psnatEnd = uint32(end)
	}
}

func withHostNetworked() testOption {
	return func(o *testOpts) {
		o.hostNetworked = true
	}
}

func withIPv6() testOption {
	return func(o *testOpts) {
		o.ipv6 = true
	}
}

func withObjName(name string) testOption {
	return func(o *testOpts) {
		o.objname = name
	}
}

func withDescription(desc string) testOption {
	return func(o *testOpts) {
		o.description = desc
	}
}

// layersMatchFields matches all Exported fields and ignore the ones explicitly
// listed. It always ignores BaseLayer as that is not set by the tests.
func layersMatchFields(l gopacket.Layer, ignore ...string) types.GomegaMatcher {
	toIgnore := make(map[string]bool)
	for _, x := range ignore {
		toIgnore[x] = true
	}

	toIgnore["BaseLayer"] = true

	f := Fields{}
	v := reflect.Indirect(reflect.ValueOf(l))
	if v.Kind() != reflect.Struct {
		return Reject()
	}

	for i := 0; i < v.NumField(); i++ {
		name := v.Type().Field(i).Name
		if !toIgnore[name] && v.Field(i).CanInterface() {
			val := v.Field(i).Interface()
			f[name] = Equal(val)
		}
	}

	return PointTo(MatchFields(IgnoreMissing|IgnoreExtras, f))
}

func udpResponseRaw(in []byte) []byte {
	pkt := gopacket.NewPacket(in, layers.LayerTypeEthernet, gopacket.Default)
	ethL := pkt.Layer(layers.LayerTypeEthernet)
	ethR := ethL.(*layers.Ethernet)
	ethR.SrcMAC, ethR.DstMAC = ethR.DstMAC, ethR.SrcMAC

	ipv4L := pkt.Layer(layers.LayerTypeIPv4)
	ipv4R := ipv4L.(*layers.IPv4)
	ipv4R.SrcIP, ipv4R.DstIP = ipv4R.DstIP, ipv4R.SrcIP

	udpL := pkt.Layer(layers.LayerTypeUDP)
	udpR := udpL.(*layers.UDP)
	udpR.SrcPort, udpR.DstPort = udpR.DstPort, udpR.SrcPort

	_ = udpR.SetNetworkLayerForChecksum(ipv4R)

	out := gopacket.NewSerializeBuffer()
	err := gopacket.SerializeLayers(out, gopacket.SerializeOptions{ComputeChecksums: true},
		ethR, ipv4R, udpR, gopacket.Payload(pkt.ApplicationLayer().Payload()))
	Expect(err).NotTo(HaveOccurred())

	return out.Bytes()
}

func udpResponseRawV6(in []byte) []byte {
	pkt := gopacket.NewPacket(in, layers.LayerTypeEthernet, gopacket.Default)
	ethL := pkt.Layer(layers.LayerTypeEthernet)
	ethR := ethL.(*layers.Ethernet)
	ethR.SrcMAC, ethR.DstMAC = ethR.DstMAC, ethR.SrcMAC

	ipv6L := pkt.Layer(layers.LayerTypeIPv6)
	ipv6R := ipv6L.(*layers.IPv6)
	ipv6R.SrcIP, ipv6R.DstIP = ipv6R.DstIP, ipv6R.SrcIP

	lrs := []gopacket.SerializableLayer{ethR, ipv6R}

	if ipv6R.NextHeader == layers.IPProtocolIPv6HopByHop {
		l := pkt.Layer(layers.LayerTypeIPv6HopByHop)
		lrs = append(lrs, l.(*layers.IPv6HopByHop))
	}

	udpL := pkt.Layer(layers.LayerTypeUDP)
	udpR := udpL.(*layers.UDP)
	udpR.SrcPort, udpR.DstPort = udpR.DstPort, udpR.SrcPort

	_ = udpR.SetNetworkLayerForChecksum(ipv6R)

	lrs = append(lrs, udpR, gopacket.Payload(pkt.ApplicationLayer().Payload()))

	out := gopacket.NewSerializeBuffer()
	err := gopacket.SerializeLayers(out, gopacket.SerializeOptions{ComputeChecksums: true}, lrs...)
	Expect(err).NotTo(HaveOccurred())

	return out.Bytes()
}

func tcpResponseRaw(in []byte) []byte {
	pkt := gopacket.NewPacket(in, layers.LayerTypeEthernet, gopacket.Default)
	ethL := pkt.Layer(layers.LayerTypeEthernet)
	ethR := ethL.(*layers.Ethernet)
	ethR.SrcMAC, ethR.DstMAC = ethR.DstMAC, ethR.SrcMAC

	ipv4L := pkt.Layer(layers.LayerTypeIPv4)
	ipv4R := ipv4L.(*layers.IPv4)
	ipv4R.SrcIP, ipv4R.DstIP = ipv4R.DstIP, ipv4R.SrcIP

	tcpL := pkt.Layer(layers.LayerTypeTCP)
	tcpR := tcpL.(*layers.TCP)
	tcpR.SrcPort, tcpR.DstPort = tcpR.DstPort, tcpR.SrcPort

	if tcpR.SYN {
		tcpR.ACK = true
	}

	_ = tcpR.SetNetworkLayerForChecksum(ipv4R)

	out := gopacket.NewSerializeBuffer()
	err := gopacket.SerializeLayers(out, gopacket.SerializeOptions{ComputeChecksums: true},
		ethR, ipv4R, tcpR, gopacket.Payload(pkt.ApplicationLayer().Payload()))
	Expect(err).NotTo(HaveOccurred())

	return out.Bytes()
}

func dumpNATMap(natMap maps.Map) {
	nt, err := nat.LoadFrontendMap(natMap)
	Expect(err).NotTo(HaveOccurred())
	for k, v := range nt {
		fmt.Printf("%s : %s\n", k, v)
	}
}

func dumpNATMapV6(natMap maps.Map) {
	nt, err := nat.LoadFrontendMapV6(natMap)
	Expect(err).NotTo(HaveOccurred())
	for k, v := range nt {
		fmt.Printf("%s : %s\n", k, v)
	}
}

func resetMap(m maps.Map) {
	err := m.Iter(func(_, _ []byte) maps.IteratorAction {
		return maps.IterDelete
	})
	Expect(err).NotTo(HaveOccurred())
}

func dumpCTMap(ctMap maps.Map) {
	ct, err := conntrack.LoadMapMem(ctMap)
	Expect(err).NotTo(HaveOccurred())
	fmt.Printf("Conntrack dump:\n")
	for k, v := range ct {
		fmt.Printf("- %s : %s\n", k, v)
	}
	fmt.Printf("\n")
}

func dumpCTMapV6(ctMap maps.Map) {
	ct, err := conntrack.LoadMapMemV6(ctMap)
	Expect(err).NotTo(HaveOccurred())
	fmt.Printf("Conntrack dump:\n")
	for k, v := range ct {
		fmt.Printf("- %s : %s\n", k, v)
	}
	fmt.Printf("\n")
}

func resetCTMap(ctMap maps.Map) {
	resetMap(ctMap)
}

func resetCTMapV6(ctMap maps.Map) {
	resetMap(ctMap)
}

func saveCTMap(ctMap maps.Map) conntrack.MapMem {
	ct, err := conntrack.LoadMapMem(ctMap)
	Expect(err).NotTo(HaveOccurred())
	return ct
}

func saveCTMapV6(ctMap maps.Map) conntrack.MapMemV6 {
	ct, err := conntrack.LoadMapMemV6(ctMap)
	Expect(err).NotTo(HaveOccurred())
	return ct
}

func restoreCTMap(ctMap maps.Map, m conntrack.MapMem) {
	for k, v := range m {
		err := ctMap.Update(k[:], v[:])
		Expect(err).NotTo(HaveOccurred())
	}
}

func restoreCTMapV6(ctMap maps.Map, m conntrack.MapMemV6) {
	for k, v := range m {
		err := ctMap.Update(k[:], v[:])
		Expect(err).NotTo(HaveOccurred())
	}
}

func dumpRTMap(rtMap maps.Map) {
	rt, err := routes.LoadMap(rtMap)
	Expect(err).NotTo(HaveOccurred())
	for k, v := range rt {
		fmt.Printf("%15s: %s\n", k.Dest(), v)
	}
}

func dumpRTMapV6(rtMap maps.Map) {
	rt, err := routes.LoadMapV6(rtMap)
	Expect(err).NotTo(HaveOccurred())
	for k, v := range rt {
		fmt.Printf("%15s: %s\n", k.Dest(), v)
	}
}

func resetRTMap(rtMap maps.Map) {
	resetMap(rtMap)
}

func resetRTMapV6(rtMap maps.Map) {
	resetMap(rtMap)
}

func saveRTMap(rtMap maps.Map) routes.MapMem {
	rt, err := routes.LoadMap(rtMap)
	Expect(err).NotTo(HaveOccurred())
	return rt
}

func saveRTMapV6(rtMap maps.Map) routes.MapMemV6 {
	rt, err := routes.LoadMapV6(rtMap)
	Expect(err).NotTo(HaveOccurred())
	return rt
}

func restoreRTMap(rtMap maps.Map, m routes.MapMem) {
	for k, v := range m {
		err := rtMap.Update(k[:], v[:])
		Expect(err).NotTo(HaveOccurred())
	}
}

func restoreRTMapV6(rtMap maps.Map, m routes.MapMemV6) {
	for k, v := range m {
		err := rtMap.Update(k[:], v[:])
		Expect(err).NotTo(HaveOccurred())
	}
}

func dumpARPMap(arpMap maps.Map) {
	ct, err := arp.LoadMapMem(arpMap)
	Expect(err).NotTo(HaveOccurred())
	fmt.Printf("ARP dump:\n")
	for k, v := range ct {
		fmt.Printf("- %s : %s\n", k, v)
	}
	fmt.Printf("\n")
}

func dumpARPMapV6(arpMap maps.Map) {
	ct, err := arp.LoadMapMemV6(arpMap)
	Expect(err).NotTo(HaveOccurred())
	fmt.Printf("ARP dump:\n")
	for k, v := range ct {
		fmt.Printf("- %s : %s\n", k, v)
	}
	fmt.Printf("\n")
}

func saveARPMap(am maps.Map) arp.MapMem {
	m, err := arp.LoadMapMem(am)
	Expect(err).NotTo(HaveOccurred())
	return m
}

func saveARPMapV6(am maps.Map) arp.MapMemV6 {
	m, err := arp.LoadMapMemV6(am)
	Expect(err).NotTo(HaveOccurred())
	return m
}

var ethDefault = &layers.Ethernet{
	SrcMAC:       []byte{0, 0, 0, 0, 0, 1},
	DstMAC:       []byte{0, 0, 0, 0, 0, 2},
	EthernetType: layers.EthernetTypeIPv4,
}

var payloadDefault = []byte("ABCDEABCDEXXXXXXXXXXXX")

var srcIP = net.IPv4(1, 1, 1, 1)
var dstIP = net.IPv4(2, 2, 2, 2)
var srcV4CIDR = ip.CIDRFromNetIP(srcIP).(ip.V4CIDR)
var dstV4CIDR = ip.CIDRFromNetIP(dstIP).(ip.V4CIDR)

var ipv4Default = &layers.IPv4{
	Version:  4,
	IHL:      5,
	TTL:      64,
	Flags:    layers.IPv4DontFragment,
	SrcIP:    srcIP,
	DstIP:    dstIP,
	Protocol: layers.IPProtocolUDP,
}

var srcIPv6 = net.IP([]byte{0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1})
var dstIPv6 = net.IP([]byte{0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2})
var srcV6CIDR = ip.CIDRFromNetIP(srcIPv6).(ip.V6CIDR)
var dstV6CIDR = ip.CIDRFromNetIP(dstIPv6).(ip.V6CIDR)

var ipv6Default = &layers.IPv6{
	Version:    6,
	HopLimit:   64,
	SrcIP:      srcIPv6,
	DstIP:      dstIPv6,
	NextHeader: layers.IPProtocolUDP,
}

var udpDefault = &layers.UDP{
	SrcPort: 1234,
	DstPort: 5678,
}

func testPacket(family int, eth *layers.Ethernet, l3 gopacket.Layer, l4 gopacket.Layer,
	payload []byte, ipv6ext ...gopacket.SerializableLayer) (
	*layers.Ethernet, gopacket.Layer, gopacket.Layer, []byte, []byte, error) {
	pkt := Packet{
		family:  family,
		eth:     eth,
		l3:      l3,
		l4:      l4,
		payload: payload,
		ipv6ext: ipv6ext,
	}
	err := pkt.Generate()
	if err != nil {
		return nil, nil, nil, nil, nil, err
	}

	p := gopacket.NewPacket(pkt.bytes, layers.LayerTypeEthernet, gopacket.Default)
	fmt.Printf("p = %+v\n", p)

	e := p.Layer(layers.LayerTypeEthernet).(*layers.Ethernet)

	var (
		ipl   gopacket.Layer
		proto layers.IPProtocol
	)

	ipv4L := p.Layer(layers.LayerTypeIPv4)
	if ipv4L != nil {
		ipv4 := ipv4L.(*layers.IPv4)
		proto = ipv4.Protocol
		ipl = ipv4L
	} else {
		ipv6L := p.Layer(layers.LayerTypeIPv6)
		if ipv6L != nil {
			ipv6 := ipv6L.(*layers.IPv6)
			proto = ipv6.NextHeader
		}
		if proto == layers.IPProtocolIPv6HopByHop {
			l := p.Layer(layers.LayerTypeIPv6HopByHop)
			proto = l.(*layers.IPv6HopByHop).NextHeader
		}
		ipl = ipv6L
	}

	var l gopacket.Layer

	switch proto {
	case layers.IPProtocolUDP:
		l = p.Layer(layers.LayerTypeUDP)
	case layers.IPProtocolTCP:
		l = p.Layer(layers.LayerTypeTCP)
	case layers.IPProtocolICMPv4:
		l = p.Layer(layers.LayerTypeICMPv4)
	}

	return e, ipl, l, pkt.payload, pkt.bytes, err
}

func testPacketV4(eth *layers.Ethernet, ipv4 *layers.IPv4, l4 gopacket.Layer, payload []byte) (
	*layers.Ethernet, *layers.IPv4, gopacket.Layer, []byte, []byte, error) {
	e, ip4, l4, p, b, err := testPacket(4, eth, ipv4, l4, payload)
	return e, ip4.(*layers.IPv4), l4, p, b, err
}

func testPacketV6(eth *layers.Ethernet, ipv6 *layers.IPv6, l4 gopacket.Layer, payload []byte, ipv6ext ...gopacket.SerializableLayer) (
	*layers.Ethernet, *layers.IPv6, gopacket.Layer, []byte, []byte, error) {
	e, ip6, l4, p, b, err := testPacket(6, eth, ipv6, l4, payload, ipv6ext...)
	return e, ip6.(*layers.IPv6), l4, p, b, err
}

type Packet struct {
	family     int
	eth        *layers.Ethernet
	l3         gopacket.Layer
	ipv4       *layers.IPv4
	ipv6       *layers.IPv6
	l4         gopacket.Layer
	udp        *layers.UDP
	tcp        *layers.TCP
	icmp       *layers.ICMPv4
	icmpv6     *layers.ICMPv6
	payload    []byte
	bytes      []byte
	layers     []gopacket.SerializableLayer
	length     int
	l4Protocol layers.IPProtocol
	l3Protocol layers.EthernetType
	ipv6ext    []gopacket.SerializableLayer
}

func (pkt *Packet) handlePayload() {
	if pkt.payload == nil {
		pkt.payload = payloadDefault
	}
	pkt.length = len(pkt.payload)
	pkt.layers = []gopacket.SerializableLayer{gopacket.Payload(pkt.payload)}
}

func (pkt *Packet) handleL4() error {
	if pkt.l4 == nil {
		pkt.l4 = udpDefault
	}

	switch v := pkt.l4.(type) {
	case *layers.UDP:
		pkt.udp = v
		pkt.length += 8
		pkt.udp.Length = uint16(pkt.length)
		pkt.l4Protocol = layers.IPProtocolUDP
		pkt.layers = append(pkt.layers, pkt.udp)
	case *layers.TCP:
		pkt.tcp = v
		pkt.length += 20
		pkt.l4Protocol = layers.IPProtocolTCP
		pkt.layers = append(pkt.layers, pkt.tcp)
	case *layers.ICMPv4:
		pkt.icmp = v
		pkt.length += 8
		pkt.l4Protocol = layers.IPProtocolICMPv4
		pkt.layers = append(pkt.layers, pkt.icmp)
	case *layers.ICMPv6:
		pkt.icmpv6 = v
		pkt.length += 8
		pkt.l4Protocol = layers.IPProtocolICMPv6
		pkt.layers = append(pkt.layers, pkt.icmpv6)
	default:
		return errors.Errorf("unrecognized l4 layer type %t", pkt.l4)
	}
	return nil
}

func (pkt *Packet) handleIPv6Ext() error {
	exts := gopacket.NewSerializeBuffer()
	err := gopacket.SerializeLayers(exts, gopacket.SerializeOptions{FixLengths: true}, pkt.ipv6ext...)
	if err != nil {
		return err
	}

	pkt.length += len(exts.Bytes())

	return nil
}

func nextHdrIPProto(nh gopacket.Layer) layers.IPProtocol {
	switch nh.(type) {
	case *layers.IPv6HopByHop:
		return layers.IPProtocolIPv6HopByHop
	case *layers.ICMPv4:
		return layers.IPProtocolICMPv4
	case *layers.IGMP:
		return layers.IPProtocolIGMP
	case *layers.IPv4:
		return layers.IPProtocolIPv4
	case *layers.TCP:
		return layers.IPProtocolTCP
	case *layers.UDP:
		return layers.IPProtocolUDP
	case *layers.RUDP:
		return layers.IPProtocolRUDP
	case *layers.IPv6:
		return layers.IPProtocolIPv6
	case *layers.IPv6Routing:
		return layers.IPProtocolIPv6Routing
	case *layers.IPv6Fragment:
		return layers.IPProtocolIPv6Fragment
	case *layers.GRE:
		return layers.IPProtocolGRE
	case *layers.ICMPv6:
		return layers.IPProtocolICMPv6
	case *layers.IPv6Destination:
		return layers.IPProtocolIPv6Destination
	case *layers.EtherIP:
		return layers.IPProtocolEtherIP
	case *layers.SCTP:
		return layers.IPProtocolSCTP
	case *layers.UDPLite:
		return layers.IPProtocolUDPLite
	}

	panic("unknown next layer")
}

func (pkt *Packet) handleL3() error {
	if reflect.ValueOf(pkt.l3).IsNil() {
		if pkt.family == 4 {
			pkt.l3 = ipv4Default
		} else {
			pkt.l3 = ipv6Default
		}
	}

	switch v := pkt.l3.(type) {
	case *layers.IPv4:
		pkt.ipv4 = v
		pkt.length += int(v.IHL * 4)
		pkt.l3Protocol = layers.EthernetTypeIPv4
		pkt.ipv4.Protocol = pkt.l4Protocol
		pkt.ipv4.Length = uint16(pkt.length)
		pkt.layers = append(pkt.layers, pkt.ipv4)
	case *layers.IPv6:
		pkt.ipv6 = v
		pkt.l3Protocol = layers.EthernetTypeIPv6
		if len(pkt.ipv6ext) > 0 {
			if err := pkt.handleIPv6Ext(); err != nil {
				return fmt.Errorf("handling ipv6 extensions: %w", err)
			}
			pkt.ipv6.NextHeader = nextHdrIPProto(pkt.ipv6ext[0].(gopacket.Layer))
			for i := len(pkt.ipv6ext); i > 0; i-- {
				pkt.layers = append(pkt.layers, pkt.ipv6ext[i-1])
			}
		} else {
			pkt.ipv6.NextHeader = pkt.l4Protocol
		}
		pkt.length += 40
		pkt.ipv6.Length = uint16(pkt.length)
		pkt.layers = append(pkt.layers, pkt.ipv6)
	default:
		return errors.Errorf("unrecognized l3 layer type %t", pkt.l3)
	}
	return nil
}

func (pkt *Packet) handleEthernet() {
	if pkt.eth == nil {
		pkt.eth = ethDefault
	}
	pkt.eth.EthernetType = pkt.l3Protocol
	pkt.layers = append(pkt.layers, pkt.eth)
}

func (pkt *Packet) setChecksum() {
	switch pkt.l4Protocol {
	case layers.IPProtocolUDP:
		if pkt.l3Protocol == layers.EthernetTypeIPv6 {
			_ = pkt.udp.SetNetworkLayerForChecksum(pkt.ipv6)
		} else {
			_ = pkt.udp.SetNetworkLayerForChecksum(pkt.ipv4)
		}
	case layers.IPProtocolTCP:
		if pkt.l3Protocol == layers.EthernetTypeIPv6 {
			_ = pkt.tcp.SetNetworkLayerForChecksum(pkt.ipv6)
		} else {
			_ = pkt.tcp.SetNetworkLayerForChecksum(pkt.ipv4)
		}
	}
}

func (pkt *Packet) Generate() error {
	pkt.handlePayload()
	err := pkt.handleL4()
	if err != nil {
		return err
	}

	err = pkt.handleL3()
	if err != nil {
		return err
	}

	pkt.handleEthernet()
	pkt.setChecksum()
	pkt.bytes, err = generatePacket(pkt.layers)

	return err
}

func generatePacket(layers []gopacket.SerializableLayer) ([]byte, error) {
	pkt := gopacket.NewSerializeBuffer()
	err := gopacket.SerializeLayers(pkt, gopacket.SerializeOptions{ComputeChecksums: true})
	if err != nil {
		return nil, fmt.Errorf("Failed to init packet buffer: %w", err)
	}

	for i, layer := range layers {
		log.Infof("Layer %d: %v", i, layer)
		if layer == nil {
			continue
		}
		err = layer.SerializeTo(pkt, gopacket.SerializeOptions{ComputeChecksums: true})
		if err != nil {
			return nil, fmt.Errorf("Failed to serialize packet: %w", err)
		}
	}
	return pkt.Bytes(), nil
}

func testPacketUDPDefault() (*layers.Ethernet, *layers.IPv4, gopacket.Layer, []byte, []byte, error) {
	ip := *ipv4Default
	ip.Options = []layers.IPv4Option{{
		OptionType:   123,
		OptionLength: 6,
		OptionData:   []byte{0xde, 0xad, 0xbe, 0xef},
	}}
	ip.IHL += 2

	e, ip4, l4, p, b, err := testPacket(4, nil, &ip, nil, nil)
	return e, ip4.(*layers.IPv4), l4, p, b, err
}

func testPacketUDPDefaultNP(destIP net.IP) (*layers.Ethernet, *layers.IPv4, gopacket.Layer, []byte, []byte, error) {
	if destIP == nil {
		return testPacketUDPDefault()
	}

	ip := *ipv4Default
	ip.DstIP = destIP
	ip.Options = []layers.IPv4Option{{
		OptionType:   123,
		OptionLength: 6,
		OptionData:   []byte{0xde, 0xad, 0xbe, 0xef},
	}}
	ip.IHL += 2

	e, ip4, l4, p, b, err := testPacket(4, nil, &ip, nil, nil)
	return e, ip4.(*layers.IPv4), l4, p, b, err
}

func ipv6HopByHopExt() gopacket.SerializableLayer {
	hop := &layers.IPv6HopByHop{}
	hop.NextHeader = layers.IPProtocolUDP

	/* from gopacket ip6_test.go */
	tlv := &layers.IPv6HopByHopOption{}
	tlv.OptionType = 0x01 // PadN
	tlv.OptionData = []byte{0x00, 0x00, 0x00, 0x00}
	hop.Options = append(hop.Options, tlv)

	return hop
}

func testPacketUDPDefaultNPV6(destIP net.IP) (*layers.Ethernet, *layers.IPv6, gopacket.Layer, []byte, []byte, error) {
	if destIP == nil {
		return testPacketV6(nil, nil, nil, nil)
	}

	ip := *ipv6Default
	ip.DstIP = destIP

	hop := &layers.IPv6HopByHop{}
	hop.NextHeader = layers.IPProtocolUDP

	/* from gopacket ip6_test.go */
	tlv := &layers.IPv6HopByHopOption{}
	tlv.OptionType = 0x01 // PadN
	tlv.OptionData = []byte{0x00, 0x00, 0x00, 0x00}
	hop.Options = append(hop.Options, tlv)

	e, ip6, l4, p, b, err := testPacketV6(nil, &ip, nil, nil, hop)
	return e, ip6, l4, p, b, err
}

func resetBPFMaps() {
	resetCTMap(ctMap)
	resetRTMap(rtMap)
	resetMap(fsafeMap)
	resetMap(natMap)
	resetMap(natBEMap)
}

func TestMapIterWithDelete(t *testing.T) {
	RegisterTestingT(t)

	m := maps.NewPinnedMap(maps.MapParameters{
		Type:       "hash",
		KeySize:    8,
		ValueSize:  8,
		MaxEntries: 1000,
		Name:       "cali_tmap",
		Flags:      unix.BPF_F_NO_PREALLOC,
	})

	err := m.EnsureExists()
	Expect(err).NotTo(HaveOccurred())

	for i := 0; i < 10; i++ {
		var k, v [8]byte

		binary.LittleEndian.PutUint64(k[:], uint64(i))
		binary.LittleEndian.PutUint64(v[:], uint64(i*7))

		err := m.Update(k[:], v[:])
		Expect(err).NotTo(HaveOccurred())
	}

	out := make(map[uint64]uint64)

	cnt := 0
	err = m.Iter(func(K, V []byte) maps.IteratorAction {
		k := binary.LittleEndian.Uint64(K)
		v := binary.LittleEndian.Uint64(V)

		out[k] = v
		cnt++

		return maps.IterDelete
	})
	Expect(err).NotTo(HaveOccurred())

	Expect(cnt).To(Equal(10))

	for i := 0; i < 10; i++ {
		Expect(out).To(HaveKey(uint64(i)))
		Expect(out[uint64(i)]).To(Equal(uint64(i * 7)))
	}
}

func TestMapIterWithDeleteLastOfBatch(t *testing.T) {
	RegisterTestingT(t)

	m := maps.NewPinnedMap(maps.MapParameters{
		Type:       "hash",
		KeySize:    8,
		ValueSize:  8,
		MaxEntries: 1000,
		Name:       "cali_tmap",
		Flags:      unix.BPF_F_NO_PREALLOC,
	})

	err := m.EnsureExists()
	Expect(err).NotTo(HaveOccurred())

	for i := 0; i < 40; i++ {
		var k, v [8]byte

		binary.LittleEndian.PutUint64(k[:], uint64(i))
		binary.LittleEndian.PutUint64(v[:], uint64(i*7))

		err := m.Update(k[:], v[:])
		Expect(err).NotTo(HaveOccurred())
	}

	out := make(map[uint64]uint64)

	cnt := 0
	err = m.Iter(func(K, V []byte) maps.IteratorAction {
		k := binary.LittleEndian.Uint64(K)
		v := binary.LittleEndian.Uint64(V)

		out[k] = v

		cnt++
		// Delete the last of the first batch. Must not make the iteration to
		// restart from the beginning.
		if cnt == maps.IteratorNumKeys {
			return maps.IterDelete
		}
		return maps.IterNone
	})
	Expect(err).NotTo(HaveOccurred())

	Expect(cnt).To(Equal(40))

	for i := 0; i < 40; i++ {
		Expect(out).To(HaveKey(uint64(i)))
		Expect(out[uint64(i)]).To(Equal(uint64(i * 7)))
	}
}

func TestJumpMap(t *testing.T) {
	RegisterTestingT(t)

	jumpMapFD := progMap.MapFD()
	pg := polprog.NewBuilder(idalloc.New(), ipsMap.MapFD(), stateMap.MapFD(), jumpMapFD, policyJumpMap.MapFD(),
		polprog.WithAllowDenyJumps(tcdefs.ProgIndexAllowed, tcdefs.ProgIndexDrop))
	rules := polprog.Rules{}
	insns, err := pg.Instructions(rules)
	Expect(err).NotTo(HaveOccurred())
	Expect(insns).To(HaveLen(1))
	progFD, err := bpf.LoadBPFProgramFromInsns(insns[0], "calico_policy", "Apache-2.0", unix.BPF_PROG_TYPE_SCHED_CLS)
	Expect(err).NotTo(HaveOccurred())

	k := make([]byte, 4)
	v := make([]byte, 4)
	binary.LittleEndian.PutUint32(v, uint32(progFD))

	err = maps.UpdateMapEntry(jumpMapFD, k, v)
	Expect(err).NotTo(HaveOccurred())

	err = maps.DeleteMapEntry(jumpMapFD, k)
	Expect(err).NotTo(HaveOccurred())

	err = maps.UpdateMapEntry(jumpMapFD, k, v)
	Expect(err).NotTo(HaveOccurred())

	err = maps.DeleteMapEntryIfExists(jumpMapFD, k)
	Expect(err).NotTo(HaveOccurred())

	err = maps.DeleteMapEntryIfExists(jumpMapFD, k)
	Expect(err).NotTo(HaveOccurred())

	err = maps.DeleteMapEntry(jumpMapFD, k)
	Expect(err).To(HaveOccurred())
}
