From nobody Tue Jun 13 11:55:42 2023 X-Original-To: dev-commits-src-all@mlmmj.nyi.freebsd.org Received: from mx1.freebsd.org (mx1.freebsd.org [IPv6:2610:1c1:1:606c::19:1]) by mlmmj.nyi.freebsd.org (Postfix) with ESMTP id 4QgRp62hfBz4d1qr; Tue, 13 Jun 2023 11:55:42 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from mxrelay.nyi.freebsd.org (mxrelay.nyi.freebsd.org [IPv6:2610:1c1:1:606c::19:3]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits) server-digest SHA256 client-signature RSA-PSS (4096 bits) client-digest SHA256) (Client CN "mxrelay.nyi.freebsd.org", Issuer "R3" (verified OK)) by mx1.freebsd.org (Postfix) with ESMTPS id 4QgRp61qqWz3Mg2; Tue, 13 Jun 2023 11:55:42 +0000 (UTC) (envelope-from git@FreeBSD.org) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1686657342; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=VEHI0TZWfkzFpB86OX3Ai4h08+FzYybWoRVISenl1lg=; b=t8s8eWAQmiExBGwkfc7hMe2vHomkpG0Jc41tFKAxGgp1tITst1oiHqev2LoCsr7O7u93Bd uf+KLNZ59eqZBAIHGwKsPXd70eSC+0YIq6M734ikzAtC9qwXdcmteEljB7uln6qNSyHPQo RWVU9uTOvw/I3c4+jeVQGWGdmfwkmixXJdtDttX2S7VBTt/jGrzMRyDbdy+zDZ+6XP4V2D Z9UDw1kzGDIhefpwZmAZdMyyj4xDWrsiIqTVZ0iqNznkXNo+F+qsropXcq2iZvyTdopvp3 mDFGKyKxPapnMeleOtMlx0UmfzGVahEveR1lxriQTnZqJ0E2R0cS+MCOUDxBHw== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1686657342; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=VEHI0TZWfkzFpB86OX3Ai4h08+FzYybWoRVISenl1lg=; b=JWz/O25/QT4Lz/chva41hQTk2yGJH1ue9daUXfd2ANzJo6H31dAi91pBkgN6snMrmenm/H qV9L2cQWEI9GTmqsJHOi7GKPaMkcB/N/2AkOoReEIT48WfQiPh0/He2OZat+G8h6anK0hG /Q0frChAuem21X2A3lusFfSkyRG3alyUAePL9prflh/gGGhUrCg9Qtj5dzFjmwA6Og2RwS 09inpO9ngtbpYNOuD4L0Vk5qmjbzVC/45ZgU8SK8TXl/Xcs16a5uw9g0th0wC+R0eRkPu0 +jKOYcn0WGkJjQ3ZBNgQrJRiCMHPJbJzSfPTOgr1crKB6JQl+16kJxIzb1qiFw== ARC-Authentication-Results: i=1; mx1.freebsd.org; none ARC-Seal: i=1; s=dkim; d=freebsd.org; t=1686657342; a=rsa-sha256; cv=none; b=NJeSwzCrYoN3Hy0UsI7C1+PQzAIIJ3WmvFFnQTfmHmruBS3DOIL8TJdS0g0ORkkGc+qI6y UYPDV1G7yMvI7WtBzHXwZ7e7AG49LsgtwTPWGR3RMp+2Y37O77ePQivyj1GJhJStUDo73x WaT7euZBTpnns9AfE3WPe9VsO6IRBRuWhv7PTFcwETNJvWdX3201N2/hE9K7ZM1YPCmiZz FCLkWWJwJsHYibWSD3VNTrQkIjOSTOY9d4twCZK8V339amvRxm9gVu0YzXySSFifO/aMlP V8zN5TXIoFzn97lUPF/j26ElukMNckT0EZr43d27GwDUpUqi+NQ3x5miLN1ljw== Received: from gitrepo.freebsd.org (gitrepo.freebsd.org [IPv6:2610:1c1:1:6068::e6a:5]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits) server-digest SHA256) (Client did not present a certificate) by mxrelay.nyi.freebsd.org (Postfix) with ESMTPS id 4QgRp60wD6zM2c; Tue, 13 Jun 2023 11:55:42 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from gitrepo.freebsd.org ([127.0.1.44]) by gitrepo.freebsd.org (8.16.1/8.16.1) with ESMTP id 35DBtgFw075969; Tue, 13 Jun 2023 11:55:42 GMT (envelope-from git@gitrepo.freebsd.org) Received: (from git@localhost) by gitrepo.freebsd.org (8.16.1/8.16.1/Submit) id 35DBtg5d075968; Tue, 13 Jun 2023 11:55:42 GMT (envelope-from git) Date: Tue, 13 Jun 2023 11:55:42 GMT Message-Id: <202306131155.35DBtg5d075968@gitrepo.freebsd.org> To: src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-main@FreeBSD.org From: "Alexander V. Chernikov" Subject: git: 9f44a47fd079 - main - ipfw(8): add ioctl/instruction generation tests List-Id: Commit messages for all branches of the src repository List-Archive: https://lists.freebsd.org/archives/dev-commits-src-all List-Help: List-Post: List-Subscribe: List-Unsubscribe: Sender: owner-dev-commits-src-all@freebsd.org X-BeenThere: dev-commits-src-all@freebsd.org MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit X-Git-Committer: melifaro X-Git-Repository: src X-Git-Refname: refs/heads/main X-Git-Reftype: branch X-Git-Commit: 9f44a47fd07924afc035991af15d84e6585dea4f Auto-Submitted: auto-generated X-ThisMailContainsUnwantedMimeParts: N The branch main has been updated by melifaro: URL: https://cgit.FreeBSD.org/src/commit/?id=9f44a47fd07924afc035991af15d84e6585dea4f commit 9f44a47fd07924afc035991af15d84e6585dea4f Author: Alexander V. Chernikov AuthorDate: 2023-06-11 08:12:04 +0000 Commit: Alexander V. Chernikov 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 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 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 + +.PATH: ${.CURDIR} + +FILES= __init__.py +SUBDIR= ipfw + +.include +FILESDIR= ${TESTSBASE}/atf_python/sys/netpfil + +.include 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 + +.PATH: ${.CURDIR} + +FILES= __init__.py insns.py insn_headers.py ioctl.py ioctl_headers.py \ + ipfw.py utils.py + +.include +FILESDIR= ${TESTSBASE}/atf_python/sys/netpfil/ipfw + +.include + 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 ***