git: d7c9de2d68ca - main - pf tests: Add option to send fragmented packets

From: Kristof Provost <kp_at_FreeBSD.org>
Date: Thu, 26 Oct 2023 14:16:53 UTC
The branch main has been updated by kp:

URL: https://cgit.FreeBSD.org/src/commit/?id=d7c9de2d68ca81c557e069c2b431529cf597886c

commit d7c9de2d68ca81c557e069c2b431529cf597886c
Author:     Kajetan Staszkiewicz <vegeta@tuxpowered.net>
AuthorDate: 2023-10-26 09:14:14 +0000
Commit:     Kristof Provost <kp@FreeBSD.org>
CommitDate: 2023-10-26 13:25:20 +0000

    pf tests: Add option to send fragmented packets
    
    Add option to send fragmented packets and to properly sniff them by
    reassembling them by the sniffer itself.
    
    Reviewed by:    kp
    Sponsored by:   InnoGames GmbH
    Differential Revision:  https://reviews.freebsd.org/D42354
---
 tests/sys/netpfil/common/pft_ping.py | 39 +++++++++++++++++++++++++++---------
 tests/sys/netpfil/common/sniffer.py  | 18 +++++++++++++----
 2 files changed, 44 insertions(+), 13 deletions(-)

diff --git a/tests/sys/netpfil/common/pft_ping.py b/tests/sys/netpfil/common/pft_ping.py
index af5b84e34c67..1abf4f609832 100644
--- a/tests/sys/netpfil/common/pft_ping.py
+++ b/tests/sys/netpfil/common/pft_ping.py
@@ -82,17 +82,29 @@ def prepare_ipv4(dst_address, send_params):
 
 def send_icmp_ping(dst_address, sendif, send_params):
     send_length = send_params['length']
+    send_frag_length = send_params['frag_length']
+    packets = []
     ether = sp.Ether()
     if ':' in dst_address:
         ip6 = prepare_ipv6(dst_address, send_params)
         icmp = sp.ICMPv6EchoRequest(data=sp.raw(build_payload(send_length)))
-        req = ether / ip6 / icmp
+        if send_frag_length:
+            for packet in sp.fragment(ip6 / icmp, fragsize=send_frag_length):
+                packets.append(ether / packet)
+        else:
+            packets.append(ether / ip6 / icmp)
+
     else:
         ip = prepare_ipv4(dst_address, send_params)
         icmp = sp.ICMP(type='echo-request')
         raw = sp.raw(build_payload(send_length))
-        req = ether / ip / icmp / raw
-    sp.sendp(req, sendif, verbose=False)
+        if send_frag_length:
+            for packet in sp.fragment(ip / icmp / raw, fragsize=send_frag_length):
+                packets.append(ether / packet)
+        else:
+            packets.append(ether / ip / icmp / raw)
+    for packet in packets:
+        sp.sendp(packet, sendif, verbose=False)
 
 
 def send_tcp_syn(dst_address, sendif, send_params):
@@ -372,7 +384,7 @@ def check_tcp_syn_reply(expect_params, packet):
         return check_tcp_syn_reply_4(expect_params, packet)
 
 
-def setup_sniffer(recvif, ping_type, sniff_type, expect_params):
+def setup_sniffer(recvif, ping_type, sniff_type, expect_params, defrag):
     if ping_type == 'icmp' and sniff_type == 'request':
         checkfn = check_ping_request
     elif ping_type == 'icmp' and sniff_type == 'reply':
@@ -384,7 +396,7 @@ def setup_sniffer(recvif, ping_type, sniff_type, expect_params):
     else:
         raise Exception('Unspported ping or sniff type')
 
-    return Sniffer(expect_params, checkfn, recvif)
+    return Sniffer(expect_params, checkfn, recvif, defrag=defrag)
 
 
 def parse_args():
@@ -417,6 +429,8 @@ def parse_args():
     parser_send = parser.add_argument_group('Values set in transmitted packets')
     parser_send.add_argument('--send-flags', nargs=1, type=str,
         help='IPv4 fragmentation flags')
