git: 681a04dbda0e - main - pf tests: verify we accept exactly one hop-by-hop header

From: Kristof Provost <kp_at_FreeBSD.org>
Date: Thu, 08 May 2025 13:10:41 UTC
The branch main has been updated by kp:

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

commit 681a04dbda0e06cccf2ecc1ab8cac1e9e2be78d2
Author:     Kristof Provost <kp@FreeBSD.org>
AuthorDate: 2025-05-06 10:07:23 +0000
Commit:     Kristof Provost <kp@FreeBSD.org>
CommitDate: 2025-05-08 13:10:25 +0000

    pf tests: verify we accept exactly one hop-by-hop header
    
    Sponsored by:   Rubicon Communications, LLC ("Netgate")
---
 tests/sys/netpfil/pf/frag6.py | 90 +++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 90 insertions(+)

diff --git a/tests/sys/netpfil/pf/frag6.py b/tests/sys/netpfil/pf/frag6.py
index c9a71f73c0cf..de14fec528fd 100644
--- a/tests/sys/netpfil/pf/frag6.py
+++ b/tests/sys/netpfil/pf/frag6.py
@@ -97,6 +97,96 @@ class TestFrag6(VnetTestTemplate):
 
         sp.send(pkts, inter = 0.1)
 
+class TestFrag6HopHyHop(VnetTestTemplate):
+    REQUIRED_MODULES = ["pf"]
+    TOPOLOGY = {
+        "vnet1": {"ifaces": ["if1", "if2"]},
+        "vnet2": {"ifaces": ["if1", "if2"]},
+        "if1": {"prefixes6": [("2001:db8::1/64", "2001:db8::2/64")]},
+        "if2": {"prefixes6": [("2001:db8:666::1/64", "2001:db8:1::2/64")]},
+    }
+
+    def vnet2_handler(self, vnet):
+        ifname = vnet.iface_alias_map["if1"].name
+        ToolsHelper.print_output("/sbin/sysctl net.inet6.ip6.forwarding=1")
+        ToolsHelper.print_output("/usr/sbin/ndp -s 2001:db8:1::1 00:01:02:03:04:05")
+        ToolsHelper.print_output("/sbin/pfctl -e")
+        ToolsHelper.print_output("/sbin/pfctl -x loud")
+        ToolsHelper.pf_rules([
+            "scrub fragment reassemble min-ttl 10",
+            "pass",
+        ])
+
+    @pytest.mark.require_user("root")
+    @pytest.mark.require_progs(["scapy"])
+    def test_hop_by_hop(self):
+        "Verify that we reject non-first hop-by-hop headers"
+        if1 = self.vnet.iface_alias_map["if1"].name
+        if2 = self.vnet.iface_alias_map["if2"].name
+        ToolsHelper.print_output("/sbin/route add -6 default 2001:db8::2")
+        ToolsHelper.print_output("/sbin/ping6 -c 1 2001:db8:1::2")
+
+        # Import in the correct vnet, so at to not confuse Scapy
+        import scapy.all as sp
+
+        # A hop-by-hop header is accepted if it's the first header
+        pkt = sp.IPv6(src="2001:db8::1", dst="2001:db8:1::1") \
+            / sp.IPv6ExtHdrHopByHop() \
+            / sp.ICMPv6EchoRequest(data=sp.raw(bytes.fromhex('f0') * 30))
+        pkt.show()
+
+        # Delay the send so the sniffer is running when we transmit.
+        s = DelayedSend(pkt)
+
+        replies = sp.sniff(iface=if2, timeout=3)
+        found = False
+        for p in replies:
+            p.show()
+            ip6 = p.getlayer(sp.IPv6)
+            hbh = p.getlayer(sp.IPv6ExtHdrHopByHop)
+            icmp6 = p.getlayer(sp.ICMPv6EchoRequest)
+
+            if not ip6 or not icmp6:
+                continue
+            assert ip6.src == "2001:db8::1"
+            assert ip6.dst == "2001:db8:1::1"
+            assert hbh
+            assert icmp6
+            found = True
+        assert found
+
+        # A hop-by-hop header causes the packet to be dropped if it's not the
+        # first extension header
+        pkt = sp.IPv6(src="2001:db8::1", dst="2001:db8:1::1") \
+            / sp.IPv6ExtHdrFragment(offset=0, m=0) \
+            / sp.IPv6ExtHdrHopByHop() \
+            / sp.ICMPv6EchoRequest(data=sp.raw(bytes.fromhex('f0') * 30))
+        pkt2 = sp.IPv6(src="2001:db8::1", dst="2001:db8:1::1") \
+            / sp.ICMPv6EchoRequest(data=sp.raw(bytes.fromhex('f0') * 30))
+
+        # Delay the send so the sniffer is running when we transmit.
+        ToolsHelper.print_output("/sbin/ping6 -c 1 2001:db8:1::2")
+
+        s = DelayedSend([ pkt2, pkt ])
+        replies = sp.sniff(iface=if2, timeout=10)
+        found = False
+        for p in replies:
+            # Expect to find the packet without the hop-by-hop header, not the
+            # one with
+            p.show()
+            ip6 = p.getlayer(sp.IPv6)
+            hbh = p.getlayer(sp.IPv6ExtHdrHopByHop)
+            icmp6 = p.getlayer(sp.ICMPv6EchoRequest)
+
+            if not ip6 or not icmp6:
+                continue
+            assert ip6.src == "2001:db8::1"
+            assert ip6.dst == "2001:db8:1::1"
+            assert not hbh
+            assert icmp6
+            found = True
+        assert found
+
 class TestFrag6_Overlap(VnetTestTemplate):
     REQUIRED_MODULES = ["pf"]
     TOPOLOGY = {