git: 9f44a47fd079 - main - ipfw(8): add ioctl/instruction generation tests

From: Alexander V. Chernikov <melifaro_at_FreeBSD.org>
Date: Tue, 13 Jun 2023 11:55:42 UTC
The branch main has been updated by melifaro:

URL: https://cgit.FreeBSD.org/src/commit/?id=9f44a47fd07924afc035991af15d84e6585dea4f

commit 9f44a47fd07924afc035991af15d84e6585dea4f
Author:     Alexander V. Chernikov <melifaro@FreeBSD.org>
AuthorDate: 2023-06-11 08:12:04 +0000
Commit:     Alexander V. Chernikov <melifaro@FreeBSD.org>
CommitDate: 2023-06-13 11:55:37 +0000

    ipfw(8): add ioctl/instruction generation tests
    
    Differential Revision: https://reviews.freebsd.org/D40488
    MFC after:      2 weeks
---
 etc/mtree/BSD.tests.dist                           |   2 +
 sbin/ipfw/ipfw2.c                                  |  47 +-
 sbin/ipfw/ipfw2.h                                  |   1 +
 sbin/ipfw/main.c                                   |   6 +-
 sbin/ipfw/tests/Makefile                           |   5 +
 sbin/ipfw/tests/test_add_rule.py                   | 400 +++++++++++++++
 tests/atf_python/sys/Makefile                      |   2 +-
 tests/atf_python/sys/netpfil/Makefile              |  11 +
 tests/atf_python/sys/netpfil/__init__.py           |   0
 tests/atf_python/sys/netpfil/ipfw/Makefile         |  12 +
 tests/atf_python/sys/netpfil/ipfw/__init__.py      |   0
 tests/atf_python/sys/netpfil/ipfw/insn_headers.py  | 198 ++++++++
 tests/atf_python/sys/netpfil/ipfw/insns.py         | 555 +++++++++++++++++++++
 tests/atf_python/sys/netpfil/ipfw/ioctl.py         | 505 +++++++++++++++++++
 tests/atf_python/sys/netpfil/ipfw/ioctl_headers.py |  90 ++++
 tests/atf_python/sys/netpfil/ipfw/ipfw.py          | 118 +++++
 tests/atf_python/sys/netpfil/ipfw/utils.py         |  61 +++
 17 files changed, 2007 insertions(+), 6 deletions(-)

diff --git a/etc/mtree/BSD.tests.dist b/etc/mtree/BSD.tests.dist
index 8dc52086fe33..8b9d0ac6bccd 100644
--- a/etc/mtree/BSD.tests.dist
+++ b/etc/mtree/BSD.tests.dist
@@ -450,6 +450,8 @@
         ..
         ifconfig
         ..
+        ipfw
+        ..
         md5
         ..
         mdconfig
diff --git a/sbin/ipfw/ipfw2.c b/sbin/ipfw/ipfw2.c
index 683465a024bc..36f39beba5bb 100644
--- a/sbin/ipfw/ipfw2.c
+++ b/sbin/ipfw/ipfw2.c
@@ -587,6 +587,13 @@ stringnum_cmp(const char *a, const char *b)
 	return (strcmp(a, b));
 }
 
+struct debug_header {
+	uint16_t cmd_type;
+	uint16_t spare1;
+	uint32_t opt_name;
+	uint32_t total_len;
+	uint32_t spare2;
+};
 
 /*
  * conditionally runs the command.
@@ -597,8 +604,18 @@ do_cmd(int optname, void *optval, uintptr_t optlen)
 {
 	int i;
 
+	if (g_co.debug_only) {
+		struct debug_header dbg = {
+			.cmd_type = 1,
+			.opt_name = optname,
+			.total_len = optlen + sizeof(struct debug_header),
+		};
+		write(1, &dbg, sizeof(dbg));
+		write(1, optval, optlen);
+	}
+
 	if (g_co.test_only)
-		return 0;
+		return (0);
 
 	if (ipfw_socket == -1)
 		ipfw_socket = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);
@@ -617,7 +634,7 @@ do_cmd(int optname, void *optval, uintptr_t optlen)
 	} else {
 		i = setsockopt(ipfw_socket, IPPROTO_IP, optname, optval, optlen);
 	}
-	return i;
+	return (i);
 }
 
 /*
@@ -634,6 +651,18 @@ int
 do_set3(int optname, ip_fw3_opheader *op3, size_t optlen)
 {
 
+	op3->opcode = optname;
+
+	if (g_co.debug_only) {
+		struct debug_header dbg = {
+			.cmd_type = 2,
+			.opt_name = optname,
+			.total_len = optlen, sizeof(struct debug_header),
+		};
+		write(1, &dbg, sizeof(dbg));
+		write(1, op3, optlen);
+	}
+
 	if (g_co.test_only)
 		return (0);
 
@@ -642,7 +671,6 @@ do_set3(int optname, ip_fw3_opheader *op3, size_t optlen)
 	if (ipfw_socket < 0)
 		err(EX_UNAVAILABLE, "socket");
 
-	op3->opcode = optname;
 
 	return (setsockopt(ipfw_socket, IPPROTO_IP, IP_FW3, op3, optlen));
 }
@@ -663,6 +691,18 @@ do_get3(int optname, ip_fw3_opheader *op3, size_t *optlen)
 	int error;
 	socklen_t len;
 
+	op3->opcode = optname;
+
+	if (g_co.debug_only) {
+		struct debug_header dbg = {
+			.cmd_type = 3,
+			.opt_name = optname,
+			.total_len = *optlen + sizeof(struct debug_header),
+		};
+		write(1, &dbg, sizeof(dbg));
+		write(1, op3, *optlen);
+	}
+
 	if (g_co.test_only)
 		return (0);
 
@@ -671,7 +711,6 @@ do_get3(int optname, ip_fw3_opheader *op3, size_t *optlen)
 	if (ipfw_socket < 0)
 		err(EX_UNAVAILABLE, "socket");
 
-	op3->opcode = optname;
 
 	len = *optlen;
 	error = getsockopt(ipfw_socket, IPPROTO_IP, IP_FW3, op3, &len);
diff --git a/sbin/ipfw/ipfw2.h b/sbin/ipfw/ipfw2.h
index a554f9b9f6fc..92fa05ae14b2 100644
--- a/sbin/ipfw/ipfw2.h
+++ b/sbin/ipfw/ipfw2.h
@@ -48,6 +48,7 @@ struct cmdline_opts {
 	int	test_only;	/* only check syntax */
 	int	comment_only;	/* only print action and comment */
 	int	verbose;	/* be verbose on some commands */
