// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

//go:build linux

package linuxfw

import (
	"bytes"
	"fmt"
	"net/netip"
	"os"
	"runtime"
	"strings"
	"testing"

	"github.com/google/nftables"
	"github.com/google/nftables/expr"
	"github.com/mdlayher/netlink"
	"github.com/vishvananda/netns"
	"tailscale.com/net/tsaddr"
)

// nfdump returns a hexdump of 4 bytes per line (like nft --debug=all), allowing
// users to make sense of large byte literals more easily.
func nfdump(b []byte) string {
	var buf bytes.Buffer
	i := 0
	for ; i < len(b); i += 4 {
		// TODO: show printable characters as ASCII
		fmt.Fprintf(&buf, "%02x %02x %02x %02x\n",
			b[i],
			b[i+1],
			b[i+2],
			b[i+3])
	}
	for ; i < len(b); i++ {
		fmt.Fprintf(&buf, "%02x ", b[i])
	}
	return buf.String()
}

func TestMaskof(t *testing.T) {
	pfx, err := netip.ParsePrefix("192.168.1.0/24")
	if err != nil {
		t.Fatal(err)
	}
	want := []byte{0xff, 0xff, 0xff, 0x00}
	if got := maskof(pfx); !bytes.Equal(got, want) {
		t.Errorf("got %v; want %v", got, want)
	}
}

// linediff returns a side-by-side diff of two nfdump() return values, flagging
// lines which are not equal with an exclamation point prefix.
func linediff(a, b string) string {
	var buf bytes.Buffer
	fmt.Fprintf(&buf, "got -- want\n")
	linesA := strings.Split(a, "\n")
	linesB := strings.Split(b, "\n")
	for idx, lineA := range linesA {
		if idx >= len(linesB) {
			break
		}
		lineB := linesB[idx]
		prefix := "! "
		if lineA == lineB {
			prefix = "  "
		}
		fmt.Fprintf(&buf, "%s%s -- %s\n", prefix, lineA, lineB)
	}
	return buf.String()
}

func newTestConn(t *testing.T, want [][]byte) *nftables.Conn {
	conn, err := nftables.New(nftables.WithTestDial(
		func(req []netlink.Message) ([]netlink.Message, error) {
			for idx, msg := range req {
				b, err := msg.MarshalBinary()
				if err != nil {
					t.Fatal(err)
				}
				if len(b) < 16 {
					continue
				}
				b = b[16:]
				if len(want) == 0 {
					t.Errorf("no want entry for message %d: %x", idx, b)
					continue
				}
				if got, want := b, want[0]; !bytes.Equal(got, want) {
					t.Errorf("message %d: %s", idx, linediff(nfdump(got), nfdump(want)))
				}
				want = want[1:]
			}
			return req, nil
		}))
	if err != nil {
		t.Fatal(err)
	}
	return conn
}

func TestInsertHookRule(t *testing.T) {
	proto := nftables.TableFamilyIPv4
	want := [][]byte{
		// batch begin
		[]byte("\x00\x00\x00\x0a"),
		// nft add table ip ts-filter-test
		[]byte("\x02\x00\x00\x00\x13\x00\x01\x00\x74\x73\x2d\x66\x69\x6c\x74\x65\x72\x2d\x74\x65\x73\x74\x00\x00\x08\x00\x02\x00\x00\x00\x00\x00"),
		// nft add chain ip ts-filter-test ts-input-test { type filter hook input priority 0 \; }
		[]byte("\x02\x00\x00\x00\x13\x00\x01\x00\x74\x73\x2d\x66\x69\x6c\x74\x65\x72\x2d\x74\x65\x73\x74\x00\x00\x12\x00\x03\x00\x74\x73\x2d\x69\x6e\x70\x75\x74\x2d\x74\x65\x73\x74\x00\x00\x00\x14\x00\x04\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x00\x0b\x00\x07\x00\x66\x69\x6c\x74\x65\x72\x00\x00"),
		// nft add chain ip ts-filter-test ts-jumpto
		[]byte("\x02\x00\x00\x00\x13\x00\x01\x00\x74\x73\x2d\x66\x69\x6c\x74\x65\x72\x2d\x74\x65\x73\x74\x00\x00\x0e\x00\x03\x00\x74\x73\x2d\x6a\x75\x6d\x70\x74\x6f\x00\x00\x00"),
		// nft add rule ip ts-filter-test ts-input-test counter jump ts-jumptp
		[]byte("\x02\x00\x00\x00\x13\x00\x01\x00\x74\x73\x2d\x66\x69\x6c\x74\x65\x72\x2d\x74\x65\x73\x74\x00\x00\x12\x00\x02\x00\x74\x73\x2d\x69\x6e\x70\x75\x74\x2d\x74\x65\x73\x74\x00\x00\x00\x70\x00\x04\x80\x2c\x00\x01\x80\x0c\x00\x01\x00\x63\x6f\x75\x6e\x74\x65\x72\x00\x1c\x00\x02\x80\x0c\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x40\x00\x01\x80\x0e\x00\x01\x00\x69\x6d\x6d\x65\x64\x69\x61\x74\x65\x00\x00\x00\x2c\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x00\x20\x00\x02\x80\x1c\x00\x02\x80\x08\x00\x01\x00\xff\xff\xff\xfd\x0e\x00\x02\x00\x74\x73\x2d\x6a\x75\x6d\x70\x74\x6f\x00\x00\x00"),
		// batch end
		[]byte("\x00\x00\x00\x0a"),
	}
	testConn := newTestConn(t, want)
	table := testConn.AddTable(&nftables.Table{
		Family: proto,
		Name:   "ts-filter-test",
	})

	fromchain := testConn.AddChain(&nftables.Chain{
		Name:     "ts-input-test",
		Table:    table,
		Type:     nftables.ChainTypeFilter,
		Hooknum:  nftables.ChainHookInput,
		Priority: nftables.ChainPriorityFilter,
	})

	tochain := testConn.AddChain(&nftables.Chain{
		Name:  "ts-jumpto",
		Table: table,
	})

	err := addHookRule(testConn, table, fromchain, tochain.Name)
	if err != nil {
		t.Fatal(err)
	}

}

