xref: /freebsd/tests/sys/netpfil/pf/tcp.py (revision 63cc817adde5aec0f6446e44c1eee0a7d5908dfd)
137b6e0d8SKristof Provost#
237b6e0d8SKristof Provost# SPDX-License-Identifier: BSD-2-Clause
337b6e0d8SKristof Provost#
437b6e0d8SKristof Provost# Copyright (c) 2025 Rubicon Communications, LLC (Netgate)
537b6e0d8SKristof Provost#
637b6e0d8SKristof Provost# Redistribution and use in source and binary forms, with or without
737b6e0d8SKristof Provost# modification, are permitted provided that the following conditions
837b6e0d8SKristof Provost# are met:
937b6e0d8SKristof Provost# 1. Redistributions of source code must retain the above copyright
1037b6e0d8SKristof Provost#    notice, this list of conditions and the following disclaimer.
1137b6e0d8SKristof Provost# 2. Redistributions in binary form must reproduce the above copyright
1237b6e0d8SKristof Provost#    notice, this list of conditions and the following disclaimer in the
1337b6e0d8SKristof Provost#    documentation and/or other materials provided with the distribution.
1437b6e0d8SKristof Provost#
1537b6e0d8SKristof Provost# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
1637b6e0d8SKristof Provost# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
1737b6e0d8SKristof Provost# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
1837b6e0d8SKristof Provost# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
1937b6e0d8SKristof Provost# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
2037b6e0d8SKristof Provost# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
2137b6e0d8SKristof Provost# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
2237b6e0d8SKristof Provost# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
2337b6e0d8SKristof Provost# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
2437b6e0d8SKristof Provost# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
2537b6e0d8SKristof Provost# SUCH DAMAGE.
2637b6e0d8SKristof Provost
2737b6e0d8SKristof Provostimport sys
2837b6e0d8SKristof Provostimport pytest
2937b6e0d8SKristof Provostimport random
3037b6e0d8SKristof Provostimport socket
3137b6e0d8SKristof Provostimport selectors
32*63cc817aSKristof Provostfrom utils import DelayedSend
3337b6e0d8SKristof Provostfrom atf_python.sys.net.tools import ToolsHelper
3437b6e0d8SKristof Provostfrom atf_python.sys.net.vnet import VnetTestTemplate
3537b6e0d8SKristof Provost
3637b6e0d8SKristof Provostclass TCPClient:
3737b6e0d8SKristof Provost    def __init__(self, src, dst, sport, dport, sp):
3837b6e0d8SKristof Provost        self.src = src
3937b6e0d8SKristof Provost        self.dst = dst
4037b6e0d8SKristof Provost        self.sport = sport
4137b6e0d8SKristof Provost        self.dport = dport
4237b6e0d8SKristof Provost        self.sp = sp
4337b6e0d8SKristof Provost        self.seq = random.randrange(1, (2**32)-1)
4437b6e0d8SKristof Provost        self.ack = 0
4537b6e0d8SKristof Provost
4637b6e0d8SKristof Provost    def syn(self):
4737b6e0d8SKristof Provost        syn = self.sp.IP(src=self.src, dst=self.dst) \
4837b6e0d8SKristof Provost            / self.sp.TCP(sport=self.sport, dport=self.dport, flags="S", seq=self.seq)
4937b6e0d8SKristof Provost        return syn
5037b6e0d8SKristof Provost
5137b6e0d8SKristof Provost    def connect(self):
5237b6e0d8SKristof Provost        syn = self.syn()
5337b6e0d8SKristof Provost        r = self.sp.sr1(syn, timeout=5)
5437b6e0d8SKristof Provost
5537b6e0d8SKristof Provost        assert r
5637b6e0d8SKristof Provost        t = r.getlayer(self.sp.TCP)
5737b6e0d8SKristof Provost        assert t
5837b6e0d8SKristof Provost        assert t.sport == self.dport
5937b6e0d8SKristof Provost        assert t.dport == self.sport
6037b6e0d8SKristof Provost        assert t.flags == "SA"
6137b6e0d8SKristof Provost
6237b6e0d8SKristof Provost        self.seq += 1
6337b6e0d8SKristof Provost        self.ack = t.seq + 1
6437b6e0d8SKristof Provost        ack = self.sp.IP(src=self.src, dst=self.dst) \
6537b6e0d8SKristof Provost            / self.sp.TCP(sport=self.sport, dport=self.dport, flags="A", ack=self.ack, seq=self.seq)
6637b6e0d8SKristof Provost        self.sp.send(ack)
6737b6e0d8SKristof Provost
6837b6e0d8SKristof Provost    def send(self, data):
6937b6e0d8SKristof Provost        length = len(data)
7037b6e0d8SKristof Provost        pkt = self.sp.IP(src=self.src, dst=self.dst) \
7137b6e0d8SKristof Provost            / self.sp.TCP(sport=self.sport, dport=self.dport, ack=self.ack, seq=self.seq, flags="") \
7237b6e0d8SKristof Provost            / self.sp.Raw(data)
7337b6e0d8SKristof Provost        self.seq += length
7437b6e0d8SKristof Provost        pkt.show()
7537b6e0d8SKristof Provost        self.sp.send(pkt)
7637b6e0d8SKristof Provost
7737b6e0d8SKristof Provostclass TestTcp(VnetTestTemplate):
7837b6e0d8SKristof Provost    REQUIRED_MODULES = [ "pf" ]
7937b6e0d8SKristof Provost    TOPOLOGY = {
8037b6e0d8SKristof Provost        "vnet1": {"ifaces": ["if1"]},
8137b6e0d8SKristof Provost        "vnet2": {"ifaces": ["if1"]},
8237b6e0d8SKristof Provost        "if1": {"prefixes4": [("192.0.2.1/24", "192.0.2.2/24")]},
8337b6e0d8SKristof Provost    }
8437b6e0d8SKristof Provost
8537b6e0d8SKristof Provost    def vnet2_handler(self, vnet):
8637b6e0d8SKristof Provost        ToolsHelper.print_output("/usr/sbin/arp -s 192.0.2.3 00:01:02:03:04:05")
8737b6e0d8SKristof Provost        ToolsHelper.print_output("/sbin/pfctl -e")
8837b6e0d8SKristof Provost        ToolsHelper.pf_rules([
8937b6e0d8SKristof Provost            "pass"
9037b6e0d8SKristof Provost        ])
9137b6e0d8SKristof Provost        ToolsHelper.print_output("/sbin/pfctl -x loud")
9237b6e0d8SKristof Provost
9337b6e0d8SKristof Provost        # Start TCP listener
9437b6e0d8SKristof Provost        sel = selectors.DefaultSelector()
9537b6e0d8SKristof Provost        t = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
9637b6e0d8SKristof Provost        t.bind(("0.0.0.0", 1234))
9737b6e0d8SKristof Provost        t.listen(100)
9837b6e0d8SKristof Provost        t.setblocking(False)
9937b6e0d8SKristof Provost        sel.register(t, selectors.EVENT_READ, data=None)
10037b6e0d8SKristof Provost
10137b6e0d8SKristof Provost        while True:
10237b6e0d8SKristof Provost            events = sel.select(timeout=2)
10337b6e0d8SKristof Provost            for key, mask in events:
10437b6e0d8SKristof Provost                sock = key.fileobj
10537b6e0d8SKristof Provost                if key.data is None:
10637b6e0d8SKristof Provost                    conn, addr = sock.accept()
10737b6e0d8SKristof Provost                    print(f"Accepted connection from {addr}")
10837b6e0d8SKristof Provost                    events = selectors.EVENT_READ | selectors.EVENT_WRITE
10937b6e0d8SKristof Provost                    sel.register(conn, events, data="TCP")
11037b6e0d8SKristof Provost                else:
11137b6e0d8SKristof Provost                    if mask & selectors.EVENT_READ:
11237b6e0d8SKristof Provost                        recv_data = sock.recv(1024)
11337b6e0d8SKristof Provost                        print(f"Received TCP {recv_data}")
11437b6e0d8SKristof Provost                        ToolsHelper.print_output("/sbin/pfctl -ss -vv")
11537b6e0d8SKristof Provost                        sock.send(recv_data)
11637b6e0d8SKristof Provost
11737b6e0d8SKristof Provost    @pytest.mark.require_user("root")
11837b6e0d8SKristof Provost    @pytest.mark.require_progs(["scapy"])
11937b6e0d8SKristof Provost    def test_challenge_ack(self):
12037b6e0d8SKristof Provost        vnet = self.vnet_map["vnet1"]
12137b6e0d8SKristof Provost        ifname = vnet.iface_alias_map["if1"].name
12237b6e0d8SKristof Provost
12337b6e0d8SKristof Provost        # Import in the correct vnet, so at to not confuse Scapy
12437b6e0d8SKristof Provost        import scapy.all as sp
12537b6e0d8SKristof Provost
12637b6e0d8SKristof Provost        a = TCPClient("192.0.2.3", "192.0.2.2", 1234, 1234, sp)
12737b6e0d8SKristof Provost        a.connect()
12837b6e0d8SKristof Provost        a.send(b"foo")
12937b6e0d8SKristof Provost
13037b6e0d8SKristof Provost        b = TCPClient("192.0.2.3", "192.0.2.2", 1234, 1234, sp)
13137b6e0d8SKristof Provost        syn = b.syn()
13237b6e0d8SKristof Provost        syn.show()
13337b6e0d8SKristof Provost        s = DelayedSend(syn)
13437b6e0d8SKristof Provost        packets = sp.sniff(iface=ifname, timeout=3)
13537b6e0d8SKristof Provost        found = False
13637b6e0d8SKristof Provost        for p in packets:
13737b6e0d8SKristof Provost            ip = p.getlayer(sp.IP)
13837b6e0d8SKristof Provost            if not ip:
13937b6e0d8SKristof Provost                continue
14037b6e0d8SKristof Provost            tcp = p.getlayer(sp.TCP)
14137b6e0d8SKristof Provost            if not tcp:
14237b6e0d8SKristof Provost                continue
14337b6e0d8SKristof Provost
14437b6e0d8SKristof Provost            if ip.src != "192.0.2.2":
14537b6e0d8SKristof Provost                continue
14637b6e0d8SKristof Provost
14737b6e0d8SKristof Provost            p.show()
14837b6e0d8SKristof Provost
14937b6e0d8SKristof Provost            assert ip.dst == "192.0.2.3"
15037b6e0d8SKristof Provost            assert tcp.sport == 1234
15137b6e0d8SKristof Provost            assert tcp.dport == 1234
15237b6e0d8SKristof Provost            assert tcp.flags == "A"
15337b6e0d8SKristof Provost
15437b6e0d8SKristof Provost            # We only expect one
15537b6e0d8SKristof Provost            assert not found
15637b6e0d8SKristof Provost            found = True
15737b6e0d8SKristof Provost
15837b6e0d8SKristof Provost        assert found
159