+	int	debug_only;	/* output ioctl i/o on stdout */
 
 	/* The options below can have multiple values. */
 
diff --git a/sbin/ipfw/main.c b/sbin/ipfw/main.c
index 577224047cd0..b1bed5ad008c 100644
--- a/sbin/ipfw/main.c
+++ b/sbin/ipfw/main.c
@@ -277,7 +277,7 @@ ipfw_main(int oldac, char **oldav)
 
 	optind = optreset = 1;	/* restart getopt() */
 	if (is_ipfw()) {
-		while ((ch = getopt(ac, av, "abcdDefhinNp:qs:STtv")) != -1)
+		while ((ch = getopt(ac, av, "abcdDefhinNp:qs:STtvx")) != -1)
 			switch (ch) {
 			case 'a':
 				do_acct = 1;
@@ -354,6 +354,10 @@ ipfw_main(int oldac, char **oldav)
 				g_co.verbose = 1;
 				break;
 
+			case 'x': /* debug output */
+				g_co.debug_only = 1;
+				break;
+
 			default:
 				free(save_av);
 				return 1;
diff --git a/sbin/ipfw/tests/Makefile b/sbin/ipfw/tests/Makefile
new file mode 100644
index 000000000000..987410f5d710
--- /dev/null
+++ b/sbin/ipfw/tests/Makefile
@@ -0,0 +1,5 @@
+PACKAGE= tests
+
+ATF_TESTS_PYTEST+=	test_add_rule.py
+
+.include <bsd.test.mk>
diff --git a/sbin/ipfw/tests/test_add_rule.py b/sbin/ipfw/tests/test_add_rule.py
new file mode 100755
index 000000000000..65b4e7d33646
--- /dev/null
+++ b/sbin/ipfw/tests/test_add_rule.py
@@ -0,0 +1,400 @@
+import errno
+import json
+import os
+import socket
+import struct
+import subprocess
+import sys
+from ctypes import c_byte
+from ctypes import c_char
+from ctypes import c_int
+from ctypes import c_long
+from ctypes import c_uint32
+from ctypes import c_uint8
+from ctypes import c_ulong
+from ctypes import c_ushort
+from ctypes import sizeof
+from ctypes import Structure
+from enum import Enum
+from typing import Any
+from typing import Dict
+from typing import List
+from typing import NamedTuple
+from typing import Optional
+from typing import Union
+
+import pytest
+from atf_python.sys.netpfil.ipfw.insns import Icmp6RejectCode
+from atf_python.sys.netpfil.ipfw.insns import IcmpRejectCode
+from atf_python.sys.netpfil.ipfw.insns import Insn
+from atf_python.sys.netpfil.ipfw.insns import InsnComment
+from atf_python.sys.netpfil.ipfw.insns import InsnEmpty
+from atf_python.sys.netpfil.ipfw.insns import InsnIp
+from atf_python.sys.netpfil.ipfw.insns import InsnIp6
+from atf_python.sys.netpfil.ipfw.insns import InsnPorts
+from atf_python.sys.netpfil.ipfw.insns import InsnProb
+from atf_python.sys.netpfil.ipfw.insns import InsnProto
+from atf_python.sys.netpfil.ipfw.insns import InsnReject
+from atf_python.sys.netpfil.ipfw.insns import InsnTable
+from atf_python.sys.netpfil.ipfw.insns import IpFwOpcode
+from atf_python.sys.netpfil.ipfw.ioctl import CTlv
+from atf_python.sys.netpfil.ipfw.ioctl import CTlvRule
+from atf_python.sys.netpfil.ipfw.ioctl import IpFwTlvType
+from atf_python.sys.netpfil.ipfw.ioctl import IpFwXRule
+from atf_python.sys.netpfil.ipfw.ioctl import NTlv
+from atf_python.sys.netpfil.ipfw.ioctl import Op3CmdType
+from atf_python.sys.netpfil.ipfw.ioctl import RawRule
+from atf_python.sys.netpfil.ipfw.ipfw import DebugIoReader
+from atf_python.sys.netpfil.ipfw.utils import enum_from_int
+from atf_python.utils import BaseTest
+
+
+IPFW_PATH = "/sbin/ipfw"
+
+
+def differ(w_obj, g_obj, w_stack=[], g_stack=[]):
+    if bytes(w_obj) == bytes(g_obj):
+        return True
+    num_objects = 0
+    for i, w_child in enumerate(w_obj.obj_list):
+        if i > len(g_obj.obj_list):
+            print("MISSING object from chain {}".format(" / ".join(w_stack)))
+            w_child.print_obj()
+            print("==========================")
+            return False
+        g_child = g_obj.obj_list[i]
+        if bytes(w_child) == bytes(g_child):
+            num_objects += 1
+            continue
+        w_stack.append(w_obj.obj_name)
+        g_stack.append(g_obj.obj_name)
+        if not differ(w_child, g_child, w_stack, g_stack):
+            return False
+        break
+    if num_objects == len(w_obj.obj_list) and num_objects < len(g_obj.obj_list):
+        g_child = g_obj.obj_list[num_objects]
+        print("EXTRA object from chain {}".format(" / ".join(g_stack)))
+        g_child.print_obj()
+        print("==========================")
+        return False
+    print("OBJECTS DIFFER")
+    print("WANTED CHAIN: {}".format(" / ".join(w_stack)))
+    w_obj.print_obj()
+    w_obj.print_obj_hex()
+    print("==========================")
+    print("GOT CHAIN: {}".format(" / ".join(g_stack)))
+    g_obj.print_obj()
+    g_obj.print_obj_hex()
+    print("==========================")
+    return False
+
+
+class TestAddRule(BaseTest):
+    def compile_rule(self, out):
+        tlvs = []
+        if "objs" in out:
+            tlvs.append(CTlv(IpFwTlvType.IPFW_TLV_TBLNAME_LIST, out["objs"]))
+        rule = RawRule(rulenum=out.get("rulenum", 0), obj_list=out["insns"])
+        tlvs.append(CTlvRule(obj_list=[rule]))
+        return IpFwXRule(Op3CmdType.IP_FW_XADD, tlvs)
+
+    def verify_rule(self, in_data: str, out_data):
+        # Prepare the desired output
+        expected = self.compile_rule(out_data)
+
+        reader = DebugIoReader(IPFW_PATH)
+        ioctls = reader.get_records(in_data)
+        assert len(ioctls) == 1  # Only 1 ioctl request expected
+        got = ioctls[0]
+
+        if not differ(expected, got):
+            print("=> CMD: {}".format(in_data))
+            print("=> WANTED:")
+            expected.print_obj()
+            print("==========================")
+            print("=> GOT:")
+            got.print_obj()
+            print("==========================")
+        assert bytes(got) == bytes(expected)
+
+    @pytest.mark.parametrize(
+        "rule",
+        [
+            pytest.param(
+                {
+                    "in": "add 200 allow ip from any to any",
+                    "out": {
+                        "insns": [InsnEmpty(IpFwOpcode.O_ACCEPT)],
+                        "rulenum": 200,
+                    },
+                },
+                id="test_rulenum",
+            ),
+            pytest.param(
+                {
+                    "in": "add allow ip from { 1.2.3.4 or 2.3.4.5 } to any",
+                    "out": {
+                        "insns": [
+                            InsnIp(IpFwOpcode.O_IP_SRC, ip="1.2.3.4", is_or=True),
+                            InsnIp(IpFwOpcode.O_IP_SRC, ip="2.3.4.5"),
+                            InsnEmpty(IpFwOpcode.O_ACCEPT),
+                        ],
+                    },
+                },
+                id="test_or",
+            ),
+            pytest.param(
+                {
+                    "in": "add allow ip from table(AAA) to table(BBB)",
+                    "out": {
+                        "objs": [
+                            NTlv(IpFwTlvType.IPFW_TLV_TBL_NAME, idx=1, name="AAA"),
+                            NTlv(IpFwTlvType.IPFW_TLV_TBL_NAME, idx=2, name="BBB"),
+                        ],
+                        "insns": [
+                            InsnTable(IpFwOpcode.O_IP_SRC_LOOKUP, arg1=1),
+                            InsnTable(IpFwOpcode.O_IP_DST_LOOKUP, arg1=2),
+                            InsnEmpty(IpFwOpcode.O_ACCEPT),
+                        ],
+                    },
+                },
+                id="test_tables",
+            ),
+            pytest.param(
+                {
+                    "in": "add allow ip from any to 1.2.3.4 // test comment",
+                    "out": {
+                        "insns": [
+                            InsnIp(IpFwOpcode.O_IP_DST, ip="1.2.3.4"),
+                            InsnComment(comment="test comment"),
+                            InsnEmpty(IpFwOpcode.O_ACCEPT),
+                        ],
+                    },
+                },
+                id="test_comment",
+            ),
+        ],
+    )
+    def test_add_rule(self, rule):
+        """Tests if the compiled rule is sane and matches the spec"""
+        self.verify_rule(rule["in"], rule["out"])
+
+    @pytest.mark.parametrize(
+        "action",
+        [
+            pytest.param(("allow", InsnEmpty(IpFwOpcode.O_ACCEPT)), id="test_allow"),
+            pytest.param(
+                (
+                    "abort",
+                    Insn(IpFwOpcode.O_REJECT, arg1=IcmpRejectCode.ICMP_REJECT_ABORT),
+                ),
+                id="abort",
+            ),
+            pytest.param(
+                (
+                    "abort6",
+                    Insn(
+                        IpFwOpcode.O_UNREACH6, arg1=Icmp6RejectCode.ICMP6_UNREACH_ABORT
+                    ),
+                ),
+                id="abort6",
+            ),
+            pytest.param(("accept", InsnEmpty(IpFwOpcode.O_ACCEPT)), id="accept"),
+            pytest.param(("deny", InsnEmpty(IpFwOpcode.O_DENY)), id="deny"),
+            pytest.param(
+                (
+                    "reject",
+                    Insn(IpFwOpcode.O_REJECT, arg1=IcmpRejectCode.ICMP_UNREACH_HOST),
+                ),
+                id="reject",
+            ),
+            pytest.param(
+                (
+                    "reset",
+                    Insn(IpFwOpcode.O_REJECT, arg1=IcmpRejectCode.ICMP_REJECT_RST),
+                ),
+                id="reset",
+            ),
+            pytest.param(
+                (
+                    "reset6",
+                    Insn(IpFwOpcode.O_UNREACH6, arg1=Icmp6RejectCode.ICMP6_UNREACH_RST),
+                ),
+                id="reset6",
+            ),
+            pytest.param(
+                (
+                    "unreach port",
+                    InsnReject(
+                        IpFwOpcode.O_REJECT, arg1=IcmpRejectCode.ICMP_UNREACH_PORT
+                    ),
+                ),
+                id="unreach_port",
+            ),
+            pytest.param(
+                (
+                    "unreach port",
+                    InsnReject(
+                        IpFwOpcode.O_REJECT, arg1=IcmpRejectCode.ICMP_UNREACH_PORT
+                    ),
+                ),
+                id="unreach_port",
+            ),
+            pytest.param(
+                (
+                    "unreach needfrag",
+                    InsnReject(
+                        IpFwOpcode.O_REJECT, arg1=IcmpRejectCode.ICMP_UNREACH_NEEDFRAG
+                    ),
+                ),
+                id="unreach_needfrag",
+            ),
+            pytest.param(
+                (
+                    "unreach needfrag 1420",
+                    InsnReject(
+                        IpFwOpcode.O_REJECT,
+                        arg1=IcmpRejectCode.ICMP_UNREACH_NEEDFRAG,
+                        mtu=1420,
+                    ),
+                ),
+                id="unreach_needfrag_mtu",
+            ),
+            pytest.param(
+                (
+                    "unreach6 port",
+                    Insn(
+                        IpFwOpcode.O_UNREACH6,
+                        arg1=Icmp6RejectCode.ICMP6_DST_UNREACH_NOPORT,
+                    ),
+                ),
+                id="unreach6_port",
+            ),
+            pytest.param(("count", InsnEmpty(IpFwOpcode.O_COUNT)), id="count"),
+            # TOK_NAT
+            pytest.param(
+                ("queue 42", Insn(IpFwOpcode.O_QUEUE, arg1=42)), id="queue_42"
+            ),
+            pytest.param(("pipe 42", Insn(IpFwOpcode.O_PIPE, arg1=42)), id="pipe_42"),
+            pytest.param(
+                ("skipto 42", Insn(IpFwOpcode.O_SKIPTO, arg1=42)), id="skipto_42"
+            ),
+            pytest.param(
+                ("netgraph 42", Insn(IpFwOpcode.O_NETGRAPH, arg1=42)), id="netgraph_42"
+            ),
+            pytest.param(
+                ("ngtee 42", Insn(IpFwOpcode.O_NGTEE, arg1=42)), id="ngtee_42"
+            ),
+            pytest.param(
+                ("divert 42", Insn(IpFwOpcode.O_DIVERT, arg1=42)), id="divert_42"
+            ),
+            pytest.param(
+                ("divert natd", Insn(IpFwOpcode.O_DIVERT, arg1=8668)), id="divert_natd"
+            ),
+            pytest.param(("tee 42", Insn(IpFwOpcode.O_TEE, arg1=42)), id="tee_42"),
+            pytest.param(
+                ("call 420", Insn(IpFwOpcode.O_CALLRETURN, arg1=420)), id="call_420"
+            ),
+            # TOK_FORWARD
+            # TOK_COMMENT
+            pytest.param(
+                ("setfib 1", Insn(IpFwOpcode.O_SETFIB, arg1=1 | 0x8000)),
+                id="setfib_1",
+                marks=pytest.mark.skip("needs net.fibs>1"),
+            ),
+            pytest.param(
+                ("setdscp 42", Insn(IpFwOpcode.O_SETDSCP, arg1=42 | 0x8000)),
+                id="setdscp_42",
+            ),
+            pytest.param(("reass", InsnEmpty(IpFwOpcode.O_REASS)), id="reass"),
+            pytest.param(
+                ("return", InsnEmpty(IpFwOpcode.O_CALLRETURN, is_not=True)), id="return"
+            ),
+        ],
+    )
+    def test_add_action(self, action):
+        """Tests if the rule action is compiled properly"""
+        rule_in = "add {} ip from any to any".format(action[0])
+        rule_out = {"insns": [action[1]]}
+        self.verify_rule(rule_in, rule_out)
+
+    @pytest.mark.parametrize(
+        "insn",
+        [
+            pytest.param(
+                {
+                    "in": "add prob 0.7 allow ip from any to any",
+                    "out": InsnProb(prob=0.7),
+                },
+                id="test_prob",
+            ),
+            pytest.param(
+                {
+                    "in": "add allow tcp from any to any",
+                    "out": InsnProto(arg1=6),
+                },
+                id="test_proto",
+            ),
+            pytest.param(
+                {
+                    "in": "add allow ip from any to any 57",
+                    "out": InsnPorts(IpFwOpcode.O_IP_DSTPORT, port_pairs=[57, 57]),
+                },
+                id="test_ports",
+            ),
+        ],
+    )
+    def test_add_single_instruction(self, insn):
+        """Tests if the compiled rule is sane and matches the spec"""
+
+        # Prepare the desired output
+        out = {
+            "insns": [insn["out"], InsnEmpty(IpFwOpcode.O_ACCEPT)],
+        }
+        self.verify_rule(insn["in"], out)
+
+    @pytest.mark.parametrize(
+        "opcode",
+        [
+            pytest.param(IpFwOpcode.O_IP_SRCPORT, id="src"),
+            pytest.param(IpFwOpcode.O_IP_DSTPORT, id="dst"),
+        ],
+    )
+    @pytest.mark.parametrize(
+        "params",
+        [
+            pytest.param(
+                {
+                    "in": "57",
+                    "out": [(57, 57)],
+                },
+                id="test_single",
+            ),
+            pytest.param(
+                {
+                    "in": "57-59",
+                    "out": [(57, 59)],
+                },
+                id="test_range",
+            ),
+            pytest.param(
+                {
+                    "in": "57-59,41",
+                    "out": [(57, 59), (41, 41)],
+                },
+                id="test_ranges",
+            ),
+        ],
+    )
+    def test_add_ports(self, params, opcode):
+        if opcode == IpFwOpcode.O_IP_DSTPORT:
+            txt = "add allow ip from any to any " + params["in"]
+        else:
+            txt = "add allow ip from any " + params["in"] + " to any"
+        out = {
+            "insns": [
+                InsnPorts(opcode, port_pairs=params["out"]),
+                InsnEmpty(IpFwOpcode.O_ACCEPT),
+            ]
+        }
+        self.verify_rule(txt, out)
diff --git a/tests/atf_python/sys/Makefile b/tests/atf_python/sys/Makefile
index 540c3803ada5..85f66a85088e 100644
--- a/tests/atf_python/sys/Makefile
+++ b/tests/atf_python/sys/Makefile
@@ -3,7 +3,7 @@
 .PATH:	${.CURDIR}
 
 FILES=	__init__.py
-SUBDIR=	net netlink
+SUBDIR=	net netlink netpfil
 
 .include <bsd.own.mk>
 FILESDIR=	${TESTSBASE}/atf_python/sys
diff --git a/tests/atf_python/sys/netpfil/Makefile b/tests/atf_python/sys/netpfil/Makefile
new file mode 100644
index 000000000000..417a16d85359
--- /dev/null
+++ b/tests/atf_python/sys/netpfil/Makefile
@@ -0,0 +1,11 @@
+.include <src.opts.mk>
+
+.PATH:	${.CURDIR}
+
+FILES=	__init__.py
+SUBDIR=	ipfw
+
+.include <bsd.own.mk>
+FILESDIR=	${TESTSBASE}/atf_python/sys/netpfil
+
+.include <bsd.prog.mk>
diff --git a/tests/atf_python/sys/netpfil/__init__.py b/tests/atf_python/sys/netpfil/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/tests/atf_python/sys/netpfil/ipfw/Makefile b/tests/atf_python/sys/netpfil/ipfw/Makefile
new file mode 100644
index 000000000000..a85dc7de9417
--- /dev/null
+++ b/tests/atf_python/sys/netpfil/ipfw/Makefile
@@ -0,0 +1,12 @@
+.include <src.opts.mk>
+
+.PATH:	${.CURDIR}
+
+FILES=	__init__.py insns.py insn_headers.py ioctl.py ioctl_headers.py \
+	ipfw.py utils.py
+
+.include <bsd.own.mk>
+FILESDIR=	${TESTSBASE}/atf_python/sys/netpfil/ipfw
+
+.include <bsd.prog.mk>
+
diff --git a/tests/atf_python/sys/netpfil/ipfw/__init__.py b/tests/atf_python/sys/netpfil/ipfw/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/tests/atf_python/sys/netpfil/ipfw/insn_headers.py b/tests/atf_python/sys/netpfil/ipfw/insn_headers.py
new file mode 100644
index 000000000000..5c160d0758d6
--- /dev/null
+++ b/tests/atf_python/sys/netpfil/ipfw/insn_headers.py
@@ -0,0 +1,198 @@
+from enum import Enum
+
+
+class IpFwOpcode(Enum):
+    O_NOP = 0
+    O_IP_SRC = 1
+    O_IP_SRC_MASK = 2
+    O_IP_SRC_ME = 3
+    O_IP_SRC_SET = 4
+    O_IP_DST = 5
+    O_IP_DST_MASK = 6
+    O_IP_DST_ME = 7
+    O_IP_DST_SET = 8
+    O_IP_SRCPORT = 9
+    O_IP_DSTPORT = 10
+    O_PROTO = 11
+    O_MACADDR2 = 12
+    O_MAC_TYPE = 13
+    O_LAYER2 = 14
+    O_IN = 15
+    O_FRAG = 16
+    O_RECV = 17
+    O_XMIT = 18
+    O_VIA = 19
+    O_IPOPT = 20
+    O_IPLEN = 21
+    O_IPID = 22
+    O_IPTOS = 23
+    O_IPPRECEDENCE = 24
+    O_IPTTL = 25
+    O_IPVER = 26
+    O_UID = 27
+    O_GID = 28
+    O_ESTAB = 29
+    O_TCPFLAGS = 30
+    O_TCPWIN = 31
+    O_TCPSEQ = 32
+    O_TCPACK = 33
+    O_ICMPTYPE = 34
+    O_TCPOPTS = 35
+    O_VERREVPATH = 36
+    O_VERSRCREACH = 37
+    O_PROBE_STATE = 38
+    O_KEEP_STATE = 39
+    O_LIMIT = 40
+    O_LIMIT_PARENT = 41
+    O_LOG = 42
+    O_PROB = 43
+    O_CHECK_STATE = 44
+    O_ACCEPT = 45
+    O_DENY = 46
+    O_REJECT = 47
+    O_COUNT = 48
+    O_SKIPTO = 49
+    O_PIPE = 50
+    O_QUEUE = 51
+    O_DIVERT = 52
+    O_TEE = 53
+    O_FORWARD_IP = 54
+    O_FORWARD_MAC = 55
+    O_NAT = 56
+    O_REASS = 57
+    O_IPSEC = 58
+    O_IP_SRC_LOOKUP = 59
+    O_IP_DST_LOOKUP = 60
+    O_ANTISPOOF = 61
+    O_JAIL = 62
+    O_ALTQ = 63
+    O_DIVERTED = 64
+    O_TCPDATALEN = 65
+    O_IP6_SRC = 66
+    O_IP6_SRC_ME = 67
+    O_IP6_SRC_MASK = 68
+    O_IP6_DST = 69
+    O_IP6_DST_ME = 70
+    O_IP6_DST_MASK = 71
+    O_FLOW6ID = 72
+    O_ICMP6TYPE = 73
+    O_EXT_HDR = 74
+    O_IP6 = 75
+    O_NETGRAPH = 76
+    O_NGTEE = 77
+    O_IP4 = 78
+    O_UNREACH6 = 79
+    O_TAG = 80
+    O_TAGGED = 81
+    O_SETFIB = 82
+    O_FIB = 83
+    O_SOCKARG = 84
+    O_CALLRETURN = 85
+    O_FORWARD_IP6 = 86
+    O_DSCP = 87
+    O_SETDSCP = 88
+    O_IP_FLOW_LOOKUP = 89
+    O_EXTERNAL_ACTION = 90
+    O_EXTERNAL_INSTANCE = 91
+    O_EXTERNAL_DATA = 92
+    O_SKIP_ACTION = 93
+    O_TCPMSS = 94
+    O_MAC_SRC_LOOKUP = 95
+    O_MAC_DST_LOOKUP = 96
+    O_SETMARK = 97
+    O_MARK = 98
+    O_LAST_OPCODE = 99
+
+
+class Op3CmdType(Enum):
+    IP_FW_TABLE_XADD = 86
+    IP_FW_TABLE_XDEL = 87
+    IP_FW_TABLE_XGETSIZE = 88
+    IP_FW_TABLE_XLIST = 89
+    IP_FW_TABLE_XDESTROY = 90
+    IP_FW_TABLES_XLIST = 92
+    IP_FW_TABLE_XINFO = 93
+    IP_FW_TABLE_XFLUSH = 94
+    IP_FW_TABLE_XCREATE = 95
+    IP_FW_TABLE_XMODIFY = 96
+    IP_FW_XGET = 97
+    IP_FW_XADD = 98
+    IP_FW_XDEL = 99
+    IP_FW_XMOVE = 100
+    IP_FW_XZERO = 101
+    IP_FW_XRESETLOG = 102
+    IP_FW_SET_SWAP = 103
+    IP_FW_SET_MOVE = 104
+    IP_FW_SET_ENABLE = 105
+    IP_FW_TABLE_XFIND = 106
+    IP_FW_XIFLIST = 107
+    IP_FW_TABLES_ALIST = 108
+    IP_FW_TABLE_XSWAP = 109
+    IP_FW_TABLE_VLIST = 110
+    IP_FW_NAT44_XCONFIG = 111
+    IP_FW_NAT44_DESTROY = 112
+    IP_FW_NAT44_XGETCONFIG = 113
+    IP_FW_NAT44_LIST_NAT = 114
+    IP_FW_NAT44_XGETLOG = 115
+    IP_FW_DUMP_SOPTCODES = 116
+    IP_FW_DUMP_SRVOBJECTS = 117
+    IP_FW_NAT64STL_CREATE = 130
+    IP_FW_NAT64STL_DESTROY = 131
+    IP_FW_NAT64STL_CONFIG = 132
+    IP_FW_NAT64STL_LIST = 133
+    IP_FW_NAT64STL_STATS = 134
+    IP_FW_NAT64STL_RESET_STATS = 135
+    IP_FW_NAT64LSN_CREATE = 140
+    IP_FW_NAT64LSN_DESTROY = 141
+    IP_FW_NAT64LSN_CONFIG = 142
+    IP_FW_NAT64LSN_LIST = 143
+    IP_FW_NAT64LSN_STATS = 144
+    IP_FW_NAT64LSN_LIST_STATES = 145
+    IP_FW_NAT64LSN_RESET_STATS = 146
+    IP_FW_NPTV6_CREATE = 150
+    IP_FW_NPTV6_DESTROY = 151
+    IP_FW_NPTV6_CONFIG = 152
+    IP_FW_NPTV6_LIST = 153
+    IP_FW_NPTV6_STATS = 154
+    IP_FW_NPTV6_RESET_STATS = 155
+    IP_FW_NAT64CLAT_CREATE = 160
+    IP_FW_NAT64CLAT_DESTROY = 161
+    IP_FW_NAT64CLAT_CONFIG = 162
+    IP_FW_NAT64CLAT_LIST = 163
+    IP_FW_NAT64CLAT_STATS = 164
+    IP_FW_NAT64CLAT_RESET_STATS = 165
+
+
+class IcmpRejectCode(Enum):
+    ICMP_UNREACH_NET = 0
+    ICMP_UNREACH_HOST = 1
+    ICMP_UNREACH_PROTOCOL = 2
+    ICMP_UNREACH_PORT = 3
+    ICMP_UNREACH_NEEDFRAG = 4
+    ICMP_UNREACH_SRCFAIL = 5
+    ICMP_UNREACH_NET_UNKNOWN = 6
+    ICMP_UNREACH_HOST_UNKNOWN = 7
+    ICMP_UNREACH_ISOLATED = 8
+    ICMP_UNREACH_NET_PROHIB = 9
+    ICMP_UNREACH_HOST_PROHIB = 10
+    ICMP_UNREACH_TOSNET = 11
+    ICMP_UNREACH_TOSHOST = 12
+    ICMP_UNREACH_FILTER_PROHIB = 13
+    ICMP_UNREACH_HOST_PRECEDENCE = 14
+    ICMP_UNREACH_PRECEDENCE_CUTOFF = 15
+    ICMP_REJECT_RST = 256
+    ICMP_REJECT_ABORT = 257
+
+
+class Icmp6RejectCode(Enum):
+    ICMP6_DST_UNREACH_NOROUTE = 0
+    ICMP6_DST_UNREACH_ADMIN = 1
+    ICMP6_DST_UNREACH_BEYONDSCOPE = 2
+    ICMP6_DST_UNREACH_NOTNEIGHBOR = 2
+    ICMP6_DST_UNREACH_ADDR = 3
+    ICMP6_DST_UNREACH_NOPORT = 4
+    ICMP6_DST_UNREACH_POLICY = 5
+    ICMP6_DST_UNREACH_REJECT = 6
+    ICMP6_DST_UNREACH_SRCROUTE = 7
+    ICMP6_UNREACH_RST = 256
+    ICMP6_UNREACH_ABORT = 257
diff --git a/tests/atf_python/sys/netpfil/ipfw/insns.py b/tests/atf_python/sys/netpfil/ipfw/insns.py
new file mode 100644
index 000000000000..12f145f49393
--- /dev/null
+++ b/tests/atf_python/sys/netpfil/ipfw/insns.py
@@ -0,0 +1,555 @@
+#!/usr/bin/env python3
+import os
+import socket
+import struct
+import subprocess
+import sys
+from ctypes import c_byte
+from ctypes import c_char
+from ctypes import c_int
+from ctypes import c_long
+from ctypes import c_uint32
+from ctypes import c_uint8
+from ctypes import c_ulong
+from ctypes import c_ushort
+from ctypes import sizeof
+from ctypes import Structure
+from enum import Enum
+from typing import Any
+from typing import Dict
+from typing import List
+from typing import NamedTuple
+from typing import Optional
+from typing import Union
+
+from atf_python.sys.netpfil.ipfw.insn_headers import IpFwOpcode
+from atf_python.sys.netpfil.ipfw.insn_headers import IcmpRejectCode
+from atf_python.sys.netpfil.ipfw.insn_headers import Icmp6RejectCode
+from atf_python.sys.netpfil.ipfw.utils import AttrDescr
+from atf_python.sys.netpfil.ipfw.utils import enum_or_int
+from atf_python.sys.netpfil.ipfw.utils import enum_from_int
+from atf_python.sys.netpfil.ipfw.utils import prepare_attrs_map
+
+
+insn_actions = (
+    IpFwOpcode.O_CHECK_STATE.value,
+    IpFwOpcode.O_REJECT.value,
+    IpFwOpcode.O_UNREACH6.value,
+    IpFwOpcode.O_ACCEPT.value,
+    IpFwOpcode.O_DENY.value,
+    IpFwOpcode.O_COUNT.value,
+    IpFwOpcode.O_NAT.value,
+    IpFwOpcode.O_QUEUE.value,
+    IpFwOpcode.O_PIPE.value,
+    IpFwOpcode.O_SKIPTO.value,
+    IpFwOpcode.O_NETGRAPH.value,
+    IpFwOpcode.O_NGTEE.value,
+    IpFwOpcode.O_DIVERT.value,
+    IpFwOpcode.O_TEE.value,
+    IpFwOpcode.O_CALLRETURN.value,
+    IpFwOpcode.O_FORWARD_IP.value,
+    IpFwOpcode.O_FORWARD_IP6.value,
+    IpFwOpcode.O_SETFIB.value,
+    IpFwOpcode.O_SETDSCP.value,
+    IpFwOpcode.O_REASS.value,
+    IpFwOpcode.O_SETMARK.value,
+    IpFwOpcode.O_EXTERNAL_ACTION.value,
+)
+
+
+class IpFwInsn(Structure):
+    _fields_ = [
+        ("opcode", c_uint8),
+        ("length", c_uint8),
+        ("arg1", c_ushort),
+    ]
+
+
+class BaseInsn(object):
+    obj_enum_class = IpFwOpcode
+
+    def __init__(self, opcode, is_or, is_not, arg1):
+        if isinstance(opcode, Enum):
+            self.obj_type = opcode.value
+            self._enum = opcode
+        else:
+            self.obj_type = opcode
+            self._enum = enum_from_int(self.obj_enum_class, self.obj_type)
+        self.is_or = is_or
+        self.is_not = is_not
+        self.arg1 = arg1
+        self.is_action = self.obj_type in insn_actions
+        self.ilen = 1
+        self.obj_list = []
+
+    @property
+    def obj_name(self):
+        if self._enum is not None:
+            return self._enum.name
+        else:
+            return "opcode#{}".format(self.obj_type)
+
+    @staticmethod
+    def get_insn_len(data: bytes) -> int:
+        (opcode_len,) = struct.unpack("@B", data[1:2])
+        return opcode_len & 0x3F
+
+    @classmethod
+    def _validate_len(cls, data, valid_options=None):
+        if len(data) < 4:
+            raise ValueError("opcode too short")
+        opcode_type, opcode_len = struct.unpack("@BB", data[:2])
+        if len(data) != ((opcode_len & 0x3F) * 4):
+            raise ValueError("wrong length")
+        if valid_options and len(data) not in valid_options:
+            raise ValueError(
+                "len {} not in {} for {}".format(
+                    len(data), valid_options,
+                    enum_from_int(cls.obj_enum_class, data[0])
+                )
+            )
+
+    @classmethod
+    def _validate(cls, data):
+        cls._validate_len(data)
+
+    @classmethod
+    def _parse(cls, data):
+        insn = IpFwInsn.from_buffer_copy(data[:4])
+        is_or = (insn.length & 0x40) != 0
+        is_not = (insn.length & 0x80) != 0
+        return cls(opcode=insn.opcode, is_or=is_or, is_not=is_not, arg1=insn.arg1)
+
*** 1231 LINES SKIPPED ***