func TestInsertLoopbackRule(t *testing.T) {
	proto := nftables.TableFamilyIPv4
	want := [][]byte{
		// batch begin
		[]byte("\x00\x00\x00\x0a"),
		// nft add table ip ts-filter-test
		[]byte("\x02\x00\x00\x00\x13\x00\x01\x00\x74\x73\x2d\x66\x69\x6c\x74\x65\x72\x2d\x74\x65\x73\x74\x00\x00\x08\x00\x02\x00\x00\x00\x00\x00"),
		// nft add chain ip ts-filter-test ts-input-test { type filter hook input priority 0 \; }
		[]byte("\x02\x00\x00\x00\x13\x00\x01\x00\x74\x73\x2d\x66\x69\x6c\x74\x65\x72\x2d\x74\x65\x73\x74\x00\x00\x12\x00\x03\x00\x74\x73\x2d\x69\x6e\x70\x75\x74\x2d\x74\x65\x73\x74\x00\x00\x00\x14\x00\x04\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x00\x0b\x00\x07\x00\x66\x69\x6c\x74\x65\x72\x00\x00"),
		// nft add rule ip ts-filter-test ts-input-test iifname "lo" ip saddr 192.168.0.2 counter accept
		[]byte("\x02\x00\x00\x00\x13\x00\x01\x00\x74\x73\x2d\x66\x69\x6c\x74\x65\x72\x2d\x74\x65\x73\x74\x00\x00\x12\x00\x02\x00\x74\x73\x2d\x69\x6e\x70\x75\x74\x2d\x74\x65\x73\x74\x00\x00\x00\x10\x01\x04\x80\x24\x00\x01\x80\x09\x00\x01\x00\x6d\x65\x74\x61\x00\x00\x00\x00\x14\x00\x02\x80\x08\x00\x02\x00\x00\x00\x00\x06\x08\x00\x01\x00\x00\x00\x00\x01\x2c\x00\x01\x80\x08\x00\x01\x00\x63\x6d\x70\x00\x20\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x00\x0c\x00\x03\x80\x06\x00\x01\x00\x6c\x6f\x00\x00\x34\x00\x01\x80\x0c\x00\x01\x00\x70\x61\x79\x6c\x6f\x61\x64\x00\x24\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x01\x08\x00\x03\x00\x00\x00\x00\x0c\x08\x00\x04\x00\x00\x00\x00\x04\x2c\x00\x01\x80\x08\x00\x01\x00\x63\x6d\x70\x00\x20\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x00\x0c\x00\x03\x80\x08\x00\x01\x00\xc0\xa8\x00\x02\x2c\x00\x01\x80\x0c\x00\x01\x00\x63\x6f\x75\x6e\x74\x65\x72\x00\x1c\x00\x02\x80\x0c\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x30\x00\x01\x80\x0e\x00\x01\x00\x69\x6d\x6d\x65\x64\x69\x61\x74\x65\x00\x00\x00\x1c\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x00\x10\x00\x02\x80\x0c\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01"),
		// batch end
		[]byte("\x00\x00\x00\x0a"),
	}
	testConn := newTestConn(t, want)
	table := testConn.AddTable(&nftables.Table{
		Family: proto,
		Name:   "ts-filter-test",
	})

	chain := testConn.AddChain(&nftables.Chain{
		Name:     "ts-input-test",
		Table:    table,
		Type:     nftables.ChainTypeFilter,
		Hooknum:  nftables.ChainHookInput,
		Priority: nftables.ChainPriorityFilter,
	})

	addr := netip.MustParseAddr("192.168.0.2")

	err := insertLoopbackRule(testConn, proto, table, chain, addr)
	if err != nil {
		t.Fatal(err)
	}
}

func TestInsertLoopbackRuleV6(t *testing.T) {
	protoV6 := nftables.TableFamilyIPv6
	want := [][]byte{
		// batch begin
		[]byte("\x00\x00\x00\x0a"),
		// nft add table ip6 ts-filter-test
		[]byte("\x0a\x00\x00\x00\x13\x00\x01\x00\x74\x73\x2d\x66\x69\x6c\x74\x65\x72\x2d\x74\x65\x73\x74\x00\x00\x08\x00\x02\x00\x00\x00\x00\x00"),
		// nft add chain ip6 ts-filter-test ts-input-test { type filter hook input priority 0\; }
		[]byte("\x0a\x00\x00\x00\x13\x00\x01\x00\x74\x73\x2d\x66\x69\x6c\x74\x65\x72\x2d\x74\x65\x73\x74\x00\x00\x12\x00\x03\x00\x74\x73\x2d\x69\x6e\x70\x75\x74\x2d\x74\x65\x73\x74\x00\x00\x00\x14\x00\x04\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x00\x0b\x00\x07\x00\x66\x69\x6c\x74\x65\x72\x00\x00"),
		// nft add rule ip6 ts-filter-test ts-input-test iifname "lo" ip6 addr 2001:db8::1 counter accept
		[]byte("\x0a\x00\x00\x00\x13\x00\x01\x00\x74\x73\x2d\x66\x69\x6c\x74\x65\x72\x2d\x74\x65\x73\x74\x00\x00\x12\x00\x02\x00\x74\x73\x2d\x69\x6e\x70\x75\x74\x2d\x74\x65\x73\x74\x00\x00\x00\x1c\x01\x04\x80\x24\x00\x01\x80\x09\x00\x01\x00\x6d\x65\x74\x61\x00\x00\x00\x00\x14\x00\x02\x80\x08\x00\x02\x00\x00\x00\x00\x06\x08\x00\x01\x00\x00\x00\x00\x01\x2c\x00\x01\x80\x08\x00\x01\x00\x63\x6d\x70\x00\x20\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x00\x0c\x00\x03\x80\x06\x00\x01\x00\x6c\x6f\x00\x00\x34\x00\x01\x80\x0c\x00\x01\x00\x70\x61\x79\x6c\x6f\x61\x64\x00\x24\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x01\x08\x00\x03\x00\x00\x00\x00\x08\x08\x00\x04\x00\x00\x00\x00\x10\x38\x00\x01\x80\x08\x00\x01\x00\x63\x6d\x70\x00\x2c\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x00\x18\x00\x03\x80\x14\x00\x01\x00\x20\x01\x0d\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x2c\x00\x01\x80\x0c\x00\x01\x00\x63\x6f\x75\x6e\x74\x65\x72\x00\x1c\x00\x02\x80\x0c\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x30\x00\x01\x80\x0e\x00\x01\x00\x69\x6d\x6d\x65\x64\x69\x61\x74\x65\x00\x00\x00\x1c\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x00\x10\x00\x02\x80\x0c\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01"),
		// batch end
		[]byte("\x00\x00\x00\x0a"),
	}
	testConn := newTestConn(t, want)
	tableV6 := testConn.AddTable(&nftables.Table{
		Family: protoV6,
		Name:   "ts-filter-test",
	})

	chainV6 := testConn.AddChain(&nftables.Chain{
		Name:     "ts-input-test",
		Table:    tableV6,
		Type:     nftables.ChainTypeFilter,
		Hooknum:  nftables.ChainHookInput,
		Priority: nftables.ChainPriorityFilter,
	})

	addrV6 := netip.MustParseAddr("2001:db8::1")

	err := insertLoopbackRule(testConn, protoV6, tableV6, chainV6, addrV6)
	if err != nil {
		t.Fatal(err)
	}
}