+    parser_send.add_argument('--send-frag-length', nargs=1, type=int,
+         help='Force IP fragmentation with given fragment length')
     parser_send.add_argument('--send-hlim', nargs=1, type=int,
         help='IPv6 Hop Limit or IPv4 Time To Live')
     parser_send.add_argument('--send-mss', nargs=1, type=int,
@@ -428,7 +442,7 @@ def parse_args():
     parser_send.add_argument('--send-tc', nargs=1, type=int,
         help='IPv6 Traffic Class or IPv4 DiffServ / ToS')
     parser_send.add_argument('--send-tcpopt-unaligned', action='store_true',
-            help='Include unaligned TCP options')
+         help='Include unaligned TCP options')
 
     # Expectations
     parser_expect = parser.add_argument_group('Values expected in sniffed packets')
@@ -467,7 +481,7 @@ def main():
     # Standardize parameters which have nargs=1.
     send_params = {}
     expect_params = {}
-    for param_name in ('flags', 'hlim', 'length', 'mss', 'seq', 'tc'):
+    for param_name in ('flags', 'hlim', 'length', 'mss', 'seq', 'tc', 'frag_length'):
         param_arg = vars(args).get(f'send_{param_name}')
         send_params[param_name] = param_arg[0] if param_arg else None
         param_arg = vars(args).get(f'expect_{param_name}')
@@ -488,6 +502,11 @@ def main():
 
     sniffers = []
 
+    if send_params['frag_length']:
+        defrag = True
+    else:
+        defrag = False
+
     if recv_ifs:
         sniffer_params = copy(expect_params)
         sniffer_params['src_address'] = None
@@ -495,7 +514,8 @@ def main():
         for iface in recv_ifs:
             LOGGER.debug(f'Installing receive sniffer on {iface}')
             sniffers.append(
-                setup_sniffer(iface, args.ping_type, 'request', sniffer_params,
+                setup_sniffer(iface, args.ping_type, 'request',
+                              sniffer_params, defrag,
             ))
 
     if reply_ifs:
@@ -505,7 +525,8 @@ def main():
         for iface in reply_ifs:
             LOGGER.debug(f'Installing reply sniffer on {iface}')
             sniffers.append(
-                setup_sniffer(iface, args.ping_type, 'reply', sniffer_params,
+                setup_sniffer(iface, args.ping_type, 'reply',
+                              sniffer_params, defrag,
             ))
 
     LOGGER.debug(f'Installed {len(sniffers)} sniffers')
diff --git a/tests/sys/netpfil/common/sniffer.py b/tests/sys/netpfil/common/sniffer.py
index ab3ddc0aea3c..14305a37278c 100644
--- a/tests/sys/netpfil/common/sniffer.py
+++ b/tests/sys/netpfil/common/sniffer.py
@@ -30,7 +30,7 @@ import scapy.all as sp
 import sys
 
 class Sniffer(threading.Thread):
-	def __init__(self, args, check_function, recvif, timeout=3):
+	def __init__(self, args, check_function, recvif, timeout=3, defrag=False):
 		threading.Thread.__init__(self)
 
 		self._sem = threading.Semaphore(0)
@@ -38,6 +38,7 @@ class Sniffer(threading.Thread):
 		self._timeout = timeout
 		self._recvif = recvif
 		self._check_function = check_function
+		self._defrag = defrag
 		self.correctPackets = 0
 
 		self.start()
@@ -55,6 +56,15 @@ class Sniffer(threading.Thread):
 
 	def run(self):
 		self.packets = []
-		self.packets = sp.sniff(iface=self._recvif,
-			stop_filter=self._checkPacket, timeout=self._timeout,
-			started_callback=self._startedCb)
+		if self._defrag:
+			# With fragment reassembly we can't stop the sniffer after catching
+			# the good packets, as those have not been reassembled. We must
+			#  wait for sniffer to finish and check returned packets instead.
+			self.packets = sp.sniff(session=sp.IPSession, iface=self._recvif,
+				timeout=self._timeout, started_callback=self._startedCb)
+			for p in self.packets:
+				self._checkPacket(p)
+		else:
+			self.packets = sp.sniff(iface=self._recvif,
+				stop_filter=self._checkPacket, timeout=self._timeout,
+				started_callback=self._startedCb)