xref: /freebsd/tests/sys/netpfil/pf/header.py (revision 7659d0fa2b873374623e7582e38fb2fe92056c87)
132546d57SKristof Provost#
232546d57SKristof Provost# SPDX-License-Identifier: BSD-2-Clause
332546d57SKristof Provost#
432546d57SKristof Provost# Copyright (c) 2025 Rubicon Communications, LLC (Netgate)
532546d57SKristof Provost#
632546d57SKristof Provost# Redistribution and use in source and binary forms, with or without
732546d57SKristof Provost# modification, are permitted provided that the following conditions
832546d57SKristof Provost# are met:
932546d57SKristof Provost# 1. Redistributions of source code must retain the above copyright
1032546d57SKristof Provost#    notice, this list of conditions and the following disclaimer.
1132546d57SKristof Provost# 2. Redistributions in binary form must reproduce the above copyright
1232546d57SKristof Provost#    notice, this list of conditions and the following disclaimer in the
1332546d57SKristof Provost#    documentation and/or other materials provided with the distribution.
1432546d57SKristof Provost#
1532546d57SKristof Provost# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
1632546d57SKristof Provost# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
1732546d57SKristof Provost# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
1832546d57SKristof Provost# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
1932546d57SKristof Provost# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
2032546d57SKristof Provost# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
2132546d57SKristof Provost# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
2232546d57SKristof Provost# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
2332546d57SKristof Provost# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
2432546d57SKristof Provost# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
2532546d57SKristof Provost# SUCH DAMAGE.
2632546d57SKristof Provost
2732546d57SKristof Provostimport pytest
2832546d57SKristof Provostimport re
2932546d57SKristof Provostfrom utils import DelayedSend
3032546d57SKristof Provostfrom atf_python.sys.net.tools import ToolsHelper
3132546d57SKristof Provostfrom atf_python.sys.net.vnet import VnetTestTemplate
3232546d57SKristof Provost
3332546d57SKristof Provostclass TestHeader(VnetTestTemplate):
3432546d57SKristof Provost    REQUIRED_MODULES = [ "pf" ]
3532546d57SKristof Provost    TOPOLOGY = {
3632546d57SKristof Provost        "vnet1": {"ifaces": ["if1", "if2"]},
3732546d57SKristof Provost        "vnet2": {"ifaces": ["if1", "if2"]},
3832546d57SKristof Provost        "if1": {"prefixes4": [("192.0.2.2/24", "192.0.2.1/24")]},
3932546d57SKristof Provost        "if2": {"prefixes4": [("198.51.100.1/24", "198.51.100.2/24")]},
4032546d57SKristof Provost    }
4132546d57SKristof Provost
4232546d57SKristof Provost    def vnet2_handler(self, vnet):
4332546d57SKristof Provost        ToolsHelper.print_output("/sbin/sysctl net.inet.ip.forwarding=1")
4432546d57SKristof Provost        ToolsHelper.print_output("/usr/sbin/arp -s 198.51.100.3 00:01:02:03:04:05")
4532546d57SKristof Provost        ToolsHelper.print_output("/sbin/pfctl -e")
4632546d57SKristof Provost        ToolsHelper.print_output("/sbin/pfctl -x loud")
4732546d57SKristof Provost        ToolsHelper.pf_rules([
4832546d57SKristof Provost            "pass",
4932546d57SKristof Provost            ])
5032546d57SKristof Provost
5132546d57SKristof Provost    @pytest.mark.require_user("root")
5232546d57SKristof Provost    @pytest.mark.require_progs(["scapy"])
5332546d57SKristof Provost    def test_too_many(self):
5432546d57SKristof Provost        "Verify that we drop packets with silly numbers of headers."
5532546d57SKristof Provost
5632546d57SKristof Provost        sendif = self.vnet.iface_alias_map["if1"].name
5732546d57SKristof Provost        recvif = self.vnet.iface_alias_map["if2"].name
5832546d57SKristof Provost        gw_mac = ToolsHelper.get_output("/sbin/ifconfig %s ether | awk '/ether/ { print $2; }'" % sendif)
5932546d57SKristof Provost        gw_mac = re.sub("0a$", "0b", gw_mac)
6032546d57SKristof Provost
6132546d57SKristof Provost        ToolsHelper.print_output("/sbin/route add default 192.0.2.1")
6232546d57SKristof Provost
6332546d57SKristof Provost        # Import in the correct vnet, so at to not confuse Scapy
6432546d57SKristof Provost        import scapy.all as sp
6532546d57SKristof Provost
6632546d57SKristof Provost        # Sanity check, ensure we get replies to normal ping
6732546d57SKristof Provost        pkt = sp.Ether(dst=gw_mac) \
6832546d57SKristof Provost            / sp.IP(dst="198.51.100.3") \
6932546d57SKristof Provost            / sp.ICMP(type='echo-request')
7032546d57SKristof Provost        s = DelayedSend(pkt, sendif)
7132546d57SKristof Provost        reply = sp.sniff(iface=recvif, timeout=3)
7232546d57SKristof Provost        print(reply)
7332546d57SKristof Provost
7432546d57SKristof Provost        found = False
7532546d57SKristof Provost        for r in reply:
7632546d57SKristof Provost            r.show()
7732546d57SKristof Provost
7832546d57SKristof Provost            icmp = r.getlayer(sp.ICMP)
7932546d57SKristof Provost            if not icmp:
8032546d57SKristof Provost                continue
8132546d57SKristof Provost            assert icmp.type == 8 # 'echo-request'
8232546d57SKristof Provost            found = True
8332546d57SKristof Provost        assert found
8432546d57SKristof Provost
8532546d57SKristof Provost        # Up to 19 AH headers will pass
8632546d57SKristof Provost        pkt = sp.Ether(dst=gw_mac) \
8732546d57SKristof Provost            / sp.IP(dst="198.51.100.3")
8832546d57SKristof Provost        for i in range(0, 18):
8932546d57SKristof Provost            pkt = pkt / sp.AH(nh=51, payloadlen=1)
9032546d57SKristof Provost        pkt = pkt / sp.AH(nh=1, payloadlen=1) / sp.ICMP(type='echo-request')
9132546d57SKristof Provost
9232546d57SKristof Provost        s = DelayedSend(pkt, sendif)
9332546d57SKristof Provost        reply = sp.sniff(iface=recvif, timeout=3)
9432546d57SKristof Provost        print(reply)
9532546d57SKristof Provost        found = False
9632546d57SKristof Provost        for r in reply:
9732546d57SKristof Provost            r.show()
9832546d57SKristof Provost
9932546d57SKristof Provost            ah = r.getlayer(sp.AH)
10032546d57SKristof Provost            if not ah:
10132546d57SKristof Provost                continue
10232546d57SKristof Provost            found = True
10332546d57SKristof Provost        assert found
10432546d57SKristof Provost
10532546d57SKristof Provost        # But more will get dropped
10632546d57SKristof Provost        pkt = sp.Ether(dst=gw_mac) \
10732546d57SKristof Provost            / sp.IP(dst="198.51.100.3")
10832546d57SKristof Provost        for i in range(0, 19):
10932546d57SKristof Provost            pkt = pkt / sp.AH(nh=51, payloadlen=1)
11032546d57SKristof Provost        pkt = pkt / sp.AH(nh=1, payloadlen=1) / sp.ICMP(type='echo-request')
11132546d57SKristof Provost
11232546d57SKristof Provost        s = DelayedSend(pkt, sendif)
11332546d57SKristof Provost        reply = sp.sniff(iface=recvif, timeout=3)
11432546d57SKristof Provost        print(reply)
11532546d57SKristof Provost
11632546d57SKristof Provost        found = False
11732546d57SKristof Provost        for r in reply:
11832546d57SKristof Provost            r.show()
11932546d57SKristof Provost
12032546d57SKristof Provost            ah = r.getlayer(sp.AH)
12132546d57SKristof Provost            if not ah:
12232546d57SKristof Provost                continue
12332546d57SKristof Provost            found = True
12432546d57SKristof Provost        assert not found
12532546d57SKristof Provost
12632546d57SKristof Provostclass TestHeader6(VnetTestTemplate):
12732546d57SKristof Provost    REQUIRED_MODULES = [ "pf" ]
128*7659d0faSKristof Provost    SKIP_MODULES = [ "ipfilter" ]
12932546d57SKristof Provost    TOPOLOGY = {
13032546d57SKristof Provost        "vnet1": {"ifaces": ["if1", "if2"]},
13132546d57SKristof Provost        "vnet2": {"ifaces": ["if1", "if2"]},
13232546d57SKristof Provost        "if1": {"prefixes6": [("2001:db8::2/64", "2001:db8::1/64")]},
13332546d57SKristof Provost        "if2": {"prefixes6": [("2001:db8:1::2/64", "2001:db8:1::1/64")]},
13432546d57SKristof Provost    }
13532546d57SKristof Provost
13632546d57SKristof Provost    def vnet2_handler(self, vnet):
13732546d57SKristof Provost        ToolsHelper.print_output("/sbin/sysctl net.inet6.ip6.forwarding=1")
13832546d57SKristof Provost        ToolsHelper.print_output("/usr/sbin/ndp -s 2001:db8:1::3 00:01:02:03:04:05")
13932546d57SKristof Provost        ToolsHelper.print_output("/sbin/pfctl -e")
14032546d57SKristof Provost        ToolsHelper.print_output("/sbin/pfctl -x loud")
14132546d57SKristof Provost        ToolsHelper.pf_rules([
14232546d57SKristof Provost            "pass",
14332546d57SKristof Provost            ])
14432546d57SKristof Provost
14532546d57SKristof Provost    @pytest.mark.require_user("root")
14632546d57SKristof Provost    @pytest.mark.require_progs(["scapy"])
14732546d57SKristof Provost    def test_too_many(self):
14832546d57SKristof Provost        "Verify that we drop packets with silly numbers of headers."
14932546d57SKristof Provost        ToolsHelper.print_output("/sbin/ifconfig")
15032546d57SKristof Provost
15132546d57SKristof Provost        sendif = self.vnet.iface_alias_map["if1"].name
15232546d57SKristof Provost        recvif = self.vnet.iface_alias_map["if2"].name
15332546d57SKristof Provost        our_mac = ToolsHelper.get_output("/sbin/ifconfig %s ether | awk '/ether/ { print $2; }'" % sendif)
15432546d57SKristof Provost        gw_mac = re.sub("0a$", "0b", our_mac)
15532546d57SKristof Provost
15632546d57SKristof Provost        ToolsHelper.print_output("/sbin/route -6 add default 2001:db8::1")
15732546d57SKristof Provost
15832546d57SKristof Provost        # Import in the correct vnet, so at to not confuse Scapy
15932546d57SKristof Provost        import scapy.all as sp
16032546d57SKristof Provost
16132546d57SKristof Provost        # Sanity check, ensure we get replies to normal ping
16232546d57SKristof Provost        pkt = sp.Ether(src=our_mac, dst=gw_mac) \
16332546d57SKristof Provost            / sp.IPv6(src="2001:db8::2", dst="2001:db8:1::3") \
16432546d57SKristof Provost            / sp.ICMPv6EchoRequest()
16532546d57SKristof Provost        s = DelayedSend(pkt, sendif)
16632546d57SKristof Provost        reply = sp.sniff(iface=recvif, timeout=3)
16732546d57SKristof Provost        print(reply)
16832546d57SKristof Provost
16932546d57SKristof Provost        found = False
17032546d57SKristof Provost        for r in reply:
17132546d57SKristof Provost            r.show()
17232546d57SKristof Provost
17332546d57SKristof Provost            icmp = r.getlayer(sp.ICMPv6EchoRequest)
17432546d57SKristof Provost            if not icmp:
17532546d57SKristof Provost                continue
17632546d57SKristof Provost            found = True
17732546d57SKristof Provost        assert found
17832546d57SKristof Provost
17932546d57SKristof Provost        # Up to 19 AH headers will pass
18032546d57SKristof Provost        pkt = sp.Ether(src=our_mac, dst=gw_mac) \
18132546d57SKristof Provost            / sp.IPv6(src="2001:db8::2", dst="2001:db8:1::3")
18232546d57SKristof Provost        for i in range(0, 18):
18332546d57SKristof Provost            pkt = pkt / sp.AH(nh=51, payloadlen=1)
18432546d57SKristof Provost        pkt = pkt / sp.AH(nh=58, payloadlen=1) / sp.ICMPv6EchoRequest()
18532546d57SKristof Provost        s = DelayedSend(pkt, sendif)
18632546d57SKristof Provost        reply = sp.sniff(iface=recvif, timeout=3)
18732546d57SKristof Provost        print(reply)
18832546d57SKristof Provost
18932546d57SKristof Provost        found = False
19032546d57SKristof Provost        for r in reply:
19132546d57SKristof Provost            r.show()
19232546d57SKristof Provost
19332546d57SKristof Provost            ah = r.getlayer(sp.AH)
19432546d57SKristof Provost            if not ah:
19532546d57SKristof Provost                continue
19632546d57SKristof Provost            found = True
19732546d57SKristof Provost        assert found
19832546d57SKristof Provost
19932546d57SKristof Provost        # But more will get dropped
20032546d57SKristof Provost        pkt = sp.Ether(src=our_mac, dst=gw_mac) \
20132546d57SKristof Provost            / sp.IPv6(src="2001:db8::2", dst="2001:db8:1::3")
20232546d57SKristof Provost        for i in range(0, 19):
20332546d57SKristof Provost            pkt = pkt / sp.AH(nh=51, payloadlen=1)
20432546d57SKristof Provost        pkt = pkt / sp.AH(nh=58, payloadlen=1) / sp.ICMPv6EchoRequest()
20532546d57SKristof Provost        s = DelayedSend(pkt, sendif)
20632546d57SKristof Provost        reply = sp.sniff(iface=recvif, timeout=3)
20732546d57SKristof Provost        print(reply)
20832546d57SKristof Provost
20932546d57SKristof Provost        found = False
21032546d57SKristof Provost        for r in reply:
21132546d57SKristof Provost            r.show()
21232546d57SKristof Provost
21332546d57SKristof Provost            ah = r.getlayer(sp.AH)
21432546d57SKristof Provost            if not ah:
21532546d57SKristof Provost                continue
21632546d57SKristof Provost            found = True
21732546d57SKristof Provost        assert not found
218