func TestAddReturnChromeOSVMRangeRule(t *testing.T) {
	proto := nftables.TableFamilyIPv4
	want := [][]byte{
		// batch begin
		[]byte("\x00\x00\x00\x0a"),
		// nft add table ip ts-filter-test
		[]byte("\x02\x00\x00\x00\x13\x00\x01\x00\x74\x73\x2d\x66\x69\x6c\x74\x65\x72\x2d\x74\x65\x73\x74\x00\x00\x08\x00\x02\x00\x00\x00\x00\x00"),
		// nft add chain ip ts-filter-test ts-input-test { type filter hook input priority 0\; }
		[]byte("\x02\x00\x00\x00\x13\x00\x01\x00\x74\x73\x2d\x66\x69\x6c\x74\x65\x72\x2d\x74\x65\x73\x74\x00\x00\x12\x00\x03\x00\x74\x73\x2d\x69\x6e\x70\x75\x74\x2d\x74\x65\x73\x74\x00\x00\x00\x14\x00\x04\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x00\x0b\x00\x07\x00\x66\x69\x6c\x74\x65\x72\x00\x00"),
		// nft add rule ip ts-filter-test ts-input-test iifname != "testTunn" ip saddr 100.115.92.0/23 counter return
		[]byte("\x02\x00\x00\x00\x13\x00\x01\x00\x74\x73\x2d\x66\x69\x6c\x74\x65\x72\x2d\x74\x65\x73\x74\x00\x00\x12\x00\x02\x00\x74\x73\x2d\x69\x6e\x70\x75\x74\x2d\x74\x65\x73\x74\x00\x00\x00\x58\x01\x04\x80\x24\x00\x01\x80\x09\x00\x01\x00\x6d\x65\x74\x61\x00\x00\x00\x00\x14\x00\x02\x80\x08\x00\x02\x00\x00\x00\x00\x06\x08\x00\x01\x00\x00\x00\x00\x01\x30\x00\x01\x80\x08\x00\x01\x00\x63\x6d\x70\x00\x24\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x01\x10\x00\x03\x80\x0c\x00\x01\x00\x74\x65\x73\x74\x54\x75\x6e\x6e\x34\x00\x01\x80\x0c\x00\x01\x00\x70\x61\x79\x6c\x6f\x61\x64\x00\x24\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x01\x08\x00\x03\x00\x00\x00\x00\x0c\x08\x00\x04\x00\x00\x00\x00\x04\x44\x00\x01\x80\x0c\x00\x01\x00\x62\x69\x74\x77\x69\x73\x65\x00\x34\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x01\x08\x00\x03\x00\x00\x00\x00\x04\x0c\x00\x04\x80\x08\x00\x01\x00\xff\xff\xfe\x00\x0c\x00\x05\x80\x08\x00\x01\x00\x00\x00\x00\x00\x2c\x00\x01\x80\x08\x00\x01\x00\x63\x6d\x70\x00\x20\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x00\x0c\x00\x03\x80\x08\x00\x01\x00\x64\x73\x5c\x00\x2c\x00\x01\x80\x0c\x00\x01\x00\x63\x6f\x75\x6e\x74\x65\x72\x00\x1c\x00\x02\x80\x0c\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x30\x00\x01\x80\x0e\x00\x01\x00\x69\x6d\x6d\x65\x64\x69\x61\x74\x65\x00\x00\x00\x1c\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x00\x10\x00\x02\x80\x0c\x00\x02\x80\x08\x00\x01\x00\xff\xff\xff\xfb"),
		// batch end
		[]byte("\x00\x00\x00\x0a"),
	}
	testConn := newTestConn(t, want)
	table := testConn.AddTable(&nftables.Table{
		Family: proto,
		Name:   "ts-filter-test",
	})
	chain := testConn.AddChain(&nftables.Chain{
		Name:     "ts-input-test",
		Table:    table,
		Type:     nftables.ChainTypeFilter,
		Hooknum:  nftables.ChainHookInput,
		Priority: nftables.ChainPriorityFilter,
	})
	err := addReturnChromeOSVMRangeRule(testConn, table, chain, "testTunn")
	if err != nil {
		t.Fatal(err)
	}
}

func TestAddDropCGNATRangeRule(t *testing.T) {
	proto := nftables.TableFamilyIPv4
	want := [][]byte{
		// batch begin
		[]byte("\x00\x00\x00\x0a"),
		// nft add table ip ts-filter-test
		[]byte("\x02\x00\x00\x00\x13\x00\x01\x00\x74\x73\x2d\x66\x69\x6c\x74\x65\x72\x2d\x74\x65\x73\x74\x00\x00\x08\x00\x02\x00\x00\x00\x00\x00"),
		// nft add chain ip ts-filter-test ts-input-test { type filter hook input priority filter; }
		[]byte("\x02\x00\x00\x00\x13\x00\x01\x00\x74\x73\x2d\x66\x69\x6c\x74\x65\x72\x2d\x74\x65\x73\x74\x00\x00\x12\x00\x03\x00\x74\x73\x2d\x69\x6e\x70\x75\x74\x2d\x74\x65\x73\x74\x00\x00\x00\x14\x00\x04\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x00\x0b\x00\x07\x00\x66\x69\x6c\x74\x65\x72\x00\x00"),
		// nft add rule ip ts-filter-test ts-input-test iifname != "testTunn" ip saddr 100.64.0.0/10 counter drop
		[]byte("\x02\x00\x00\x00\x13\x00\x01\x00\x74\x73\x2d\x66\x69\x6c\x74\x65\x72\x2d\x74\x65\x73\x74\x00\x00\x12\x00\x02\x00\x74\x73\x2d\x69\x6e\x70\x75\x74\x2d\x74\x65\x73\x74\x00\x00\x00\x58\x01\x04\x80\x24\x00\x01\x80\x09\x00\x01\x00\x6d\x65\x74\x61\x00\x00\x00\x00\x14\x00\x02\x80\x08\x00\x02\x00\x00\x00\x00\x06\x08\x00\x01\x00\x00\x00\x00\x01\x30\x00\x01\x80\x08\x00\x01\x00\x63\x6d\x70\x00\x24\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x01\x10\x00\x03\x80\x0c\x00\x01\x00\x74\x65\x73\x74\x54\x75\x6e\x6e\x34\x00\x01\x80\x0c\x00\x01\x00\x70\x61\x79\x6c\x6f\x61\x64\x00\x24\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x01\x08\x00\x03\x00\x00\x00\x00\x0c\x08\x00\x04\x00\x00\x00\x00\x04\x44\x00\x01\x80\x0c\x00\x01\x00\x62\x69\x74\x77\x69\x73\x65\x00\x34\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x01\x08\x00\x03\x00\x00\x00\x00\x04\x0c\x00\x04\x80\x08\x00\x01\x00\xff\xc0\x00\x00\x0c\x00\x05\x80\x08\x00\x01\x00\x00\x00\x00\x00\x2c\x00\x01\x80\x08\x00\x01\x00\x63\x6d\x70\x00\x20\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x00\x0c\x00\x03\x80\x08\x00\x01\x00\x64\x40\x00\x00\x2c\x00\x01\x80\x0c\x00\x01\x00\x63\x6f\x75\x6e\x74\x65\x72\x00\x1c\x00\x02\x80\x0c\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x30\x00\x01\x80\x0e\x00\x01\x00\x69\x6d\x6d\x65\x64\x69\x61\x74\x65\x00\x00\x00\x1c\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x00\x10\x00\x02\x80\x0c\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x00"),
		// batch end
		[]byte("\x00\x00\x00\x0a"),
	}
	testConn := newTestConn(t, want)
	table := testConn.AddTable(&nftables.Table{
		Family: proto,
		Name:   "ts-filter-test",
	})
	chain := testConn.AddChain(&nftables.Chain{
		Name:     "ts-input-test",
		Table:    table,
		Type:     nftables.ChainTypeFilter,
		Hooknum:  nftables.ChainHookInput,
		Priority: nftables.ChainPriorityFilter,
	})
	err := addDropCGNATRangeRule(testConn, table, chain, "testTunn")
	if err != nil {
		t.Fatal(err)
	}
}

