Index: tests/sys/netpfil/pf/Makefile =================================================================== --- tests/sys/netpfil/pf/Makefile +++ tests/sys/netpfil/pf/Makefile @@ -40,6 +40,7 @@ frag-overindex.py \ frag-overlimit.py \ frag-overreplace.py \ + pfsync_defer.py \ utils.subr ${PACKAGE}FILESMODE_CVE-2019-5597.py= 0555 @@ -48,5 +49,6 @@ ${PACKAGE}FILESMODE_frag-overindex.py= 0555 ${PACKAGE}FILESMODE_frag-overlimit.py= 0555 ${PACKAGE}FILESMODE_frag-overreplace.py= 0555 +${PACKAGE}FILESMODE_pfsync_defer.py= 0555 .include Index: tests/sys/netpfil/pf/pfsync.sh =================================================================== --- tests/sys/netpfil/pf/pfsync.sh +++ tests/sys/netpfil/pf/pfsync.sh @@ -27,6 +27,8 @@ . $(atf_get_srcdir)/utils.subr +common_dir=$(atf_get_srcdir)/../common + atf_test_case "basic" "cleanup" basic_head() { @@ -95,6 +97,23 @@ pfsynct_cleanup } +atf_test_case "basic_defer" "cleanup" +basic_defer_head() +{ + atf_set descr 'Basic defer mode pfsync test' + atf_set require.user root +} + +basic_defer_body() +{ + common_body defer +} + +basic_defer_cleanup() +{ + pfsynct_cleanup +} + atf_test_case "defer" "cleanup" defer_head() { @@ -104,7 +123,56 @@ defer_body() { - common_body defer + pfsynct_init + + epair_sync=$(vnet_mkepair) + epair_in=$(vnet_mkepair) + epair_out=$(vnet_mkepair) + + vnet_mkjail alcatraz ${epair_sync}a ${epair_in}a ${epair_out}a + + jexec alcatraz ifconfig ${epair_sync}a 192.0.2.1/24 up + jexec alcatraz ifconfig ${epair_out}a 198.51.100.1/24 up + jexec alcatraz ifconfig ${epair_in}a 203.0.113.1/24 up + jexec alcatraz arp -s 203.0.113.2 00:01:02:03:04:05 + jexec alcatraz sysctl net.inet.ip.forwarding=1 + + jexec alcatraz ifconfig pfsync0 \ + syncdev ${epair_sync}a \ + maxupd 1 \ + defer \ + up + + ifconfig ${epair_sync}b 192.0.2.2/24 up + ifconfig ${epair_out}b 198.51.100.2/24 up + ifconfig ${epair_in}b up + route add -net 203.0.113.0/24 198.51.100.1 + + # Enable pf + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "set skip on ${epair_sync}a" \ + "pass keep state" + + atf_check -s exit:0 env PYTHONPATH=${common_dir} \ + $(atf_get_srcdir)/pfsync_defer.py \ + --syncdev ${epair_sync}b \ + --indev ${epair_in}b \ + --outdev ${epair_out}b + + # Now disable defer mode and expect failure. + jexec alcatraz ifconfig pfsync0 -defer + + # Flush state + pft_set_rules alcatraz \ + "set skip on ${epair_sync}a" \ + "pass keep state" + + atf_check -s exit:1 env PYTHONPATH=${common_dir} \ + $(atf_get_srcdir)/pfsync_defer.py \ + --syncdev ${epair_sync}b \ + --indev ${epair_in}b \ + --outdev ${epair_out}b } defer_cleanup() @@ -182,6 +250,7 @@ atf_init_test_cases() { atf_add_test_case "basic" + atf_add_test_case "basic_defer" atf_add_test_case "defer" atf_add_test_case "bulk" } Index: tests/sys/netpfil/pf/pfsync_defer.py =================================================================== --- /dev/null +++ tests/sys/netpfil/pf/pfsync_defer.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2021 Rubicon Communications, LLC (Netgate) +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. +# + +import argparse +import logging +logging.getLogger("scapy").setLevel(logging.CRITICAL) +import scapy.all as sp +import socket +import sys +import time +from sniffer import Sniffer + +got_pfsync = False +got_ping = False +sent_ping = False + +def check_pfsync(args, packet): + global got_pfsync + global got_ping + + ip = packet.getlayer(sp.IP) + if not ip: + return False + + if ip.proto != 240: + return False + + # Only look at the first packet + if got_pfsync: + return False + + got_pfsync = time.time() + + return False + +def check_reply(args, packet): + global got_pfsync + global got_ping + + if not packet.getlayer(sp.ICMP): + return False + + # Only look at the first packet + if got_ping: + return False + + got_ping = time.time() + + return False + +def ping(intf): + global sent_ping + + ether = sp.Ether() + ip = sp.IP(dst="203.0.113.2", src="198.51.100.2") + icmp = sp.ICMP(type='echo-request') + raw = sp.raw(bytes.fromhex('00010203')) + + req = ether / ip / icmp / raw + sp.sendp(req, iface=intf, verbose=False) + sent_ping = time.time() + +def main(): + global got_pfsync + global got_ping + global sent_ping + + parser = argparse.ArgumentParser("pfsync_defer.py", + description="pfsync defer mode test") + parser.add_argument('--syncdev', nargs=1, + required=True, + help='The pfsync interface') + parser.add_argument('--outdev', nargs=1, + required=True, + help='The interface we will send packets on') + parser.add_argument('--indev', nargs=1, + required=True, + help='The interface we will receive packets on') + + args = parser.parse_args() + + syncmon = Sniffer(args, check_pfsync, args.syncdev[0]) + datamon = Sniffer(args, check_reply, args.indev[0]) + + # Send traffic on datadev, which should create state and produce a pfsync message + ping(args.outdev[0]) + + syncmon.join() + datamon.join() + + if not got_pfsync: + sys.exit(1) + + if not got_ping: + sys.exit(1) + + if got_pfsync > got_ping: + sys.exit(1) + + # Deferred packets are delayed up to 20ms (unless the pfsync peer, which we + # don't have here, acks their state update earlier) + if (sent_ping + 0.020) > got_ping: + sys.exit(1) + +if __name__ == '__main__': + main()