xref: /freebsd/tests/sys/netpfil/pf/icmp.py (revision 86f2641b99f01eb8e8191c4435f22c17433b0c2f)
1b27d3f71SKristof Provost#
2b27d3f71SKristof Provost# SPDX-License-Identifier: BSD-2-Clause
3b27d3f71SKristof Provost#
4b27d3f71SKristof Provost# Copyright (c) 2024 Rubicon Communications, LLC (Netgate)
5b27d3f71SKristof Provost#
6b27d3f71SKristof Provost# Redistribution and use in source and binary forms, with or without
7b27d3f71SKristof Provost# modification, are permitted provided that the following conditions
8b27d3f71SKristof Provost# are met:
9b27d3f71SKristof Provost# 1. Redistributions of source code must retain the above copyright
10b27d3f71SKristof Provost#    notice, this list of conditions and the following disclaimer.
11b27d3f71SKristof Provost# 2. Redistributions in binary form must reproduce the above copyright
12b27d3f71SKristof Provost#    notice, this list of conditions and the following disclaimer in the
13b27d3f71SKristof Provost#    documentation and/or other materials provided with the distribution.
14b27d3f71SKristof Provost#
15b27d3f71SKristof Provost# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
16b27d3f71SKristof Provost# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17b27d3f71SKristof Provost# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18b27d3f71SKristof Provost# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
19b27d3f71SKristof Provost# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20b27d3f71SKristof Provost# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
21b27d3f71SKristof Provost# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22b27d3f71SKristof Provost# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
23b27d3f71SKristof Provost# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
24b27d3f71SKristof Provost# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
25b27d3f71SKristof Provost# SUCH DAMAGE.
26b27d3f71SKristof Provost
27b27d3f71SKristof Provostimport pytest
28b27d3f71SKristof Provostimport subprocess
29b27d3f71SKristof Provostimport re
30b27d3f71SKristof Provostfrom atf_python.sys.net.tools import ToolsHelper
31b27d3f71SKristof Provostfrom atf_python.sys.net.vnet import VnetTestTemplate
32b27d3f71SKristof Provost
33b27d3f71SKristof Provostdef check(cmd):
34b27d3f71SKristof Provost    ps = subprocess.Popen(cmd, shell=True)
35b27d3f71SKristof Provost    ret = ps.wait()
36b27d3f71SKristof Provost    if ret != 0:
37b27d3f71SKristof Provost        raise Exception("Command %s returned %d" % (cmd, ret))
38b27d3f71SKristof Provost
39b27d3f71SKristof Provostclass TestICMP(VnetTestTemplate):
40b27d3f71SKristof Provost    REQUIRED_MODULES = [ "pf" ]
41b27d3f71SKristof Provost    TOPOLOGY = {
42b27d3f71SKristof Provost        "vnet1": {"ifaces": ["if1"]},
43b27d3f71SKristof Provost        "vnet2": {"ifaces": ["if1", "if2"]},
44b27d3f71SKristof Provost        "vnet3": {"ifaces": ["if2"]},
45b27d3f71SKristof Provost        "if1": {"prefixes4": [("192.0.2.2/24", "192.0.2.1/24")]},
46b27d3f71SKristof Provost        "if2": {"prefixes4": [("198.51.100.1/24", "198.51.100.2/24")]},
47b27d3f71SKristof Provost    }
48b27d3f71SKristof Provost
49b27d3f71SKristof Provost    def vnet2_handler(self, vnet):
50b27d3f71SKristof Provost        ifname = vnet.iface_alias_map["if1"].name
51fffedd81SKristof Provost        if2name = vnet.iface_alias_map["if2"].name
52b27d3f71SKristof Provost
53b27d3f71SKristof Provost        ToolsHelper.print_output("/sbin/pfctl -e")
54b27d3f71SKristof Provost        ToolsHelper.pf_rules([
55b27d3f71SKristof Provost            "set reassemble yes",
56*86f2641bSKristof Provost            "set state-policy if-bound",
57b27d3f71SKristof Provost            "block",
58b27d3f71SKristof Provost            "pass inet proto icmp icmp-type echoreq",
59b27d3f71SKristof Provost            ])
60b27d3f71SKristof Provost
61b27d3f71SKristof Provost        ToolsHelper.print_output("/sbin/sysctl net.inet.ip.forwarding=1")
62b27d3f71SKristof Provost        ToolsHelper.print_output("/sbin/pfctl -x loud")
63b27d3f71SKristof Provost
64fffedd81SKristof Provost        ToolsHelper.print_output("/sbin/ifconfig %s mtu 1492" % if2name)
65fffedd81SKristof Provost
66b27d3f71SKristof Provost    def vnet3_handler(self, vnet):
67b27d3f71SKristof Provost        # Import in the correct vnet, so at to not confuse Scapy
68b27d3f71SKristof Provost        import scapy.all as sp
69b27d3f71SKristof Provost
70b27d3f71SKristof Provost        ifname = vnet.iface_alias_map["if2"].name
71b27d3f71SKristof Provost        ToolsHelper.print_output("/sbin/route add default 198.51.100.1")
72b27d3f71SKristof Provost        ToolsHelper.print_output("/sbin/ifconfig %s inet alias 198.51.100.3/24" % ifname)
73fffedd81SKristof Provost        ToolsHelper.print_output("/sbin/ifconfig %s mtu 1492" % ifname)
74b27d3f71SKristof Provost
75b27d3f71SKristof Provost        def checkfn(packet):
76b27d3f71SKristof Provost            icmp = packet.getlayer(sp.ICMP)
77b27d3f71SKristof Provost            if not icmp:
78b27d3f71SKristof Provost                return False
79b27d3f71SKristof Provost
80b27d3f71SKristof Provost            if icmp.type != 3:
81b27d3f71SKristof Provost                return False
82b27d3f71SKristof Provost
83b27d3f71SKristof Provost            packet.show()
84b27d3f71SKristof Provost            return True
85b27d3f71SKristof Provost
86b27d3f71SKristof Provost        sp.sniff(iface=ifname, stop_filter=checkfn)
87b27d3f71SKristof Provost        vnet.pipe.send("Got ICMP destination unreachable packet")
88b27d3f71SKristof Provost
89b27d3f71SKristof Provost    @pytest.mark.require_user("root")
9065cc5af1SJose Luis Duran    @pytest.mark.require_progs(["scapy"])
91b27d3f71SKristof Provost    def test_inner_match(self):
92b27d3f71SKristof Provost        vnet = self.vnet_map["vnet1"]
93b27d3f71SKristof Provost        dst_vnet = self.vnet_map["vnet3"]
94b27d3f71SKristof Provost        sendif = vnet.iface_alias_map["if1"].name
95b27d3f71SKristof Provost
96b27d3f71SKristof Provost        our_mac = ToolsHelper.get_output("/sbin/ifconfig %s ether | awk '/ether/ { print $2; }'" % sendif)
97b27d3f71SKristof Provost        dst_mac = re.sub("0a$", "0b", our_mac)
98b27d3f71SKristof Provost
99b27d3f71SKristof Provost        # Import in the correct vnet, so at to not confuse Scapy
100b27d3f71SKristof Provost        import scapy.all as sp
101b27d3f71SKristof Provost
102b27d3f71SKristof Provost        ToolsHelper.print_output("/sbin/route add default 192.0.2.1")
103b27d3f71SKristof Provost
104b27d3f71SKristof Provost        # Sanity check
105b27d3f71SKristof Provost        check("/sbin/ping -c 1 192.0.2.1")
106b27d3f71SKristof Provost        check("/sbin/ping -c 1 198.51.100.1")
107b27d3f71SKristof Provost        check("/sbin/ping -c 2 198.51.100.3")
108b27d3f71SKristof Provost
109b27d3f71SKristof Provost        # Establish a state
110b27d3f71SKristof Provost        pkt = sp.Ether(src=our_mac, dst=dst_mac) \
111b27d3f71SKristof Provost            / sp.IP(src="192.0.2.2", dst="198.51.100.2") \
112b27d3f71SKristof Provost            / sp.ICMP(type='echo-request') \
113b27d3f71SKristof Provost            / "PAYLOAD"
114b27d3f71SKristof Provost        sp.sendp(pkt, sendif, verbose=False)
115b27d3f71SKristof Provost
116b27d3f71SKristof Provost        # Now try to pass an ICMP error message piggy-backing on that state, but
117b27d3f71SKristof Provost        # use a different source address
118b27d3f71SKristof Provost        pkt = sp.Ether(src=our_mac, dst=dst_mac) \
119b27d3f71SKristof Provost            / sp.IP(src="192.0.2.2", dst="198.51.100.3") \
120b27d3f71SKristof Provost            / sp.ICMP(type='dest-unreach') \
121b27d3f71SKristof Provost            / sp.IP(src="198.51.100.2", dst="192.0.2.2") \
122b27d3f71SKristof Provost            / sp.ICMP(type='echo-reply')
123b27d3f71SKristof Provost        sp.sendp(pkt, sendif, verbose=False)
124b27d3f71SKristof Provost
125b27d3f71SKristof Provost        try:
126b27d3f71SKristof Provost            rcvd = self.wait_object(dst_vnet.pipe, timeout=1)
127b27d3f71SKristof Provost            if rcvd:
128b27d3f71SKristof Provost                raise Exception(rcvd)
129b27d3f71SKristof Provost        except TimeoutError as e:
130b27d3f71SKristof Provost            # We expect the timeout here. It means we didn't get the destination
131b27d3f71SKristof Provost            # unreachable packet in vnet3
132b27d3f71SKristof Provost            pass
133fffedd81SKristof Provost
134fffedd81SKristof Provost    def check_icmp_echo(self, sp, payload_size):
135fffedd81SKristof Provost        packet = sp.IP(dst="198.51.100.2", flags="DF") \
136fffedd81SKristof Provost            / sp.ICMP(type='echo-request') \
137fffedd81SKristof Provost            / sp.raw(bytes.fromhex('f0') * payload_size)
138fffedd81SKristof Provost
139fffedd81SKristof Provost        p = sp.sr1(packet, iface=self.vnet.iface_alias_map["if1"].name,
140fffedd81SKristof Provost            timeout=3)
141fffedd81SKristof Provost        p.show()
142fffedd81SKristof Provost
143fffedd81SKristof Provost        ip = p.getlayer(sp.IP)
144fffedd81SKristof Provost        icmp = p.getlayer(sp.ICMP)
145fffedd81SKristof Provost        assert ip
146fffedd81SKristof Provost        assert icmp
147fffedd81SKristof Provost
148fffedd81SKristof Provost        if payload_size > 1464:
149fffedd81SKristof Provost            # Expect ICMP destination unreachable, fragmentation needed
150fffedd81SKristof Provost            assert ip.src == "192.0.2.1"
151fffedd81SKristof Provost            assert ip.dst == "192.0.2.2"
152fffedd81SKristof Provost            assert icmp.type == 3 # dest-unreach
153fffedd81SKristof Provost            assert icmp.code == 4
154fffedd81SKristof Provost            assert icmp.nexthopmtu == 1492
155fffedd81SKristof Provost        else:
156fffedd81SKristof Provost            # Expect echo reply
157fffedd81SKristof Provost            assert ip.src == "198.51.100.2"
158fffedd81SKristof Provost            assert ip.dst == "192.0.2.2"
159fffedd81SKristof Provost            assert icmp.type == 0 # "echo-reply"
160fffedd81SKristof Provost            assert icmp.code == 0
161fffedd81SKristof Provost
162fffedd81SKristof Provost        return
163fffedd81SKristof Provost
164fffedd81SKristof Provost    @pytest.mark.require_user("root")
16565cc5af1SJose Luis Duran    @pytest.mark.require_progs(["scapy"])
166fffedd81SKristof Provost    def test_fragmentation_needed(self):
167fffedd81SKristof Provost        ToolsHelper.print_output("/sbin/route add default 192.0.2.1")
168fffedd81SKristof Provost
169fffedd81SKristof Provost        ToolsHelper.print_output("/sbin/ping -c 1 198.51.100.2")
170fffedd81SKristof Provost        ToolsHelper.print_output("/sbin/ping -c 1 -D -s 1472 198.51.100.2")
171fffedd81SKristof Provost
172fffedd81SKristof Provost        # Import in the correct vnet, so at to not confuse Scapy
173fffedd81SKristof Provost        import scapy.all as sp
174fffedd81SKristof Provost
175fffedd81SKristof Provost        self.check_icmp_echo(sp, 128)
176fffedd81SKristof Provost        self.check_icmp_echo(sp, 1464)
177fffedd81SKristof Provost        self.check_icmp_echo(sp, 1468)
178