func TestAddSetSubnetRouteMarkRule(t *testing.T) {
	proto := nftables.TableFamilyIPv4
	want := [][]byte{
		// batch begin
		[]byte("\x00\x00\x00\x0a"),
		// nft add table ip ts-filter-test
		[]byte("\x02\x00\x00\x00\x13\x00\x01\x00\x74\x73\x2d\x66\x69\x6c\x74\x65\x72\x2d\x74\x65\x73\x74\x00\x00\x08\x00\x02\x00\x00\x00\x00\x00"),
		// nft add chain ip ts-filter-test ts-forward-test { type filter hook forward priority 0\; }
		[]byte("\x02\x00\x00\x00\x13\x00\x01\x00\x74\x73\x2d\x66\x69\x6c\x74\x65\x72\x2d\x74\x65\x73\x74\x00\x00\x14\x00\x03\x00\x74\x73\x2d\x66\x6f\x72\x77\x61\x72\x64\x2d\x74\x65\x73\x74\x00\x14\x00\x04\x80\x08\x00\x01\x00\x00\x00\x00\x02\x08\x00\x02\x00\x00\x00\x00\x00\x0b\x00\x07\x00\x66\x69\x6c\x74\x65\x72\x00\x00"),
		// nft add rule ip ts-filter-test ts-forward-test iifname "testTunn" counter meta mark set mark and 0xff00ffff xor 0x40000
		[]byte("\x02\x00\x00\x00\x13\x00\x01\x00\x74\x73\x2d\x66\x69\x6c\x74\x65\x72\x2d\x74\x65\x73\x74\x00\x00\x14\x00\x02\x00\x74\x73\x2d\x66\x6f\x72\x77\x61\x72\x64\x2d\x74\x65\x73\x74\x00\x10\x01\x04\x80\x24\x00\x01\x80\x09\x00\x01\x00\x6d\x65\x74\x61\x00\x00\x00\x00\x14\x00\x02\x80\x08\x00\x02\x00\x00\x00\x00\x06\x08\x00\x01\x00\x00\x00\x00\x01\x30\x00\x01\x80\x08\x00\x01\x00\x63\x6d\x70\x00\x24\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x00\x10\x00\x03\x80\x0c\x00\x01\x00\x74\x65\x73\x74\x54\x75\x6e\x6e\x2c\x00\x01\x80\x0c\x00\x01\x00\x63\x6f\x75\x6e\x74\x65\x72\x00\x1c\x00\x02\x80\x0c\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x24\x00\x01\x80\x09\x00\x01\x00\x6d\x65\x74\x61\x00\x00\x00\x00\x14\x00\x02\x80\x08\x00\x02\x00\x00\x00\x00\x03\x08\x00\x01\x00\x00\x00\x00\x01\x44\x00\x01\x80\x0c\x00\x01\x00\x62\x69\x74\x77\x69\x73\x65\x00\x34\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x01\x08\x00\x03\x00\x00\x00\x00\x04\x0c\x00\x04\x80\x08\x00\x01\x00\xff\x00\xff\xff\x0c\x00\x05\x80\x08\x00\x01\x00\x00\x04\x00\x00\x24\x00\x01\x80\x09\x00\x01\x00\x6d\x65\x74\x61\x00\x00\x00\x00\x14\x00\x02\x80\x08\x00\x02\x00\x00\x00\x00\x03\x08\x00\x03\x00\x00\x00\x00\x01"),
		// batch end
		[]byte("\x00\x00\x00\x0a"),
	}
	testConn := newTestConn(t, want)
	table := testConn.AddTable(&nftables.Table{
		Family: proto,
		Name:   "ts-filter-test",
	})
	chain := testConn.AddChain(&nftables.Chain{
		Name:     "ts-forward-test",
		Table:    table,
		Type:     nftables.ChainTypeFilter,
		Hooknum:  nftables.ChainHookForward,
		Priority: nftables.ChainPriorityFilter,
	})
	err := addSetSubnetRouteMarkRule(testConn, table, chain, "testTunn")
	if err != nil {
		t.Fatal(err)
	}
}

func TestAddDropOutgoingPacketFromCGNATRangeRuleWithTunname(t *testing.T) {
	proto := nftables.TableFamilyIPv4
	want := [][]byte{
		// batch begin
		[]byte("\x00\x00\x00\x0a"),
		// nft add table ip ts-filter-test
		[]byte("\x02\x00\x00\x00\x13\x00\x01\x00\x74\x73\x2d\x66\x69\x6c\x74\x65\x72\x2d\x74\x65\x73\x74\x00\x00\x08\x00\x02\x00\x00\x00\x00\x00"),
		// nft add chain ip ts-filter-test ts-forward-test { type filter hook forward priority 0\; }
		[]byte("\x02\x00\x00\x00\x13\x00\x01\x00\x74\x73\x2d\x66\x69\x6c\x74\x65\x72\x2d\x74\x65\x73\x74\x00\x00\x14\x00\x03\x00\x74\x73\x2d\x66\x6f\x72\x77\x61\x72\x64\x2d\x74\x65\x73\x74\x00\x14\x00\x04\x80\x08\x00\x01\x00\x00\x00\x00\x02\x08\x00\x02\x00\x00\x00\x00\x00\x0b\x00\x07\x00\x66\x69\x6c\x74\x65\x72\x00\x00"),
		// nft add rule ip ts-filter-test ts-forward-test oifname "testTunn" ip saddr 100.64.0.0/10 counter drop
		[]byte("\x02\x00\x00\x00\x13\x00\x01\x00\x74\x73\x2d\x66\x69\x6c\x74\x65\x72\x2d\x74\x65\x73\x74\x00\x00\x14\x00\x02\x00\x74\x73\x2d\x66\x6f\x72\x77\x61\x72\x64\x2d\x74\x65\x73\x74\x00\x58\x01\x04\x80\x24\x00\x01\x80\x09\x00\x01\x00\x6d\x65\x74\x61\x00\x00\x00\x00\x14\x00\x02\x80\x08\x00\x02\x00\x00\x00\x00\x07\x08\x00\x01\x00\x00\x00\x00\x01\x30\x00\x01\x80\x08\x00\x01\x00\x63\x6d\x70\x00\x24\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x00\x10\x00\x03\x80\x0c\x00\x01\x00\x74\x65\x73\x74\x54\x75\x6e\x6e\x34\x00\x01\x80\x0c\x00\x01\x00\x70\x61\x79\x6c\x6f\x61\x64\x00\x24\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x01\x08\x00\x03\x00\x00\x00\x00\x0c\x08\x00\x04\x00\x00\x00\x00\x04\x44\x00\x01\x80\x0c\x00\x01\x00\x62\x69\x74\x77\x69\x73\x65\x00\x34\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x01\x08\x00\x03\x00\x00\x00\x00\x04\x0c\x00\x04\x80\x08\x00\x01\x00\xff\xc0\x00\x00\x0c\x00\x05\x80\x08\x00\x01\x00\x00\x00\x00\x00\x2c\x00\x01\x80\x08\x00\x01\x00\x63\x6d\x70\x00\x20\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x00\x0c\x00\x03\x80\x08\x00\x01\x00\x64\x40\x00\x00\x2c\x00\x01\x80\x0c\x00\x01\x00\x63\x6f\x75\x6e\x74\x65\x72\x00\x1c\x00\x02\x80\x0c\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x30\x00\x01\x80\x0e\x00\x01\x00\x69\x6d\x6d\x65\x64\x69\x61\x74\x65\x00\x00\x00\x1c\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x00\x10\x00\x02\x80\x0c\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x00"),
		// batch end
		[]byte("\x00\x00\x00\x0a"),
	}
	testConn := newTestConn(t, want)
	table := testConn.AddTable(&nftables.Table{
		Family: proto,
		Name:   "ts-filter-test",
	})
	chain := testConn.AddChain(&nftables.Chain{
		Name:     "ts-forward-test",
		Table:    table,
		Type:     nftables.ChainTypeFilter,
		Hooknum:  nftables.ChainHookForward,
		Priority: nftables.ChainPriorityFilter,
	})
	err := addDropOutgoingPacketFromCGNATRangeRuleWithTunname(testConn, table, chain, "testTunn")
	if err != nil {
		t.Fatal(err)
	}
}

