xref: /freebsd/tests/sys/netpfil/pf/tcp.py (revision 0440b3d2cbf83afb55209a9938b31a48912f222c)
1#
2# SPDX-License-Identifier: BSD-2-Clause
3#
4# Copyright (c) 2025 Rubicon Communications, LLC (Netgate)
5#
6# Redistribution and use in source and binary forms, with or without
7# modification, are permitted provided that the following conditions
8# are met:
9# 1. Redistributions of source code must retain the above copyright
10#    notice, this list of conditions and the following disclaimer.
11# 2. Redistributions in binary form must reproduce the above copyright
12#    notice, this list of conditions and the following disclaimer in the
13#    documentation and/or other materials provided with the distribution.
14#
15# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
16# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
19# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
21# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
23# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
24# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
25# SUCH DAMAGE.
26
27import sys
28import pytest
29import random
30import socket
31import selectors
32import threading
33import time
34from atf_python.sys.net.tools import ToolsHelper
35from atf_python.sys.net.vnet import VnetTestTemplate
36
37class DelayedSend(threading.Thread):
38    def __init__(self, packet):
39        threading.Thread.__init__(self)
40        self._packet = packet
41
42        self.start()
43
44    def run(self):
45        import scapy.all as sp
46        time.sleep(1)
47        sp.send(self._packet)
48
49class TCPClient:
50    def __init__(self, src, dst, sport, dport, sp):
51        self.src = src
52        self.dst = dst
53        self.sport = sport
54        self.dport = dport
55        self.sp = sp
56        self.seq = random.randrange(1, (2**32)-1)
57        self.ack = 0
58
59    def syn(self):
60        syn = self.sp.IP(src=self.src, dst=self.dst) \
61            / self.sp.TCP(sport=self.sport, dport=self.dport, flags="S", seq=self.seq)
62        return syn
63
64    def connect(self):
65        syn = self.syn()
66        r = self.sp.sr1(syn, timeout=5)
67
68        assert r
69        t = r.getlayer(self.sp.TCP)
70        assert t
71        assert t.sport == self.dport
72        assert t.dport == self.sport
73        assert t.flags == "SA"
74
75        self.seq += 1
76        self.ack = t.seq + 1
77        ack = self.sp.IP(src=self.src, dst=self.dst) \
78            / self.sp.TCP(sport=self.sport, dport=self.dport, flags="A", ack=self.ack, seq=self.seq)
79        self.sp.send(ack)
80
81    def send(self, data):
82        length = len(data)
83        pkt = self.sp.IP(src=self.src, dst=self.dst) \
84            / self.sp.TCP(sport=self.sport, dport=self.dport, ack=self.ack, seq=self.seq, flags="") \
85            / self.sp.Raw(data)
86        self.seq += length
87        pkt.show()
88        self.sp.send(pkt)
89
90class TestTcp(VnetTestTemplate):
91    REQUIRED_MODULES = [ "pf" ]
92    TOPOLOGY = {
93        "vnet1": {"ifaces": ["if1"]},
94        "vnet2": {"ifaces": ["if1"]},
95        "if1": {"prefixes4": [("192.0.2.1/24", "192.0.2.2/24")]},
96    }
97
98    def vnet2_handler(self, vnet):
99        ToolsHelper.print_output("/usr/sbin/arp -s 192.0.2.3 00:01:02:03:04:05")
100        ToolsHelper.print_output("/sbin/pfctl -e")
101        ToolsHelper.pf_rules([
102            "pass"
103        ])
104        ToolsHelper.print_output("/sbin/pfctl -x loud")
105
106        # Start TCP listener
107        sel = selectors.DefaultSelector()
108        t = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
109        t.bind(("0.0.0.0", 1234))
110        t.listen(100)
111        t.setblocking(False)
112        sel.register(t, selectors.EVENT_READ, data=None)
113
114        while True:
115            events = sel.select(timeout=2)
116            for key, mask in events:
117                sock = key.fileobj
118                if key.data is None:
119                    conn, addr = sock.accept()
120                    print(f"Accepted connection from {addr}")
121                    events = selectors.EVENT_READ | selectors.EVENT_WRITE
122                    sel.register(conn, events, data="TCP")
123                else:
124                    if mask & selectors.EVENT_READ:
125                        recv_data = sock.recv(1024)
126                        print(f"Received TCP {recv_data}")
127                        ToolsHelper.print_output("/sbin/pfctl -ss -vv")
128                        sock.send(recv_data)
129
130    @pytest.mark.require_user("root")
131    @pytest.mark.require_progs(["scapy"])
132    def test_challenge_ack(self):
133        vnet = self.vnet_map["vnet1"]
134        ifname = vnet.iface_alias_map["if1"].name
135
136        # Import in the correct vnet, so at to not confuse Scapy
137        import scapy.all as sp
138
139        #time.sleep(30)
140
141        a = TCPClient("192.0.2.3", "192.0.2.2", 1234, 1234, sp)
142        a.connect()
143        a.send(b"foo")
144
145        b = TCPClient("192.0.2.3", "192.0.2.2", 1234, 1234, sp)
146        syn = b.syn()
147        syn.show()
148        s = DelayedSend(syn)
149        packets = sp.sniff(iface=ifname, timeout=3)
150        found = False
151        for p in packets:
152            ip = p.getlayer(sp.IP)
153            if not ip:
154                continue
155            tcp = p.getlayer(sp.TCP)
156            if not tcp:
157                continue
158
159            if ip.src != "192.0.2.2":
160                continue
161
162            p.show()
163
164            assert ip.dst == "192.0.2.3"
165            assert tcp.sport == 1234
166            assert tcp.dport == 1234
167            assert tcp.flags == "A"
168
169            # We only expect one
170            assert not found
171            found = True
172
173        assert found
174