xref: /freebsd/tests/sys/netpfil/pf/icmp.py (revision 65cc5af1cf88ed124ab16091624e918faa61c7f2)
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",
56b27d3f71SKristof Provost            "block",
57b27d3f71SKristof Provost            "pass inet proto icmp icmp-type echoreq",
58b27d3f71SKristof Provost            ])
59b27d3f71SKristof Provost
60b27d3f71SKristof Provost        ToolsHelper.print_output("/sbin/sysctl net.inet.ip.forwarding=1")
61b27d3f71SKristof Provost        ToolsHelper.print_output("/sbin/pfctl -x loud")
62b27d3f71SKristof Provost
63fffedd81SKristof Provost        ToolsHelper.print_output("/sbin/ifconfig %s mtu 1492" % if2name)
64fffedd81SKristof Provost
65b27d3f71SKristof Provost    def vnet3_handler(self, vnet):
66b27d3f71SKristof Provost        # Import in the correct vnet, so at to not confuse Scapy
67b27d3f71SKristof Provost        import scapy.all as sp
68b27d3f71SKristof Provost
69b27d3f71SKristof Provost        ifname = vnet.iface_alias_map["if2"].name
70b27d3f71SKristof Provost        ToolsHelper.print_output("/sbin/route add default 198.51.100.1")
71b27d3f71SKristof Provost        ToolsHelper.print_output("/sbin/ifconfig %s inet alias 198.51.100.3/24" % ifname)
72fffedd81SKristof Provost        ToolsHelper.print_output("/sbin/ifconfig %s mtu 1492" % ifname)
73b27d3f71SKristof Provost
74b27d3f71SKristof Provost        def checkfn(packet):
75b27d3f71SKristof Provost            icmp = packet.getlayer(sp.ICMP)
76b27d3f71SKristof Provost            if not icmp:
77b27d3f71SKristof Provost                return False
78b27d3f71SKristof Provost
79b27d3f71SKristof Provost            if icmp.type != 3:
80b27d3f71SKristof Provost                return False
81b27d3f71SKristof Provost
82b27d3f71SKristof Provost            packet.show()
83b27d3f71SKristof Provost            return True
84b27d3f71SKristof Provost
85b27d3f71SKristof Provost        sp.sniff(iface=ifname, stop_filter=checkfn)
86b27d3f71SKristof Provost        vnet.pipe.send("Got ICMP destination unreachable packet")
87b27d3f71SKristof Provost
88b27d3f71SKristof Provost    @pytest.mark.require_user("root")
89*65cc5af1SJose Luis Duran    @pytest.mark.require_progs(["scapy"])
90b27d3f71SKristof Provost    def test_inner_match(self):
91b27d3f71SKristof Provost        vnet = self.vnet_map["vnet1"]
92b27d3f71SKristof Provost        dst_vnet = self.vnet_map["vnet3"]
93b27d3f71SKristof Provost        sendif = vnet.iface_alias_map["if1"].name
94b27d3f71SKristof Provost
95b27d3f71SKristof Provost        our_mac = ToolsHelper.get_output("/sbin/ifconfig %s ether | awk '/ether/ { print $2; }'" % sendif)
96b27d3f71SKristof Provost        dst_mac = re.sub("0a$", "0b", our_mac)
97b27d3f71SKristof Provost
98b27d3f71SKristof Provost        # Import in the correct vnet, so at to not confuse Scapy
99b27d3f71SKristof Provost        import scapy.all as sp
100b27d3f71SKristof Provost
101b27d3f71SKristof Provost        ToolsHelper.print_output("/sbin/route add default 192.0.2.1")
102b27d3f71SKristof Provost
103b27d3f71SKristof Provost        # Sanity check
104b27d3f71SKristof Provost        check("/sbin/ping -c 1 192.0.2.1")
105b27d3f71SKristof Provost        check("/sbin/ping -c 1 198.51.100.1")
106b27d3f71SKristof Provost        check("/sbin/ping -c 2 198.51.100.3")
107b27d3f71SKristof Provost
108b27d3f71SKristof Provost        # Establish a state
109b27d3f71SKristof Provost        pkt = sp.Ether(src=our_mac, dst=dst_mac) \
110b27d3f71SKristof Provost            / sp.IP(src="192.0.2.2", dst="198.51.100.2") \
111b27d3f71SKristof Provost            / sp.ICMP(type='echo-request') \
112b27d3f71SKristof Provost            / "PAYLOAD"
113b27d3f71SKristof Provost        sp.sendp(pkt, sendif, verbose=False)
114b27d3f71SKristof Provost
115b27d3f71SKristof Provost        # Now try to pass an ICMP error message piggy-backing on that state, but
116b27d3f71SKristof Provost        # use a different source address
117b27d3f71SKristof Provost        pkt = sp.Ether(src=our_mac, dst=dst_mac) \
118b27d3f71SKristof Provost            / sp.IP(src="192.0.2.2", dst="198.51.100.3") \
119b27d3f71SKristof Provost            / sp.ICMP(type='dest-unreach') \
120b27d3f71SKristof Provost            / sp.IP(src="198.51.100.2", dst="192.0.2.2") \
121b27d3f71SKristof Provost            / sp.ICMP(type='echo-reply')
122b27d3f71SKristof Provost        sp.sendp(pkt, sendif, verbose=False)
123b27d3f71SKristof Provost
124b27d3f71SKristof Provost        try:
125b27d3f71SKristof Provost            rcvd = self.wait_object(dst_vnet.pipe, timeout=1)
126b27d3f71SKristof Provost            if rcvd:
127b27d3f71SKristof Provost                raise Exception(rcvd)
128b27d3f71SKristof Provost        except TimeoutError as e:
129b27d3f71SKristof Provost            # We expect the timeout here. It means we didn't get the destination
130b27d3f71SKristof Provost            # unreachable packet in vnet3
131b27d3f71SKristof Provost            pass
132fffedd81SKristof Provost
133fffedd81SKristof Provost    def check_icmp_echo(self, sp, payload_size):
134fffedd81SKristof Provost        packet = sp.IP(dst="198.51.100.2", flags="DF") \
135fffedd81SKristof Provost            / sp.ICMP(type='echo-request') \
136fffedd81SKristof Provost            / sp.raw(bytes.fromhex('f0') * payload_size)
137fffedd81SKristof Provost
138fffedd81SKristof Provost        p = sp.sr1(packet, iface=self.vnet.iface_alias_map["if1"].name,
139fffedd81SKristof Provost            timeout=3)
140fffedd81SKristof Provost        p.show()
141fffedd81SKristof Provost
142fffedd81SKristof Provost        ip = p.getlayer(sp.IP)
143fffedd81SKristof Provost        icmp = p.getlayer(sp.ICMP)
144fffedd81SKristof Provost        assert ip
145fffedd81SKristof Provost        assert icmp
146fffedd81SKristof Provost
147fffedd81SKristof Provost        if payload_size > 1464:
148fffedd81SKristof Provost            # Expect ICMP destination unreachable, fragmentation needed
149fffedd81SKristof Provost            assert ip.src == "192.0.2.1"
150fffedd81SKristof Provost            assert ip.dst == "192.0.2.2"
151fffedd81SKristof Provost            assert icmp.type == 3 # dest-unreach
152fffedd81SKristof Provost            assert icmp.code == 4
153fffedd81SKristof Provost            assert icmp.nexthopmtu == 1492
154fffedd81SKristof Provost        else:
155fffedd81SKristof Provost            # Expect echo reply
156fffedd81SKristof Provost            assert ip.src == "198.51.100.2"
157fffedd81SKristof Provost            assert ip.dst == "192.0.2.2"
158fffedd81SKristof Provost            assert icmp.type == 0 # "echo-reply"
159fffedd81SKristof Provost            assert icmp.code == 0
160fffedd81SKristof Provost
161fffedd81SKristof Provost        return
162fffedd81SKristof Provost
163fffedd81SKristof Provost    @pytest.mark.require_user("root")
164*65cc5af1SJose Luis Duran    @pytest.mark.require_progs(["scapy"])
165fffedd81SKristof Provost    def test_fragmentation_needed(self):
166fffedd81SKristof Provost        ToolsHelper.print_output("/sbin/route add default 192.0.2.1")
167fffedd81SKristof Provost
168fffedd81SKristof Provost        ToolsHelper.print_output("/sbin/ping -c 1 198.51.100.2")
169fffedd81SKristof Provost        ToolsHelper.print_output("/sbin/ping -c 1 -D -s 1472 198.51.100.2")
170fffedd81SKristof Provost
171fffedd81SKristof Provost        # Import in the correct vnet, so at to not confuse Scapy
172fffedd81SKristof Provost        import scapy.all as sp
173fffedd81SKristof Provost
174fffedd81SKristof Provost        self.check_icmp_echo(sp, 128)
175fffedd81SKristof Provost        self.check_icmp_echo(sp, 1464)
176fffedd81SKristof Provost        self.check_icmp_echo(sp, 1468)
177