func TestAddAcceptOutgoingPacketRule(t *testing.T) {
	proto := nftables.TableFamilyIPv4
	want := [][]byte{
		// batch begin
		[]byte("\x00\x00\x00\x0a"),
		// nft add table ip ts-filter-test
		[]byte("\x02\x00\x00\x00\x13\x00\x01\x00\x74\x73\x2d\x66\x69\x6c\x74\x65\x72\x2d\x74\x65\x73\x74\x00\x00\x08\x00\x02\x00\x00\x00\x00\x00"),
		// nft add chain ip ts-filter-test ts-forward-test { type filter hook forward priority 0\; }
		[]byte("\x02\x00\x00\x00\x13\x00\x01\x00\x74\x73\x2d\x66\x69\x6c\x74\x65\x72\x2d\x74\x65\x73\x74\x00\x00\x14\x00\x03\x00\x74\x73\x2d\x66\x6f\x72\x77\x61\x72\x64\x2d\x74\x65\x73\x74\x00\x14\x00\x04\x80\x08\x00\x01\x00\x00\x00\x00\x02\x08\x00\x02\x00\x00\x00\x00\x00\x0b\x00\x07\x00\x66\x69\x6c\x74\x65\x72\x00\x00"),
		// nft add rule ip ts-filter-test ts-forward-test oifname "testTunn" counter accept
		[]byte("\x02\x00\x00\x00\x13\x00\x01\x00\x74\x73\x2d\x66\x69\x6c\x74\x65\x72\x2d\x74\x65\x73\x74\x00\x00\x14\x00\x02\x00\x74\x73\x2d\x66\x6f\x72\x77\x61\x72\x64\x2d\x74\x65\x73\x74\x00\xb4\x00\x04\x80\x24\x00\x01\x80\x09\x00\x01\x00\x6d\x65\x74\x61\x00\x00\x00\x00\x14\x00\x02\x80\x08\x00\x02\x00\x00\x00\x00\x07\x08\x00\x01\x00\x00\x00\x00\x01\x30\x00\x01\x80\x08\x00\x01\x00\x63\x6d\x70\x00\x24\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x00\x10\x00\x03\x80\x0c\x00\x01\x00\x74\x65\x73\x74\x54\x75\x6e\x6e\x2c\x00\x01\x80\x0c\x00\x01\x00\x63\x6f\x75\x6e\x74\x65\x72\x00\x1c\x00\x02\x80\x0c\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x30\x00\x01\x80\x0e\x00\x01\x00\x69\x6d\x6d\x65\x64\x69\x61\x74\x65\x00\x00\x00\x1c\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x00\x10\x00\x02\x80\x0c\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01"),
		// batch end
		[]byte("\x00\x00\x00\x0a"),
	}
	testConn := newTestConn(t, want)
	table := testConn.AddTable(&nftables.Table{
		Family: proto,
		Name:   "ts-filter-test",
	})
	chain := testConn.AddChain(&nftables.Chain{
		Name:     "ts-forward-test",
		Table:    table,
		Type:     nftables.ChainTypeFilter,
		Hooknum:  nftables.ChainHookForward,
		Priority: nftables.ChainPriorityFilter,
	})
	err := addAcceptOutgoingPacketRule(testConn, table, chain, "testTunn")
	if err != nil {
		t.Fatal(err)
	}
}

func TestAddMatchSubnetRouteMarkRuleMasq(t *testing.T) {
	proto := nftables.TableFamilyIPv4
	want := [][]byte{
		// batch begin
		[]byte("\x00\x00\x00\x0a"),
		// nft add table ip ts-nat-test
		[]byte("\x02\x00\x00\x00\x10\x00\x01\x00\x74\x73\x2d\x6e\x61\x74\x2d\x74\x65\x73\x74\x00\x08\x00\x02\x00\x00\x00\x00\x00"),
		// nft add chain ip ts-nat-test ts-postrouting-test { type nat hook postrouting priority 100; }
		[]byte("\x02\x00\x00\x00\x10\x00\x01\x00\x74\x73\x2d\x6e\x61\x74\x2d\x74\x65\x73\x74\x00\x18\x00\x03\x00\x74\x73\x2d\x70\x6f\x73\x74\x72\x6f\x75\x74\x69\x6e\x67\x2d\x74\x65\x73\x74\x00\x14\x00\x04\x80\x08\x00\x01\x00\x00\x00\x00\x04\x08\x00\x02\x00\x00\x00\x00\x64\x08\x00\x07\x00\x6e\x61\x74\x00"),
		// nft add rule ip ts-nat-test ts-postrouting-test meta mark & 0x00ff0000 == 0x00040000 counter masquerade
		[]byte("\x02\x00\x00\x00\x10\x00\x01\x00\x74\x73\x2d\x6e\x61\x74\x2d\x74\x65\x73\x74\x00\x18\x00\x02\x00\x74\x73\x2d\x70\x6f\x73\x74\x72\x6f\x75\x74\x69\x6e\x67\x2d\x74\x65\x73\x74\x00\xf4\x00\x04\x80\x24\x00\x01\x80\x09\x00\x01\x00\x6d\x65\x74\x61\x00\x00\x00\x00\x14\x00\x02\x80\x08\x00\x02\x00\x00\x00\x00\x03\x08\x00\x01\x00\x00\x00\x00\x01\x44\x00\x01\x80\x0c\x00\x01\x00\x62\x69\x74\x77\x69\x73\x65\x00\x34\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x01\x08\x00\x03\x00\x00\x00\x00\x04\x0c\x00\x04\x80\x08\x00\x01\x00\x00\xff\x00\x00\x0c\x00\x05\x80\x08\x00\x01\x00\x00\x00\x00\x00\x2c\x00\x01\x80\x08\x00\x01\x00\x63\x6d\x70\x00\x20\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x00\x0c\x00\x03\x80\x08\x00\x01\x00\x00\x04\x00\x00\x2c\x00\x01\x80\x0c\x00\x01\x00\x63\x6f\x75\x6e\x74\x65\x72\x00\x1c\x00\x02\x80\x0c\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x30\x00\x01\x80\x0e\x00\x01\x00\x69\x6d\x6d\x65\x64\x69\x61\x74\x65\x00\x00\x00\x1c\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x00\x10\x00\x02\x80\x0c\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01"),
		// batch end
		[]byte("\x00\x00\x00\x0a"),
	}
	testConn := newTestConn(t, want)
	table := testConn.AddTable(&nftables.Table{
		Family: proto,
		Name:   "ts-nat-test",
	})
	chain := testConn.AddChain(&nftables.Chain{
		Name:     "ts-postrouting-test",
		Table:    table,
		Type:     nftables.ChainTypeNAT,
		Hooknum:  nftables.ChainHookPostrouting,
		Priority: nftables.ChainPriorityNATSource,
	})
	err := addMatchSubnetRouteMarkRule(testConn, table, chain, Accept)
	if err != nil {
		t.Fatal(err)
	}
}

func TestAddMatchSubnetRouteMarkRuleAccept(t *testing.T) {
	proto := nftables.TableFamilyIPv4
	want := [][]byte{
		// batch begin
		[]byte("\x00\x00\x00\x0a"),
		// nft add table ip ts-filter-test
		[]byte("\x02\x00\x00\x00\x13\x00\x01\x00\x74\x73\x2d\x66\x69\x6c\x74\x65\x72\x2d\x74\x65\x73\x74\x00\x00\x08\x00\x02\x00\x00\x00\x00\x00"),
		// nft add chain ip ts-filter-test ts-forward-test { type filter hook forward priority 0\; }
		[]byte("\x02\x00\x00\x00\x13\x00\x01\x00\x74\x73\x2d\x66\x69\x6c\x74\x65\x72\x2d\x74\x65\x73\x74\x00\x00\x14\x00\x03\x00\x74\x73\x2d\x66\x6f\x72\x77\x61\x72\x64\x2d\x74\x65\x73\x74\x00\x14\x00\x04\x80\x08\x00\x01\x00\x00\x00\x00\x02\x08\x00\x02\x00\x00\x00\x00\x00\x0b\x00\x07\x00\x66\x69\x6c\x74\x65\x72\x00\x00"),
		// nft add rule ip ts-filter-test ts-forward-test meta mark and 0x00ff0000 eq 0x00040000 counter accept
		[]byte("\x02\x00\x00\x00\x13\x00\x01\x00\x74\x73\x2d\x66\x69\x6c\x74\x65\x72\x2d\x74\x65\x73\x74\x00\x00\x14\x00\x02\x00\x74\x73\x2d\x66\x6f\x72\x77\x61\x72\x64\x2d\x74\x65\x73\x74\x00\xf4\x00\x04\x80\x24\x00\x01\x80\x09\x00\x01\x00\x6d\x65\x74\x61\x00\x00\x00\x00\x14\x00\x02\x80\x08\x00\x02\x00\x00\x00\x00\x03\x08\x00\x01\x00\x00\x00\x00\x01\x44\x00\x01\x80\x0c\x00\x01\x00\x62\x69\x74\x77\x69\x73\x65\x00\x34\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x01\x08\x00\x03\x00\x00\x00\x00\x04\x0c\x00\x04\x80\x08\x00\x01\x00\x00\xff\x00\x00\x0c\x00\x05\x80\x08\x00\x01\x00\x00\x00\x00\x00\x2c\x00\x01\x80\x08\x00\x01\x00\x63\x6d\x70\x00\x20\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x00\x0c\x00\x03\x80\x08\x00\x01\x00\x00\x04\x00\x00\x2c\x00\x01\x80\x0c\x00\x01\x00\x63\x6f\x75\x6e\x74\x65\x72\x00\x1c\x00\x02\x80\x0c\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x30\x00\x01\x80\x0e\x00\x01\x00\x69\x6d\x6d\x65\x64\x69\x61\x74\x65\x00\x00\x00\x1c\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x00\x10\x00\x02\x80\x0c\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01"),
		// batch end
		[]byte("\x00\x00\x00\x0a"),
	}
	testConn := newTestConn(t, want)
	table := testConn.AddTable(&nftables.Table{
		Family: proto,
		Name:   "ts-filter-test",
	})
	chain := testConn.AddChain(&nftables.Chain{
		Name:     "ts-forward-test",
		Table:    table,
		Type:     nftables.ChainTypeFilter,
		Hooknum:  nftables.ChainHookForward,
		Priority: nftables.ChainPriorityFilter,
	})
	err := addMatchSubnetRouteMarkRule(testConn, table, chain, Accept)
	if err != nil {
		t.Fatal(err)
	}
}

func newSysConn(t *testing.T) *nftables.Conn {
	t.Helper()

	runtime.LockOSThread()

	ns, err := netns.New()
	if err != nil {
		t.Fatalf("netns.New() failed: %v", err)
	}
	c, err := nftables.New(nftables.WithNetNSFd(int(ns)))
	if err != nil {
		t.Fatalf("nftables.New() failed: %v", err)
	}

	t.Cleanup(func() { cleanupSysConn(t, ns) })

	return c
}

func cleanupSysConn(t *testing.T, ns netns.NsHandle) {
	defer runtime.UnlockOSThread()

	if err := ns.Close(); err != nil {
		t.Fatalf("newNS.Close() failed: %v", err)
	}
}

func newFakeNftablesRunner(t *testing.T, conn *nftables.Conn) *nftablesRunner {
	nft4 := &nftable{Proto: nftables.TableFamilyIPv4}
	nft6 := &nftable{Proto: nftables.TableFamilyIPv6}

	return &nftablesRunner{
		conn:           conn,
		nft4:           nft4,
		nft6:           nft6,
		v6Available:    true,
		v6NATAvailable: true,
	}
}

func TestAddAndDelNetfilterChains(t *testing.T) {
	if os.Geteuid() != 0 {
		t.Skip(t.Name(), " requires privileges to create a namespace in order to run")
		return
	}
	conn := newSysConn(t)

	runner := newFakeNftablesRunner(t, conn)
	runner.AddChains()

	tables, err := conn.ListTables()
	if err != nil {
		t.Fatalf("conn.ListTables() failed: %v", err)
	}

	if len(tables) != 4 {
		t.Fatalf("len(tables) = %d, want 4", len(tables))
	}

	chainsV4, err := conn.ListChainsOfTableFamily(nftables.TableFamilyIPv4)
	if err != nil {
		t.Fatalf("list chains failed: %v", err)
	}

	if len(chainsV4) != 6 {
		t.Fatalf("len(chainsV4) = %d, want 6", len(chainsV4))
	}

	chainsV6, err := conn.ListChainsOfTableFamily(nftables.TableFamilyIPv6)
	if err != nil {
		t.Fatalf("list chains failed: %v", err)
	}

	if len(chainsV6) != 6 {
		t.Fatalf("len(chainsV6) = %d, want 6", len(chainsV6))
	}

	runner.DelChains()

	tables, err = conn.ListTables()
	if err != nil {
		t.Fatalf("conn.ListTables() failed: %v", err)
	}

	if len(tables) != 0 {
		t.Fatalf("len(tables) = %d, want 0", len(tables))
	}
}

func getTsChains(
	conn *nftables.Conn,
	proto nftables.TableFamily) (*nftables.Chain, *nftables.Chain, *nftables.Chain, error) {
	chains, err := conn.ListChainsOfTableFamily(nftables.TableFamilyIPv4)
	if err != nil {
		return nil, nil, nil, fmt.Errorf("list chains failed: %w", err)
	}
	var chainInput, chainForward, chainPostrouting *nftables.Chain
	for _, chain := range chains {
		switch chain.Name {
		case "ts-input":
			chainInput = chain
		case "ts-forward":
			chainForward = chain
		case "ts-postrouting":
			chainPostrouting = chain
		}
	}
	return chainInput, chainForward, chainPostrouting, nil
}

// findV4BaseRules verifies that the base rules are present in the input and forward chains.
func findV4BaseRules(
	conn *nftables.Conn,
	inpChain *nftables.Chain,
	forwChain *nftables.Chain,
	tunname string) ([]*nftables.Rule, error) {
	want := []*nftables.Rule{}
	rule, err := createRangeRule(inpChain.Table, inpChain, tunname, tsaddr.ChromeOSVMRange(), expr.VerdictReturn)
	if err != nil {
		return nil, fmt.Errorf("create rule: %w", err)
	}
	want = append(want, rule)
	rule, err = createRangeRule(inpChain.Table, inpChain, tunname, tsaddr.CGNATRange(), expr.VerdictDrop)
	if err != nil {
		return nil, fmt.Errorf("create rule: %w", err)
	}
	want = append(want, rule)
	rule, err = createDropOutgoingPacketFromCGNATRangeRuleWithTunname(forwChain.Table, forwChain, tunname)
	if err != nil {
		return nil, fmt.Errorf("create rule: %w", err)
	}
	want = append(want, rule)

	get := []*nftables.Rule{}
	for _, rule := range want {
		getRule, err := findRule(conn, rule)
		if err != nil {
			return nil, fmt.Errorf("find rule: %w", err)
		}
		get = append(get, getRule)
	}
	return get, nil
}

func findCommonBaseRules(
	conn *nftables.Conn,
	forwChain *nftables.Chain,
	tunname string) ([]*nftables.Rule, error) {
	want := []*nftables.Rule{}
	rule, err := createSetSubnetRouteMarkRule(forwChain.Table, forwChain, tunname)
	if err != nil {
		return nil, fmt.Errorf("create rule: %w", err)
	}
	want = append(want, rule)
	rule, err = createMatchSubnetRouteMarkRule(forwChain.Table, forwChain, Accept)
	if err != nil {
		return nil, fmt.Errorf("create rule: %w", err)
	}
	want = append(want, rule)
	rule = createAcceptOutgoingPacketRule(forwChain.Table, forwChain, tunname)
	want = append(want, rule)

	get := []*nftables.Rule{}
	for _, rule := range want {
		getRule, err := findRule(conn, rule)
		if err != nil {
			return nil, fmt.Errorf("find rule: %w", err)
		}
		get = append(get, getRule)
	}

	return get, nil
}

func TestNFTAddAndDelNetfilterBase(t *testing.T) {
	if os.Geteuid() != 0 {
		t.Skip(t.Name(), " requires privileges to create a namespace in order to run")
		return
	}

	conn := newSysConn(t)

	runner := newFakeNftablesRunner(t, conn)
	runner.AddChains()
	defer runner.DelChains()
	runner.AddBase("testTunn")

	// check number of rules in each IPv4 TS chain
	inputV4, forwardV4, postroutingV4, err := getTsChains(conn, nftables.TableFamilyIPv4)
	if err != nil {
		t.Fatalf("getTsChains() failed: %v", err)
	}

	inputV4Rules, err := conn.GetRules(runner.nft4.Filter, inputV4)
	if err != nil {
		t.Fatalf("conn.GetRules() failed: %v", err)
	}
	if len(inputV4Rules) != 2 {
		t.Fatalf("len(inputV4Rules) = %d, want 2", len(inputV4Rules))
	}

	forwardV4Rules, err := conn.GetRules(runner.nft4.Filter, forwardV4)
	if err != nil {
		t.Fatalf("conn.GetRules() failed: %v", err)
	}
	if len(forwardV4Rules) != 4 {
		t.Fatalf("len(forwardV4Rules) = %d, want 4", len(forwardV4Rules))
	}

	postroutingV4Rules, err := conn.GetRules(runner.nft4.Nat, postroutingV4)
	if err != nil {
		t.Fatalf("conn.GetRules() failed: %v", err)
	}
	if len(postroutingV4Rules) != 0 {
		t.Fatalf("len(postroutingV4Rules) = %d, want 0", len(postroutingV4Rules))
	}

	_, err = findV4BaseRules(conn, inputV4, forwardV4, "testTunn")
	if err != nil {
		t.Fatalf("missing v4 base rule: %v", err)
	}
	_, err = findCommonBaseRules(conn, forwardV4, "testTunn")
	if err != nil {
		t.Fatalf("missing v4 base rule: %v", err)
	}

	// Check number of rules in each IPv6 TS chain.
	inputV6, forwardV6, postroutingV6, err := getTsChains(conn, nftables.TableFamilyIPv6)
	if err != nil {
		t.Fatalf("getTsChains() failed: %v", err)
	}

	inputV6Rules, err := conn.GetRules(runner.nft6.Filter, inputV6)
	if err != nil {
		t.Fatalf("conn.GetRules() failed: %v", err)
	}
	if len(inputV6Rules) != 0 {
		t.Fatalf("len(inputV6Rules) = %d, want 0", len(inputV4Rules))
	}

	forwardV6Rules, err := conn.GetRules(runner.nft6.Filter, forwardV6)
	if err != nil {
		t.Fatalf("conn.GetRules() failed: %v", err)
	}
	if len(forwardV6Rules) != 3 {
		t.Fatalf("len(forwardV6Rules) = %d, want 3", len(forwardV4Rules))
	}

	postroutingV6Rules, err := conn.GetRules(runner.nft6.Nat, postroutingV6)
	if err != nil {
		t.Fatalf("conn.GetRules() failed: %v", err)
	}
	if len(postroutingV6Rules) != 0 {
		t.Fatalf("len(postroutingV6Rules) = %d, want 0", len(postroutingV4Rules))
	}

	_, err = findCommonBaseRules(conn, forwardV6, "testTunn")
	if err != nil {
		t.Fatalf("missing v6 base rule: %v", err)
	}

	runner.DelBase()

	chains, err := conn.ListChains()
	if err != nil {
		t.Fatalf("conn.ListChains() failed: %v", err)
	}
	for _, chain := range chains {
		chainRules, err := conn.GetRules(chain.Table, chain)
		if err != nil {
			t.Fatalf("conn.GetRules() failed: %v", err)
		}
		if len(chainRules) != 0 {
			t.Fatalf("len(chainRules) = %d, want 0", len(chainRules))
		}
	}
}

func findLoopBackRule(conn *nftables.Conn, proto nftables.TableFamily, table *nftables.Table, chain *nftables.Chain, addr netip.Addr) (*nftables.Rule, error) {
	matchingAddr := addr.AsSlice()
	saddrExpr, err := newLoadSaddrExpr(proto, 1)
	if err != nil {
		return nil, fmt.Errorf("get expr: %w", err)
	}
	loopBackRule := &nftables.Rule{
		Table: table,
		Chain: chain,
		Exprs: []expr.Any{
			&expr.Meta{
				Key:      expr.MetaKeyIIFNAME,
				Register: 1,
			},
			&expr.Cmp{
				Op:       expr.CmpOpEq,
				Register: 1,
				Data:     []byte("lo"),
			},
			saddrExpr,
			&expr.Cmp{
				Op:       expr.CmpOpEq,
				Register: 1,
				Data:     matchingAddr,
			},
			&expr.Counter{},
			&expr.Verdict{
				Kind: expr.VerdictAccept,
			},
		},
	}

	existingLoopBackRule, err := findRule(conn, loopBackRule)
	if err != nil {
		return nil, fmt.Errorf("find loop back rule: %w", err)
	}
	return existingLoopBackRule, nil
}

func TestNFTAddAndDelLoopbackRule(t *testing.T) {
	if os.Geteuid() != 0 {
		t.Skip(t.Name(), " requires privileges to create a namespace in order to run")
		return
	}

	conn := newSysConn(t)

	runner := newFakeNftablesRunner(t, conn)
	runner.AddChains()
	defer runner.DelChains()
	runner.AddBase("testTunn")
	defer runner.DelBase()

	addr := netip.MustParseAddr("192.168.0.2")
	addrV6 := netip.MustParseAddr("2001:db8::2")
	runner.AddLoopbackRule(addr)
	runner.AddLoopbackRule(addrV6)

	inputV4, _, _, err := getTsChains(conn, nftables.TableFamilyIPv4)
	if err != nil {
		t.Fatalf("getTsChains() failed: %v", err)
	}

	inputV4Rules, err := conn.GetRules(runner.nft4.Filter, inputV4)
	if err != nil {
		t.Fatalf("conn.GetRules() failed: %v", err)
	}
	if len(inputV4Rules) != 3 {
		t.Fatalf("len(inputV4Rules) = %d, want 3", len(inputV4Rules))
	}

	existingLoopBackRule, err := findLoopBackRule(conn, nftables.TableFamilyIPv4, runner.nft4.Filter, inputV4, addr)
	if err != nil {
		t.Fatalf("findLoopBackRule() failed: %v", err)
	}

	if existingLoopBackRule.Position != 0 {
		t.Fatalf("existingLoopBackRule.Handle = %d, want 0", existingLoopBackRule.Handle)
	}

	inputV6, _, _, err := getTsChains(conn, nftables.TableFamilyIPv6)
	if err != nil {
		t.Fatalf("getTsChains() failed: %v", err)
	}

	inputV6Rules, err := conn.GetRules(runner.nft6.Filter, inputV4)
	if err != nil {
		t.Fatalf("conn.GetRules() failed: %v", err)
	}
	if len(inputV6Rules) != 1 {
		t.Fatalf("len(inputV4Rules) = %d, want 1", len(inputV4Rules))
	}

	existingLoopBackRuleV6, err := findLoopBackRule(conn, nftables.TableFamilyIPv6, runner.nft6.Filter, inputV6, addrV6)
	if err != nil {
		t.Fatalf("findLoopBackRule() failed: %v", err)
	}

	if existingLoopBackRuleV6.Position != 0 {
		t.Fatalf("existingLoopBackRule.Handle = %d, want 0", existingLoopBackRule.Handle)
	}

	runner.DelLoopbackRule(addr)
	runner.DelLoopbackRule(addrV6)

	inputV4Rules, err = conn.GetRules(runner.nft4.Filter, inputV4)
	if err != nil {
		t.Fatalf("conn.GetRules() failed: %v", err)
	}
	if len(inputV4Rules) != 2 {
		t.Fatalf("len(inputV4Rules) = %d, want 2", len(inputV4Rules))
	}
}

func TestNFTAddAndDelHookRule(t *testing.T) {
	if os.Geteuid() != 0 {
		t.Skip(t.Name(), " requires privileges to create a namespace in order to run")
		return
	}

	conn := newSysConn(t)
	runner := newFakeNftablesRunner(t, conn)
	runner.AddChains()
	defer runner.DelChains()
	runner.AddHooks()

	forwardChain, err := getChainFromTable(conn, runner.nft4.Filter, "FORWARD")
	if err != nil {
		t.Fatalf("failed to get forwardChain: %v", err)
	}

	forwardChainRules, err := conn.GetRules(forwardChain.Table, forwardChain)
	if err != nil {
		t.Fatalf("failed to get rules: %v", err)
	}

	if len(forwardChainRules) != 1 {
		t.Fatalf("expected 1 rule in FORWARD chain, got %v", len(forwardChainRules))
	}

	inputChain, err := getChainFromTable(conn, runner.nft4.Filter, "INPUT")
	if err != nil {
		t.Fatalf("failed to get inputChain: %v", err)
	}

	inputChainRules, err := conn.GetRules(inputChain.Table, inputChain)
	if err != nil {
		t.Fatalf("failed to get rules: %v", err)
	}

	if len(inputChainRules) != 1 {
		t.Fatalf("expected 1 rule in INPUT chain, got %v", len(inputChainRules))
	}

	postroutingChain, err := getChainFromTable(conn, runner.nft4.Nat, "POSTROUTING")
	if err != nil {
		t.Fatalf("failed to get postroutingChain: %v", err)
	}

	postroutingChainRules, err := conn.GetRules(postroutingChain.Table, postroutingChain)
	if err != nil {
		t.Fatalf("failed to get rules: %v", err)
	}

	if len(postroutingChainRules) != 1 {
		t.Fatalf("expected 1 rule in POSTROUTING chain, got %v", len(postroutingChainRules))
	}

	runner.DelHooks(t.Logf)

	forwardChainRules, err = conn.GetRules(forwardChain.Table, forwardChain)
	if err != nil {
		t.Fatalf("failed to get rules: %v", err)
	}

	if len(forwardChainRules) != 0 {
		t.Fatalf("expected 0 rule in FORWARD chain, got %v", len(forwardChainRules))
	}

	inputChainRules, err = conn.GetRules(inputChain.Table, inputChain)
	if err != nil {
		t.Fatalf("failed to get rules: %v", err)
	}

	if len(inputChainRules) != 0 {
		t.Fatalf("expected 0 rule in INPUT chain, got %v", len(inputChainRules))
	}

	postroutingChainRules, err = conn.GetRules(postroutingChain.Table, postroutingChain)
	if err != nil {
		t.Fatalf("failed to get rules: %v", err)
	}

	if len(postroutingChainRules) != 0 {
		t.Fatalf("expected 0 rule in POSTROUTING chain, got %v", len(postroutingChainRules))
	}
}
